Skip to content

Commit 88635e1

Browse files
.Net: Replace stj-schema-mapper source code with M.E.AI schema generation (#9807)
Replaces the source copied implementation from https://github.com/eiriktsarpalis/stj-schema-mapper with the equivalent component now made available through Microsoft.Extensions.AI.Abstractions. cc @RogerBarreto @stephentoub @SergeyMenshykh
1 parent 494590f commit 88635e1

25 files changed

+67
-3277
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
2-
using System.ComponentModel;
3-
using System.Reflection;
42
using System.Text.Json;
5-
using System.Text.Json.Nodes;
63
using System.Text.Json.Serialization;
7-
using JsonSchemaMapper;
4+
using Microsoft.Extensions.AI;
85
using Microsoft.SemanticKernel;
96

107
namespace Step04;
@@ -14,55 +11,18 @@ internal static class JsonSchemaGenerator
1411
/// <summary>
1512
/// Wrapper for generating a JSON schema as string from a .NET type.
1613
/// </summary>
17-
public static string FromType<SchemaType>()
14+
public static string FromType<TSchemaType>()
1815
{
1916
JsonSerializerOptions options = new(JsonSerializerOptions.Default)
2017
{
2118
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
2219
};
23-
JsonSchemaMapperConfiguration config = new()
20+
AIJsonSchemaCreateOptions config = new()
2421
{
25-
TreatNullObliviousAsNonNullable = true,
26-
TransformSchemaNode = (context, schema) =>
27-
{
28-
// NOTE: This can be replaced with `IncludeAdditionalProperties = false` when upgraded to System.Json.Text 9.0.0
29-
if (context.TypeInfo.Type == typeof(SchemaType))
30-
{
31-
schema["additionalProperties"] = false;
32-
}
33-
34-
// Determine if a type or property and extract the relevant attribute provider
35-
ICustomAttributeProvider? attributeProvider = context.PropertyInfo is not null
36-
? context.PropertyInfo.AttributeProvider
37-
: context.TypeInfo.Type;
38-
39-
// Look up any description attributes
40-
DescriptionAttribute? descriptionAttr = attributeProvider?
41-
.GetCustomAttributes(inherit: true)
42-
.Select(attr => attr as DescriptionAttribute)
43-
.FirstOrDefault(attr => attr is not null);
44-
45-
// Apply description attribute to the generated schema
46-
if (descriptionAttr != null)
47-
{
48-
if (schema is not JsonObject jObj)
49-
{
50-
// Handle the case where the schema is a boolean
51-
JsonValueKind valueKind = schema.GetValueKind();
52-
schema = jObj = new JsonObject();
53-
if (valueKind is JsonValueKind.False)
54-
{
55-
jObj.Add("not", true);
56-
}
57-
}
58-
59-
jObj["description"] = descriptionAttr.Description;
60-
}
61-
62-
return schema;
63-
}
22+
IncludeSchemaKeyword = false,
23+
DisallowAdditionalProperties = true,
6424
};
6525

66-
return KernelJsonSchemaBuilder.Build(typeof(SchemaType), "Intent Result", config).AsJson();
26+
return KernelJsonSchemaBuilder.Build(typeof(TSchemaType), "Intent Result", config).AsJson();
6727
}
6828
}

dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiFunctionTests.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParamete
9595
{ "type": "object",
9696
"required": ["param1", "param2"],
9797
"properties": {
98-
"param1": { "type": "string", "description": "String param 1" },
99-
"param2": { "type": "integer", "description": "Int param 2" } } }
98+
"param1": { "description": "String param 1", "type": "string" },
99+
"param2": { "description": "Int param 2" , "type": "integer"} } }
100100
""";
101101

102102
KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[]
@@ -126,8 +126,8 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParame
126126
{ "type": "object",
127127
"required": ["param1", "param2"],
128128
"properties": {
129-
"param1": { "type": "string", "description": "String param 1" },
130-
"param2": { "type": "integer", "description": "Int param 2" } } }
129+
"param1": { "description": "String param 1", "type": "string" },
130+
"param2": { "description": "Int param 2", "type": "integer"} } }
131131
""";
132132

133133
KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[]
@@ -180,7 +180,7 @@ public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescript
180180

181181
// Assert
182182
Assert.Equal(
183-
"""{"type":"object","required":[],"properties":{"param1":{"type":"string","description":"something neat"}}}""",
183+
"""{"type":"object","required":[],"properties":{"param1":{"description":"something neat","type":"string"}}}""",
184184
JsonSerializer.Serialize(result.Parameters));
185185
}
186186
}

dotnet/src/Connectors/Connectors.Google.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public void ItCanCreateValidGeminiFunctionManualForPlugin()
200200
// Assert
201201
Assert.NotNull(result);
202202
Assert.Equal(
203-
"""{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"type":"string","enum":["Value1","Value2"],"description":"Enum parameter"},"parameter3":{"type":"string","format":"date-time","description":"DateTime parameter"}}}""",
203+
"""{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"description":"String parameter","type":"string"},"parameter2":{"description":"Enum parameter","type":"string","enum":["Value1","Value2"]},"parameter3":{"description":"DateTime parameter","type":"string"}}}""",
204204
JsonSerializer.Serialize(result.Parameters)
205205
);
206206
}

dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIFunctionTests.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public void ItCanConvertToFunctionDefinitionWithPluginName()
9292
[Fact]
9393
public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParameterType()
9494
{
95-
string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """;
95+
string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "description": "String param 1", "type": "string" }, "param2": { "description": "Int param 2", "type": "integer" } } } """;
9696

9797
KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[]
9898
{
@@ -118,7 +118,7 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParamete
118118
[Fact]
119119
public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParameterType()
120120
{
121-
string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """;
121+
string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "description": "String param 1", "type": "string" }, "param2": { "description": "Int param 2", "type": "integer" } } } """;
122122

123123
KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[]
124124
{
@@ -174,7 +174,7 @@ public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescript
174174
Assert.NotNull(pd.properties);
175175
Assert.Single(pd.properties);
176176
Assert.Equal(
177-
JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"string", "description":"something neat" }""")),
177+
JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "description":"something neat", "type":"string" }""")),
178178
JsonSerializer.Serialize(pd.properties.First().Value.RootElement));
179179
}
180180

dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIJsonSchemaTransformerTests.cs

+8-9
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,23 @@
33
using System.Collections.Generic;
44
using System.ComponentModel;
55
using System.Text.Json;
6-
using JsonSchemaMapper;
6+
using Microsoft.Extensions.AI;
77
using Microsoft.SemanticKernel;
8-
using Microsoft.SemanticKernel.Connectors.OpenAI;
98
using Xunit;
109

1110
namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core;
1211

1312
/// <summary>
14-
/// Unit tests for <see cref="OpenAIJsonSchemaTransformer"/> class.
13+
/// Unit tests for schema transformations used by OpenAI clients.
1514
/// </summary>
1615
public sealed class OpenAIJsonSchemaTransformerTests
1716
{
18-
private static readonly JsonSchemaMapperConfiguration s_jsonSchemaMapperConfiguration = new()
17+
private static readonly AIJsonSchemaCreateOptions s_jsonSchemaCreateOptions = new()
1918
{
20-
IncludeSchemaVersion = false,
21-
IncludeTypeInEnums = true,
22-
TreatNullObliviousAsNonNullable = true,
23-
TransformSchemaNode = OpenAIJsonSchemaTransformer.Transform,
19+
IncludeSchemaKeyword = false,
20+
IncludeTypeInEnumSchemas = true,
21+
DisallowAdditionalProperties = true,
22+
RequireAllProperties = true,
2423
};
2524

2625
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
@@ -124,7 +123,7 @@ public void ItTransformsJsonSchemaCorrectly()
124123
""";
125124

126125
// Act
127-
var schema = KernelJsonSchemaBuilder.Build(type, configuration: s_jsonSchemaMapperConfiguration);
126+
var schema = KernelJsonSchemaBuilder.Build(type, configuration: s_jsonSchemaCreateOptions);
128127

129128
// Assert
130129
Assert.Equal(NormalizeJson(expectedSchema), NormalizeJson(schema.ToString()));

dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ public void ItCanCreateValidAzureOpenAIFunctionManualForPlugin()
196196
// Assert
197197
Assert.NotNull(result);
198198
Assert.Equal(
199-
"""{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"type":"string","enum":["Value1","Value2"],"description":"Enum parameter"},"parameter3":{"type":"string","format":"date-time","description":"DateTime parameter"}}}""",
199+
"""{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"description":"String parameter","type":"string"},"parameter2":{"description":"Enum parameter","type":"string","enum":["Value1","Value2"]},"parameter3":{"description":"DateTime parameter","type":"string"}}}""",
200200
result.FunctionParameters.ToString()
201201
);
202202
}

dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIJsonSchemaTransformer.cs

-71
This file was deleted.

dotnet/src/Connectors/Connectors.OpenAI/Helpers/OpenAIChatResponseFormatBuilder.cs

+7-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System;
44
using System.Text;
55
using System.Text.Json;
6-
using JsonSchemaMapper;
76
using OpenAI.Chat;
87

98
namespace Microsoft.SemanticKernel.Connectors.OpenAI;
@@ -14,14 +13,14 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI;
1413
internal static class OpenAIChatResponseFormatBuilder
1514
{
1615
/// <summary>
17-
/// <see cref="JsonSchemaMapperConfiguration"/> for JSON schema format for structured outputs.
16+
/// <see cref="Microsoft.Extensions.AI.AIJsonSchemaCreateOptions"/> for JSON schema format for structured outputs.
1817
/// </summary>
19-
private static readonly JsonSchemaMapperConfiguration s_jsonSchemaMapperConfiguration = new()
18+
private static readonly Microsoft.Extensions.AI.AIJsonSchemaCreateOptions s_jsonSchemaCreateOptions = new()
2019
{
21-
IncludeSchemaVersion = false,
22-
IncludeTypeInEnums = true,
23-
TreatNullObliviousAsNonNullable = true,
24-
TransformSchemaNode = OpenAIJsonSchemaTransformer.Transform
20+
IncludeSchemaKeyword = false,
21+
IncludeTypeInEnumSchemas = true,
22+
DisallowAdditionalProperties = true,
23+
RequireAllProperties = true,
2524
};
2625

2726
/// <summary>
@@ -56,7 +55,7 @@ internal static ChatResponseFormat GetJsonSchemaResponseFormat(Type formatObject
5655
{
5756
var type = formatObjectType.IsGenericType && formatObjectType.GetGenericTypeDefinition() == typeof(Nullable<>) ? Nullable.GetUnderlyingType(formatObjectType)! : formatObjectType;
5857

59-
var schema = KernelJsonSchemaBuilder.Build(type, configuration: s_jsonSchemaMapperConfiguration);
58+
var schema = KernelJsonSchemaBuilder.Build(type, configuration: s_jsonSchemaCreateOptions);
6059
var schemaBinaryData = BinaryData.FromString(schema.ToString());
6160

6261
var typeName = GetTypeName(type);

dotnet/src/InternalUtilities/src/Schema/.editorconfig

-9
This file was deleted.

0 commit comments

Comments
 (0)