Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e101bac
fix: Json Schema Generation, used System.Text.Json JsonSchema generat…
Mar 9, 2025
e289da0
fix: warning suppressions
Mar 9, 2025
315af73
feat: Added FunctionTool attribute, which can be used to convert indi…
Mar 10, 2025
36cf256
feat: Added GoogleFunctionTool optional parameters in GenerateJsonSch…
Mar 10, 2025
904bbf0
Merge branch 'tryAGI:main' into main
gunpal5 Mar 10, 2025
6bfd9b7
Merge remote-tracking branch 'origin/main'
Mar 10, 2025
1ebee3f
Updated README.md
Mar 10, 2025
46a569b
fix: schema generation
Mar 10, 2025
d245705
Merge remote-tracking branch 'origin/main'
Mar 10, 2025
8071875
removed uncessary variable
Mar 10, 2025
234c61e
feat: Added M.E.A.I AIFunction generation
Mar 10, 2025
d8a5f36
Simplify description retrieval in SchemaSubsetHelper.
Mar 10, 2025
4075971
Refactor MeaiFunction.cs for improved readability
Mar 10, 2025
28da355
Refactor MeaiFunction to enhance clarity and functionality.
Mar 10, 2025
8830a3f
Update GoogleFunctionTool type references in generator
Mar 10, 2025
6243888
fix: Native AOT for Method Function Tools
Mar 10, 2025
972b9c5
Merge remote-tracking branch 'origin/main'
Mar 10, 2025
b49d3d2
Add snapshot files for ToolsJsonSerializerContext tests
Mar 10, 2025
e30dd35
Disable unused test method in SnapshotTests
Mar 10, 2025
d896319
Merge remote-tracking branch 'origin/main'
Mar 12, 2025
5de972c
Merge remote-tracking branch 'origin/main'
Mar 12, 2025
8c68191
fix: MeaiFunction ignoring simple values types arguments
Mar 12, 2025
b9e53e0
remove unused codes.
Mar 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CSharpToJsonSchema.sln
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Global
{8AFCC30C-C59D-498D-BE68-9A328B3E5599} = {AAA11B78-2764-4520-A97E-46AA7089A588}
{247C813A-9072-4DF3-B403-B35E3688DB4B} = {AAA11B78-2764-4520-A97E-46AA7089A588}
{6167F915-83EB-42F9-929B-AD4719A55811} = {AAA11B78-2764-4520-A97E-46AA7089A588}
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92} = {AAA11B78-2764-4520-A97E-46AA7089A588}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CED9A020-DBA5-4BE6-8096-75E528648EC1}
Expand Down
70 changes: 0 additions & 70 deletions src/libs/CSharpToJsonSchema.Generators/Conversion/ToModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ public static InterfaceData PrepareData(
IsVoid: x.ReturnsVoid || x.ReturnType.MetadataName == "Task",
IsStrict: isStrict,
Parameters: parameters.Select(static y => y).ToArray(),
Descriptions: parameters.Select(static l => GetParameterDescriptions(l)).SelectMany(s => s)
.ToDictionary(s => s.Key, s => s.Value),
ReturnType: x.ReturnType
);
})
Expand Down Expand Up @@ -91,8 +89,6 @@ public static InterfaceData PrepareMethodData(
IsVoid: x.ReturnsVoid || x.ReturnType.MetadataName == "Task",
IsStrict: isStrict,
Parameters: parameters.Select(static y => y).ToArray(),
Descriptions: parameters.Select(static l => GetParameterDescriptions(l)).SelectMany(s => s)
.ToDictionary(s => s.Key, s => s.Value),
ReturnType:x.ReturnType
);
methodList.Add(methodData);
Expand Down Expand Up @@ -140,72 +136,6 @@ public static InterfaceData PrepareMethodData(

return string.Join(".", commonParts);
}


// private static Dictionary<string, bool> GetIsRequired(IParameterSymbol[] parameters, Dictionary<string, bool>? dics = null)
// {
// dics ??= new Dictionary<string, bool>();
//
// foreach (var parameter in parameters)
// {
// if (dics.TryAdd(parameter.Name, IsRequired(parameter)))
// {
// if (parameter is IParameterSymbol namedTypeSymbol)
// {
// GetIsRequired(namedTypeSymbol.Type.GetMembers().OfType<IPropertySymbol>().ToArray(),dics)
// }
// }
// }
//
// return dics;
// }
//
// private static bool IsRequired(ISymbol parameter)
// {
// return false;
// //parameter.GetAttributes().OfType<global::System.ComponentModel.D.RequiredAttribute>()
// }

private static List<KeyValuePair<string, string>> GetParameterDescriptions(IParameterSymbol parameters,
Dictionary<string, string>? dics = null)
{
dics ??= new Dictionary<string, string>();


if (dics.TryAdd(parameters.Name.ToCamelCase(), GetDescription(parameters)))
{
if (parameters is IParameterSymbol namedTypeSymbol)
{
GetParameterDescriptions(namedTypeSymbol.Type.GetMembers().OfType<IPropertySymbol>().ToArray(), dics);
}
}

return dics.Select(x => new KeyValuePair<string, string>(x.Key, x.Value)).ToList();
}

private static Dictionary<string, string> GetParameterDescriptions(IPropertySymbol[] parameters,
Dictionary<string, string>? dics = null)
{
dics ??= new Dictionary<string, string>();

foreach (var parameter in parameters)
{
var description = GetDescription(parameter);
if (string.IsNullOrWhiteSpace(description)) continue;

if (dics.TryAdd(parameter.Name, description))
{
if (parameter is IPropertySymbol namedTypeSymbol)
{
GetParameterDescriptions(namedTypeSymbol.Type.GetMembers().OfType<IPropertySymbol>().ToArray(),
dics);
}
}
}

return dics;
}

private static OpenApiSchema ToParameterData(ITypeSymbol typeSymbol, string? name = null,
string? description = null, bool isRequired = true)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ public readonly record struct MethodData(
bool IsVoid,
bool IsStrict,
IParameterSymbol[] Parameters,
Dictionary<string, string> Descriptions,
ITypeSymbol ReturnType);

public readonly record struct PropertyMetadata(string Name, string Description, bool IsRequired);
2 changes: 1 addition & 1 deletion src/libs/CSharpToJsonSchema.Generators/Sources.Tools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ private static string GetDictionaryString(MethodData data)
{
StringBuilder sb = new StringBuilder();

var methodDescriptions = data.Descriptions;
var methodDescriptions = new Dictionary<string, string>();
methodDescriptions.Add("MainFunction_Desc", data.Description);
sb.Append("{");
var lst = methodDescriptions.Select(s => $"\"{s.Key.ToCamelCase()}\":\"{s.Value}\"");
Expand Down
2 changes: 2 additions & 0 deletions src/libs/CSharpToJsonSchema/MeaiFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ private string GetArgsString(IEnumerable<KeyValuePair<string, object?>> argument
jsonObject[args.Key] = JsonArray.Create(element);
else if (element.ValueKind == JsonValueKind.Object)
jsonObject[args.Key] = JsonObject.Create(element);
else
jsonObject[args.Key] = JsonValue.Create(element);
}
else if (args.Value is JsonNode node)
{
Expand Down
219 changes: 91 additions & 128 deletions src/libs/CSharpToJsonSchema/SchemaSubsetHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Text.Json.Nodes;
using System.ComponentModel;
using System.Reflection;
using System.Text.Json.Nodes;
using System.Text.Json.Schema;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
Expand All @@ -7,151 +9,120 @@ namespace CSharpToJsonSchema;

public static class SchemaBuilder
{
/// <summary>
/// Converts a JSON document that contains valid json schema <see href="https://json-schema.org/specification"/> as e.g.
/// generated by <code>Microsoft.Extensions.AI.AIJsonUtilities.CreateJsonSchema</code> or <code>JsonSchema.Net</code>'s
/// <see cref="JsonSchemaBuilder"/> to a subset that is compatible with LLM's APIs.
/// </summary>
/// <param name="constructedSchema">Generated, valid json schema.</param>
/// <returns>Subset of the given json schema in a LLM-compatible format.</returns>
public static OpenApiSchema ConvertToCompatibleSchemaSubset(JsonNode node)
{
ConvertNullableProperties(node);
var x1 = node;
var x2 = x1.ToJsonString();
var schema = JsonSerializer.Deserialize(x2, OpenApiSchemaJsonContext.Default.OpenApiSchema);
return schema;
}

private static void ConvertNullableProperties(JsonNode? node)
{
// If the node is an object, look for a "type" property or nested definitions
if (node is JsonObject obj)
{
// If "type" is an array, remove "null" and collapse if it leaves only one type
if (obj.TryGetPropertyValue("type", out var typeValue) && typeValue is JsonArray array)
{
if (array.Count == 2)
{
var notNullTypes = array.Where(x => x is not null && x.GetValue<string>() != "null").ToList();
if (notNullTypes.Count == 1)
{
obj["type"] = notNullTypes[0]!.GetValue<string>();
obj["nullable"] = true;
}
else
{
throw new InvalidOperationException(
$"LLM's API for strucutured output requires every property to have one defined type, not multiple options. Path: {obj.GetPath()} Schema: {obj.ToJsonString()}");
}
}
else if (array.Count > 2)
{
throw new InvalidOperationException(
$"LLM's API for strucutured output requires every property to have one defined type, not multiple options. Path: {obj.GetPath()} Schema: {obj.ToJsonString()}");
}
}

// Recursively convert any nested schema in "properties"
if (obj.TryGetPropertyValue("properties", out var propertiesNode) &&
propertiesNode is JsonObject propertiesObj)
{
foreach (var property in propertiesObj)
{
ConvertNullableProperties(property.Value);
}
}

if (obj.TryGetPropertyValue("type", out var newTypeValue)
&& newTypeValue is JsonNode
&& newTypeValue.GetValueKind() == JsonValueKind.String
&& "object".Equals(newTypeValue.GetValue<string>(), StringComparison.OrdinalIgnoreCase)
&& propertiesNode is not JsonObject)
{
throw new InvalidOperationException(
$"LLM's API for strucutured output requires every object to have predefined properties. Notably, it does not support dictionaries. Path: {obj.GetPath()} Schema: {obj.ToJsonString()}");
}

// Recursively convert any nested schema in "items"
if (obj.TryGetPropertyValue("items", out var itemsNode))
{
ConvertNullableProperties(itemsNode);
}
}

// If the node is an array, traverse each element
if (node is JsonArray arr)
{
foreach (var element in arr)
{
ConvertNullableProperties(element);
}
}
}

public static OpenApiSchema ConvertToSchema<T>(JsonSerializerOptions? jsonOptions = null)
{
if (jsonOptions == null && !JsonSerializer.IsReflectionEnabledByDefault)
{
throw new InvalidOperationException("Please provide a JsonSerializerOptions instance to use in AOT mode.");
}


var newJsonOptions = new JsonSerializerOptions(jsonOptions)
{
NumberHandling = JsonNumberHandling.Strict
};

var typeInfo = newJsonOptions.GetTypeInfo(typeof(T));

return ConvertToCompatibleSchemaSubset(typeInfo.GetJsonSchemaAsNode());
}


public static OpenApiSchema ConvertToSchema(JsonTypeInfo type, string descriptionString)
{
var typeInfo = type;

var dics = JsonSerializer.Deserialize(descriptionString,
OpenApiSchemaJsonContext.Default.IDictionaryStringString);
List<string> required = new List<string>();
var x = ConvertToCompatibleSchemaSubset(typeInfo.GetJsonSchemaAsNode(
var x = typeInfo.GetJsonSchemaAsNode(
exporterOptions: new JsonSchemaExporterOptions()
{
TransformSchemaNode = (a, b) =>
TransformSchemaNode = (context, schema) =>
{
if (a.TypeInfo.Type.IsEnum)
if (context.TypeInfo.Type.IsEnum)
{
b["type"] = "string";
schema["type"] = "string";
}

if (a.PropertyInfo == null)
return b;
var propName = ToCamelCase(a.PropertyInfo.Name);
if (dics.ContainsKey(propName))
{
b["description"] = dics[propName];
}

return b;

ExtractDescription(context, schema, dics);
if (context.PropertyInfo == null)
return schema;

return schema;
},
}));
});

var schema = JsonSerializer.Deserialize(x.ToJsonString(), OpenApiSchemaJsonContext.Default.OpenApiSchema);


foreach (var re in x.Properties)
foreach (var re in schema.Properties)
{
required.Add(re.Key);
}

var mainDescription = x.Description ?? (dics.TryGetValue("mainFunction_Desc", out var desc) ? desc : "");
var mainDescription = schema.Description ?? (dics.TryGetValue("mainFunction_Desc", out var desc) ? desc : "");
return new OpenApiSchema()
{
Description = mainDescription,
Properties = x.Properties,
Properties = schema.Properties,
Required = required,
Type = "object"
};
}

private static void ExtractDescription(JsonSchemaExporterContext context, JsonNode schema, IDictionary<string, string> dics)
{
// 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);

var description = descriptionAttr?.Description;
if (string.IsNullOrEmpty(description))
{
if (context.PropertyInfo is null)
{
var propertyName = ToCamelCase(context.TypeInfo.Type.Name);
dics.TryGetValue(propertyName, out description);
}
}

FixType(schema);

// Apply description attribute to the generated schema.
if (description is not 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.Insert(0, "description", description);
}
}
Comment on lines +55 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Check for possible null references when handling schema objects.

Inside ExtractDescription, if schema is unexpectedly null or not a JsonObject, lines 85–90 do some transformations but assume schema can always be re-cast to JsonObject. Consider explicitly verifying or throwing an exception earlier if schema is null.


private static void FixType(JsonNode schema)
{
// If "type" is an array, remove "null" and collapse if it leaves only one type
var typeValue = schema["type"];
if (typeValue!= null && typeValue is JsonArray array)
{
if (array.Count == 2)
{
var notNullTypes = array.Where(x => x is not null && x.GetValue<string>() != "null").ToList();
if (notNullTypes.Count == 1)
{
schema["type"] = notNullTypes[0]!.GetValue<string>();
schema["nullable"] = true;
}
else
{
throw new InvalidOperationException(
$"LLM's API for strucutured output requires every property to have one defined type, not multiple options. Path: {schema.GetPath()} Schema: {schema.ToJsonString()}");
}
}
else if (array.Count > 2)
{
throw new InvalidOperationException(
$"LLM's API for strucutured output requires every property to have one defined type, not multiple options. Path: {schema.GetPath()} Schema: {schema.ToJsonString()}");
}
}
}
public static string ToCamelCase(string str)
{
if (!string.IsNullOrEmpty(str) && str.Length > 1)
Expand All @@ -161,12 +132,4 @@ public static string ToCamelCase(string str)

return str.ToLowerInvariant();
}

public static string ConvertToSchema(Type type, JsonSerializerOptions? jsonOptions)
{
var node = jsonOptions.GetJsonSchemaAsNode(type);
var x = ConvertToCompatibleSchemaSubset(node);

return JsonSerializer.Serialize(x.Properties, OpenApiSchemaJsonContext.Default.IDictionaryStringOpenApiSchema);
}
}
2 changes: 1 addition & 1 deletion src/tests/AotConsole/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
if (string.IsNullOrWhiteSpace(key))
return;
var prompt = "how does student john doe in senior grade is doing this year, enrollment start 01-01-2024 to 01-01-2025?";
//prompt = "what is written on page 96 in the book 'damdamadum'";
var client = new OpenAIClient(new ApiKeyCredential(key));

Microsoft.Extensions.AI.OpenAIChatClient openAiClient = new OpenAIChatClient(client.GetChatClient("gpt-4o-mini"));
Expand Down
Loading
Loading