Skip to content

Commit 2e45b65

Browse files
eiriktsarpalisjeffhandley
authored andcommitted
Add an AIJsonSchemaTransformOptions property inside AIJsonSchemaCreateOptions and mark redundant properties in the latter as obsolete. (#6427)
* Add an AIJsonSchemaTransformOptions property inside AIJsonSchemaCreateOptions and mark redundant properties in the latter as obsolete. * s/inferred/created
1 parent 6fd42db commit 2e45b65

File tree

5 files changed

+90
-19
lines changed

5 files changed

+90
-19
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonSchemaTransformOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ public sealed record class AIJsonSchemaTransformOptions
3838
/// </summary>
3939
public bool UseNullableKeyword { get; init; }
4040

41+
/// <summary>
42+
/// Gets a value indicating whether to move the default keyword to the description field in the schema.
43+
/// </summary>
44+
public bool MoveDefaultKeywordToDescription { get; init; }
45+
4146
/// <summary>
4247
/// Gets the default options instance.
4348
/// </summary>

src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Transform.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@ public static JsonElement TransformSchema(JsonElement schema, AIJsonSchemaTransf
3030
}
3131

3232
JsonNode? nodeSchema = JsonSerializer.SerializeToNode(schema, JsonContext.Default.JsonElement);
33+
JsonNode transformedSchema = TransformSchema(nodeSchema, transformOptions);
34+
return JsonSerializer.SerializeToElement(transformedSchema, JsonContextNoIndentation.Default.JsonNode);
35+
}
36+
37+
private static JsonNode TransformSchema(JsonNode? schema, AIJsonSchemaTransformOptions transformOptions)
38+
{
3339
List<string>? path = transformOptions.TransformSchemaNode is not null ? [] : null;
34-
JsonNode transformedSchema = TransformSchemaCore(nodeSchema, transformOptions, path);
35-
return JsonSerializer.Deserialize(transformedSchema, JsonContext.Default.JsonElement);
40+
return TransformSchemaCore(schema, transformOptions, path);
3641
}
3742

3843
private static JsonNode TransformSchemaCore(JsonNode? schema, AIJsonSchemaTransformOptions transformOptions, List<string>? path)
@@ -169,6 +174,18 @@ private static JsonNode TransformSchemaCore(JsonNode? schema, AIJsonSchemaTransf
169174
}
170175
}
171176

177+
if (transformOptions.MoveDefaultKeywordToDescription &&
178+
schemaObj.TryGetPropertyValue(DefaultPropertyName, out JsonNode? defaultSchema))
179+
{
180+
string? description = schemaObj.TryGetPropertyValue(DescriptionPropertyName, out JsonNode? descriptionSchema) ? descriptionSchema?.GetValue<string>() : null;
181+
string defaultValueJson = JsonSerializer.Serialize(defaultSchema, JsonContextNoIndentation.Default.JsonNode!);
182+
description = description is null
183+
? $"Default value: {defaultValueJson}"
184+
: $"{description} (Default value: {defaultValueJson})";
185+
schemaObj[DescriptionPropertyName] = description;
186+
_ = schemaObj.Remove(DefaultPropertyName);
187+
}
188+
172189
break;
173190

174191
default:

src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ internal sealed class AzureAIInferenceChatClient : IChatClient
2929
{
3030
RequireAllProperties = true,
3131
DisallowAdditionalProperties = true,
32-
ConvertBooleanSchemas = true
32+
ConvertBooleanSchemas = true,
33+
MoveDefaultKeywordToDescription = true,
3334
});
3435

3536
/// <summary>Metadata about the client.</summary>

src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ internal sealed partial class OpenAIChatClient : IChatClient
3030
{
3131
RequireAllProperties = true,
3232
DisallowAdditionalProperties = true,
33-
ConvertBooleanSchemas = true
33+
ConvertBooleanSchemas = true,
34+
MoveDefaultKeywordToDescription = true,
3435
});
3536

3637
/// <summary>Gets the default OpenAI endpoint.</summary>

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
using Microsoft.Extensions.AI.JsonSchemaExporter;
1616
using Xunit;
1717

18+
#pragma warning disable 0618 // Suppress obsolete warnings
19+
1820
namespace Microsoft.Extensions.AI;
1921

2022
public static partial class AIJsonUtilitiesTests
@@ -72,10 +74,11 @@ public static void AIJsonSchemaCreateOptions_DefaultInstance_ReturnsExpectedValu
7274
{
7375
AIJsonSchemaCreateOptions options = useSingleton ? AIJsonSchemaCreateOptions.Default : new AIJsonSchemaCreateOptions();
7476
Assert.True(options.IncludeTypeInEnumSchemas);
75-
Assert.True(options.DisallowAdditionalProperties);
77+
Assert.False(options.DisallowAdditionalProperties);
7678
Assert.False(options.IncludeSchemaKeyword);
7779
Assert.False(options.RequireAllProperties);
7880
Assert.Null(options.TransformSchemaNode);
81+
Assert.Null(options.TransformOptions);
7982
}
8083
8184
[Fact]
@@ -106,6 +109,12 @@ public static void AIJsonSchemaCreateOptions_UsesStructuralEquality()
106109
property.SetValue(options2, includeParameter);
107110
break;
108111
112+
case null when property.PropertyType == typeof(AIJsonSchemaTransformOptions):
113+
AIJsonSchemaTransformOptions transformOptions = new AIJsonSchemaTransformOptions { RequireAllProperties = true };
114+
property.SetValue(options1, transformOptions);
115+
property.SetValue(options2, transformOptions);
116+
break;
117+
109118
default:
110119
Assert.Fail($"Unexpected property type: {property.PropertyType}");
111120
break;
@@ -152,8 +161,7 @@ public static void CreateJsonSchema_DefaultParameters_GeneratesExpectedJsonSchem
152161
"default": "defaultValue"
153162
}
154163
},
155-
"required": ["Key", "EnumValue"],
156-
"additionalProperties": false
164+
"required": ["Key", "EnumValue"]
157165
}
158166
""").RootElement;
159167

@@ -176,23 +184,28 @@ public static void CreateJsonSchema_OverriddenParameters_GeneratesExpectedJsonSc
176184
"type": "integer"
177185
},
178186
"EnumValue": {
187+
"type": "string",
179188
"enum": ["A", "B"]
180189
},
181190
"Value": {
182191
"description": "Default value: \"defaultValue\"",
183192
"type": ["string", "null"]
184193
}
185194
},
186-
"required": ["Key", "EnumValue", "Value"]
195+
"required": ["Key", "EnumValue", "Value"],
196+
"additionalProperties": false
187197
}
188198
""").RootElement;
189199

190200
AIJsonSchemaCreateOptions inferenceOptions = new AIJsonSchemaCreateOptions
191201
{
192-
IncludeTypeInEnumSchemas = false,
193-
DisallowAdditionalProperties = false,
194202
IncludeSchemaKeyword = true,
195-
RequireAllProperties = true,
203+
TransformOptions = new()
204+
{
205+
DisallowAdditionalProperties = true,
206+
RequireAllProperties = true,
207+
MoveDefaultKeywordToDescription = true,
208+
}
196209
};
197210

198211
JsonElement actual = AIJsonUtilities.CreateJsonSchema(
@@ -227,8 +240,7 @@ public static void CreateJsonSchema_UserDefinedTransformer()
227240
"default": "defaultValue"
228241
}
229242
},
230-
"required": ["Key", "EnumValue"],
231-
"additionalProperties": false
243+
"required": ["Key", "EnumValue"]
232244
}
233245
""").RootElement;
234246

@@ -268,8 +280,7 @@ public static void CreateJsonSchema_FiltersDisallowedKeywords()
268280
"Char" : {
269281
"type": "string"
270282
}
271-
},
272-
"additionalProperties": false
283+
}
273284
}
274285
""").RootElement;
275286

@@ -341,14 +352,23 @@ public static void CreateFunctionJsonSchema_OptionalParameters(bool requireAllPr
341352
}
342353
""").RootElement;
343354

355+
AIJsonSchemaCreateOptions inferenceOptions = new()
356+
{
357+
TransformOptions = new()
358+
{
359+
RequireAllProperties = requireAllProperties,
360+
MoveDefaultKeywordToDescription = requireAllProperties,
361+
}
362+
};
363+
344364
AIFunction func = AIFunctionFactory.Create((
345365
[Description("The city to get the weather for")] string city,
346366
[Description("The unit to calculate the current temperature to")] string unit = "celsius") => "sunny",
347367
new AIFunctionFactoryOptions
348368
{
349369
Name = "get_weather",
350370
Description = "Gets the current weather for a current location",
351-
JsonSchemaCreateOptions = new AIJsonSchemaCreateOptions { RequireAllProperties = requireAllProperties }
371+
JsonSchemaCreateOptions = inferenceOptions
352372
});
353373

354374
Assert.NotNull(func.UnderlyingMethod);
@@ -358,7 +378,7 @@ public static void CreateFunctionJsonSchema_OptionalParameters(bool requireAllPr
358378
func.UnderlyingMethod,
359379
title: func.Name,
360380
description: func.Description,
361-
inferenceOptions: new AIJsonSchemaCreateOptions { RequireAllProperties = requireAllProperties });
381+
inferenceOptions: inferenceOptions);
362382
AssertDeepEquals(expected, resolvedSchema);
363383
}
364384

@@ -423,7 +443,7 @@ public static void CreateJsonSchema_ValidateWithTestData(ITestData testData)
423443

424444
JsonTypeInfo typeInfo = options.GetTypeInfo(testData.Type);
425445
AIJsonSchemaCreateOptions? createOptions = typeInfo.Properties.Any(prop => prop.IsExtensionData)
426-
? new() { DisallowAdditionalProperties = false } // Do not append additionalProperties: false to the schema if the type has extension data.
446+
? new() { TransformOptions = new() { DisallowAdditionalProperties = false } } // Do not append additionalProperties: false to the schema if the type has extension data.
427447
: null;
428448

429449
JsonElement schema = AIJsonUtilities.CreateJsonSchema(testData.Type, serializerOptions: options, inferenceOptions: createOptions);
@@ -706,6 +726,33 @@ public static void TransformJsonSchema_UseNullableKeyword()
706726
AssertDeepEquals(expectedSchema, transformedSchema);
707727
}
708728

729+
[Fact]
730+
public static void TransformJsonSchema_MoveDefaultKeywordToDescription()
731+
{
732+
JsonElement schema = JsonDocument.Parse("""
733+
{
734+
"description": "My awesome schema",
735+
"type": "array",
736+
"default": [1,2,3]
737+
}
738+
""").RootElement;
739+
740+
JsonElement expectedSchema = JsonDocument.Parse("""
741+
{
742+
"description": "My awesome schema (Default value: [1,2,3])",
743+
"type": "array"
744+
}
745+
""").RootElement;
746+
747+
AIJsonSchemaTransformOptions options = new()
748+
{
749+
MoveDefaultKeywordToDescription = true,
750+
};
751+
752+
JsonElement transformedSchema = AIJsonUtilities.TransformSchema(schema, options);
753+
AssertDeepEquals(expectedSchema, transformedSchema);
754+
}
755+
709756
[Theory]
710757
[MemberData(nameof(TestTypes.GetTestDataUsingAllValues), MemberType = typeof(TestTypes))]
711758
public static void TransformJsonSchema_ValidateWithTestData(ITestData testData)
@@ -718,7 +765,7 @@ public static void TransformJsonSchema_ValidateWithTestData(ITestData testData)
718765

719766
JsonTypeInfo typeInfo = options.GetTypeInfo(testData.Type);
720767
AIJsonSchemaCreateOptions? createOptions = typeInfo.Properties.Any(prop => prop.IsExtensionData)
721-
? new() { DisallowAdditionalProperties = false } // Do not append additionalProperties: false to the schema if the type has extension data.
768+
? new() { TransformOptions = new() { DisallowAdditionalProperties = false } } // Do not append additionalProperties: false to the schema if the type has extension data.
722769
: null;
723770

724771
JsonElement schema = AIJsonUtilities.CreateJsonSchema(testData.Type, serializerOptions: options, inferenceOptions: createOptions);

0 commit comments

Comments
 (0)