Skip to content

Commit

Permalink
.Net: Replace stj-schema-mapper source code with M.E.AI schema genera…
Browse files Browse the repository at this point in the history
…tion (#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
  • Loading branch information
eiriktsarpalis authored Dec 10, 2024
1 parent 494590f commit 88635e1
Show file tree
Hide file tree
Showing 25 changed files with 67 additions and 3,277 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using JsonSchemaMapper;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;

namespace Step04;
Expand All @@ -14,55 +11,18 @@ internal static class JsonSchemaGenerator
/// <summary>
/// Wrapper for generating a JSON schema as string from a .NET type.
/// </summary>
public static string FromType<SchemaType>()
public static string FromType<TSchemaType>()
{
JsonSerializerOptions options = new(JsonSerializerOptions.Default)
{
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
};
JsonSchemaMapperConfiguration config = new()
AIJsonSchemaCreateOptions config = new()
{
TreatNullObliviousAsNonNullable = true,
TransformSchemaNode = (context, schema) =>
{
// NOTE: This can be replaced with `IncludeAdditionalProperties = false` when upgraded to System.Json.Text 9.0.0
if (context.TypeInfo.Type == typeof(SchemaType))
{
schema["additionalProperties"] = false;
}

// Determine if a type or property and extract the relevant attribute provider
ICustomAttributeProvider? attributeProvider = context.PropertyInfo is not null
? context.PropertyInfo.AttributeProvider
: context.TypeInfo.Type;

// Look up any description attributes
DescriptionAttribute? descriptionAttr = attributeProvider?
.GetCustomAttributes(inherit: true)
.Select(attr => attr as DescriptionAttribute)
.FirstOrDefault(attr => attr is not null);

// Apply description attribute to the generated schema
if (descriptionAttr != null)
{
if (schema is not JsonObject jObj)
{
// Handle the case where the schema is a boolean
JsonValueKind valueKind = schema.GetValueKind();
schema = jObj = new JsonObject();
if (valueKind is JsonValueKind.False)
{
jObj.Add("not", true);
}
}

jObj["description"] = descriptionAttr.Description;
}

return schema;
}
IncludeSchemaKeyword = false,
DisallowAdditionalProperties = true,
};

return KernelJsonSchemaBuilder.Build(typeof(SchemaType), "Intent Result", config).AsJson();
return KernelJsonSchemaBuilder.Build(typeof(TSchemaType), "Intent Result", config).AsJson();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParamete
{ "type": "object",
"required": ["param1", "param2"],
"properties": {
"param1": { "type": "string", "description": "String param 1" },
"param2": { "type": "integer", "description": "Int param 2" } } }
"param1": { "description": "String param 1", "type": "string" },
"param2": { "description": "Int param 2" , "type": "integer"} } }
""";

KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[]
Expand Down Expand Up @@ -126,8 +126,8 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParame
{ "type": "object",
"required": ["param1", "param2"],
"properties": {
"param1": { "type": "string", "description": "String param 1" },
"param2": { "type": "integer", "description": "Int param 2" } } }
"param1": { "description": "String param 1", "type": "string" },
"param2": { "description": "Int param 2", "type": "integer"} } }
""";

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

// Assert
Assert.Equal(
"""{"type":"object","required":[],"properties":{"param1":{"type":"string","description":"something neat"}}}""",
"""{"type":"object","required":[],"properties":{"param1":{"description":"something neat","type":"string"}}}""",
JsonSerializer.Serialize(result.Parameters));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public void ItCanCreateValidGeminiFunctionManualForPlugin()
// Assert
Assert.NotNull(result);
Assert.Equal(
"""{"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"}}}""",
"""{"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"}}}""",
JsonSerializer.Serialize(result.Parameters)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public void ItCanConvertToFunctionDefinitionWithPluginName()
[Fact]
public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParameterType()
{
string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """;
string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "description": "String param 1", "type": "string" }, "param2": { "description": "Int param 2", "type": "integer" } } } """;

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

KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[]
{
Expand Down Expand Up @@ -174,7 +174,7 @@ public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescript
Assert.NotNull(pd.properties);
Assert.Single(pd.properties);
Assert.Equal(
JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"string", "description":"something neat" }""")),
JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "description":"something neat", "type":"string" }""")),
JsonSerializer.Serialize(pd.properties.First().Value.RootElement));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,23 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Text.Json;
using JsonSchemaMapper;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Xunit;

namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core;

/// <summary>
/// Unit tests for <see cref="OpenAIJsonSchemaTransformer"/> class.
/// Unit tests for schema transformations used by OpenAI clients.
/// </summary>
public sealed class OpenAIJsonSchemaTransformerTests
{
private static readonly JsonSchemaMapperConfiguration s_jsonSchemaMapperConfiguration = new()
private static readonly AIJsonSchemaCreateOptions s_jsonSchemaCreateOptions = new()
{
IncludeSchemaVersion = false,
IncludeTypeInEnums = true,
TreatNullObliviousAsNonNullable = true,
TransformSchemaNode = OpenAIJsonSchemaTransformer.Transform,
IncludeSchemaKeyword = false,
IncludeTypeInEnumSchemas = true,
DisallowAdditionalProperties = true,
RequireAllProperties = true,
};

private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
Expand Down Expand Up @@ -124,7 +123,7 @@ public void ItTransformsJsonSchemaCorrectly()
""";

// Act
var schema = KernelJsonSchemaBuilder.Build(type, configuration: s_jsonSchemaMapperConfiguration);
var schema = KernelJsonSchemaBuilder.Build(type, configuration: s_jsonSchemaCreateOptions);

// Assert
Assert.Equal(NormalizeJson(expectedSchema), NormalizeJson(schema.ToString()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ public void ItCanCreateValidAzureOpenAIFunctionManualForPlugin()
// Assert
Assert.NotNull(result);
Assert.Equal(
"""{"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"}}}""",
"""{"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"}}}""",
result.FunctionParameters.ToString()
);
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System;
using System.Text;
using System.Text.Json;
using JsonSchemaMapper;
using OpenAI.Chat;

namespace Microsoft.SemanticKernel.Connectors.OpenAI;
Expand All @@ -14,14 +13,14 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI;
internal static class OpenAIChatResponseFormatBuilder
{
/// <summary>
/// <see cref="JsonSchemaMapperConfiguration"/> for JSON schema format for structured outputs.
/// <see cref="Microsoft.Extensions.AI.AIJsonSchemaCreateOptions"/> for JSON schema format for structured outputs.
/// </summary>
private static readonly JsonSchemaMapperConfiguration s_jsonSchemaMapperConfiguration = new()
private static readonly Microsoft.Extensions.AI.AIJsonSchemaCreateOptions s_jsonSchemaCreateOptions = new()
{
IncludeSchemaVersion = false,
IncludeTypeInEnums = true,
TreatNullObliviousAsNonNullable = true,
TransformSchemaNode = OpenAIJsonSchemaTransformer.Transform
IncludeSchemaKeyword = false,
IncludeTypeInEnumSchemas = true,
DisallowAdditionalProperties = true,
RequireAllProperties = true,
};

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

var schema = KernelJsonSchemaBuilder.Build(type, configuration: s_jsonSchemaMapperConfiguration);
var schema = KernelJsonSchemaBuilder.Build(type, configuration: s_jsonSchemaCreateOptions);
var schemaBinaryData = BinaryData.FromString(schema.ToString());

var typeName = GetTypeName(type);
Expand Down
9 changes: 0 additions & 9 deletions dotnet/src/InternalUtilities/src/Schema/.editorconfig

This file was deleted.

Loading

0 comments on commit 88635e1

Please sign in to comment.