Skip to content

Commit 8c68191

Browse files
author
Gunpal Jain
committed
fix: MeaiFunction ignoring simple values types arguments
refactor: Simplified JsonSchema generation codes
1 parent 5de972c commit 8c68191

File tree

7 files changed

+125
-136
lines changed

7 files changed

+125
-136
lines changed

src/libs/CSharpToJsonSchema.Generators/Conversion/ToModels.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ public static InterfaceData PrepareData(
3737
IsVoid: x.ReturnsVoid || x.ReturnType.MetadataName == "Task",
3838
IsStrict: isStrict,
3939
Parameters: parameters.Select(static y => y).ToArray(),
40-
Descriptions: parameters.Select(static l => GetParameterDescriptions(l)).SelectMany(s => s)
41-
.ToDictionary(s => s.Key, s => s.Value),
4240
ReturnType: x.ReturnType
4341
);
4442
})
@@ -91,8 +89,6 @@ public static InterfaceData PrepareMethodData(
9189
IsVoid: x.ReturnsVoid || x.ReturnType.MetadataName == "Task",
9290
IsStrict: isStrict,
9391
Parameters: parameters.Select(static y => y).ToArray(),
94-
Descriptions: parameters.Select(static l => GetParameterDescriptions(l)).SelectMany(s => s)
95-
.ToDictionary(s => s.Key, s => s.Value),
9692
ReturnType:x.ReturnType
9793
);
9894
methodList.Add(methodData);

src/libs/CSharpToJsonSchema.Generators/Models/MethodData.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ public readonly record struct MethodData(
99
bool IsVoid,
1010
bool IsStrict,
1111
IParameterSymbol[] Parameters,
12-
Dictionary<string, string> Descriptions,
1312
ITypeSymbol ReturnType);
1413

1514
public readonly record struct PropertyMetadata(string Name, string Description, bool IsRequired);

src/libs/CSharpToJsonSchema.Generators/Sources.Tools.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ private static string GetDictionaryString(MethodData data)
9797
{
9898
StringBuilder sb = new StringBuilder();
9999

100-
var methodDescriptions = data.Descriptions;
100+
var methodDescriptions = new Dictionary<string, string>();
101101
methodDescriptions.Add("MainFunction_Desc", data.Description);
102102
sb.Append("{");
103103
var lst = methodDescriptions.Select(s => $"\"{s.Key.ToCamelCase()}\":\"{s.Value}\"");

src/libs/CSharpToJsonSchema/MeaiFunction.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ private string GetArgsString(IEnumerable<KeyValuePair<string, object?>> argument
8888
jsonObject[args.Key] = JsonArray.Create(element);
8989
else if (element.ValueKind == JsonValueKind.Object)
9090
jsonObject[args.Key] = JsonObject.Create(element);
91+
else
92+
jsonObject[args.Key] = JsonValue.Create(element);
9193
}
9294
else if (args.Value is JsonNode node)
9395
{
Lines changed: 91 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.Text.Json.Nodes;
1+
using System.ComponentModel;
2+
using System.Reflection;
3+
using System.Text.Json.Nodes;
24
using System.Text.Json.Schema;
35
using System.Text.Json.Serialization;
46
using System.Text.Json.Serialization.Metadata;
@@ -7,151 +9,120 @@ namespace CSharpToJsonSchema;
79

810
public static class SchemaBuilder
911
{
10-
/// <summary>
11-
/// Converts a JSON document that contains valid json schema <see href="https://json-schema.org/specification"/> as e.g.
12-
/// generated by <code>Microsoft.Extensions.AI.AIJsonUtilities.CreateJsonSchema</code> or <code>JsonSchema.Net</code>'s
13-
/// <see cref="JsonSchemaBuilder"/> to a subset that is compatible with LLM's APIs.
14-
/// </summary>
15-
/// <param name="constructedSchema">Generated, valid json schema.</param>
16-
/// <returns>Subset of the given json schema in a LLM-compatible format.</returns>
17-
public static OpenApiSchema ConvertToCompatibleSchemaSubset(JsonNode node)
18-
{
19-
ConvertNullableProperties(node);
20-
var x1 = node;
21-
var x2 = x1.ToJsonString();
22-
var schema = JsonSerializer.Deserialize(x2, OpenApiSchemaJsonContext.Default.OpenApiSchema);
23-
return schema;
24-
}
25-
26-
private static void ConvertNullableProperties(JsonNode? node)
27-
{
28-
// If the node is an object, look for a "type" property or nested definitions
29-
if (node is JsonObject obj)
30-
{
31-
// If "type" is an array, remove "null" and collapse if it leaves only one type
32-
if (obj.TryGetPropertyValue("type", out var typeValue) && typeValue is JsonArray array)
33-
{
34-
if (array.Count == 2)
35-
{
36-
var notNullTypes = array.Where(x => x is not null && x.GetValue<string>() != "null").ToList();
37-
if (notNullTypes.Count == 1)
38-
{
39-
obj["type"] = notNullTypes[0]!.GetValue<string>();
40-
obj["nullable"] = true;
41-
}
42-
else
43-
{
44-
throw new InvalidOperationException(
45-
$"LLM's API for strucutured output requires every property to have one defined type, not multiple options. Path: {obj.GetPath()} Schema: {obj.ToJsonString()}");
46-
}
47-
}
48-
else if (array.Count > 2)
49-
{
50-
throw new InvalidOperationException(
51-
$"LLM's API for strucutured output requires every property to have one defined type, not multiple options. Path: {obj.GetPath()} Schema: {obj.ToJsonString()}");
52-
}
53-
}
54-
55-
// Recursively convert any nested schema in "properties"
56-
if (obj.TryGetPropertyValue("properties", out var propertiesNode) &&
57-
propertiesNode is JsonObject propertiesObj)
58-
{
59-
foreach (var property in propertiesObj)
60-
{
61-
ConvertNullableProperties(property.Value);
62-
}
63-
}
64-
65-
if (obj.TryGetPropertyValue("type", out var newTypeValue)
66-
&& newTypeValue is JsonNode
67-
&& newTypeValue.GetValueKind() == JsonValueKind.String
68-
&& "object".Equals(newTypeValue.GetValue<string>(), StringComparison.OrdinalIgnoreCase)
69-
&& propertiesNode is not JsonObject)
70-
{
71-
throw new InvalidOperationException(
72-
$"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()}");
73-
}
74-
75-
// Recursively convert any nested schema in "items"
76-
if (obj.TryGetPropertyValue("items", out var itemsNode))
77-
{
78-
ConvertNullableProperties(itemsNode);
79-
}
80-
}
81-
82-
// If the node is an array, traverse each element
83-
if (node is JsonArray arr)
84-
{
85-
foreach (var element in arr)
86-
{
87-
ConvertNullableProperties(element);
88-
}
89-
}
90-
}
91-
92-
public static OpenApiSchema ConvertToSchema<T>(JsonSerializerOptions? jsonOptions = null)
93-
{
94-
if (jsonOptions == null && !JsonSerializer.IsReflectionEnabledByDefault)
95-
{
96-
throw new InvalidOperationException("Please provide a JsonSerializerOptions instance to use in AOT mode.");
97-
}
98-
99-
100-
var newJsonOptions = new JsonSerializerOptions(jsonOptions)
101-
{
102-
NumberHandling = JsonNumberHandling.Strict
103-
};
104-
105-
var typeInfo = newJsonOptions.GetTypeInfo(typeof(T));
106-
107-
return ConvertToCompatibleSchemaSubset(typeInfo.GetJsonSchemaAsNode());
108-
}
109-
12+
11013
public static OpenApiSchema ConvertToSchema(JsonTypeInfo type, string descriptionString)
11114
{
11215
var typeInfo = type;
11316

11417
var dics = JsonSerializer.Deserialize(descriptionString,
11518
OpenApiSchemaJsonContext.Default.IDictionaryStringString);
11619
List<string> required = new List<string>();
117-
var x = ConvertToCompatibleSchemaSubset(typeInfo.GetJsonSchemaAsNode(
20+
var x = typeInfo.GetJsonSchemaAsNode(
11821
exporterOptions: new JsonSchemaExporterOptions()
11922
{
120-
TransformSchemaNode = (a, b) =>
23+
TransformSchemaNode = (context, schema) =>
12124
{
122-
if (a.TypeInfo.Type.IsEnum)
25+
if (context.TypeInfo.Type.IsEnum)
12326
{
124-
b["type"] = "string";
27+
schema["type"] = "string";
12528
}
126-
127-
if (a.PropertyInfo == null)
128-
return b;
129-
var propName = ToCamelCase(a.PropertyInfo.Name);
130-
if (dics.ContainsKey(propName))
131-
{
132-
b["description"] = dics[propName];
133-
}
134-
135-
return b;
29+
30+
ExtractDescription(context, schema, dics);
31+
if (context.PropertyInfo == null)
32+
return schema;
33+
34+
return schema;
13635
},
137-
}));
36+
});
37+
38+
var schema = JsonSerializer.Deserialize(x.ToJsonString(), OpenApiSchemaJsonContext.Default.OpenApiSchema);
13839

139-
140-
foreach (var re in x.Properties)
40+
foreach (var re in schema.Properties)
14141
{
14242
required.Add(re.Key);
14343
}
14444

145-
var mainDescription = x.Description ?? (dics.TryGetValue("mainFunction_Desc", out var desc) ? desc : "");
45+
var mainDescription = schema.Description ?? (dics.TryGetValue("mainFunction_Desc", out var desc) ? desc : "");
14646
return new OpenApiSchema()
14747
{
14848
Description = mainDescription,
149-
Properties = x.Properties,
49+
Properties = schema.Properties,
15050
Required = required,
15151
Type = "object"
15252
};
15353
}
54+
55+
private static void ExtractDescription(JsonSchemaExporterContext context, JsonNode schema, IDictionary<string, string> dics)
56+
{
57+
// Determine if a type or property and extract the relevant attribute provider.
58+
ICustomAttributeProvider? attributeProvider = context.PropertyInfo is not null
59+
? context.PropertyInfo.AttributeProvider
60+
: context.TypeInfo.Type;
61+
62+
// Look up any description attributes.
63+
DescriptionAttribute? descriptionAttr = attributeProvider?
64+
.GetCustomAttributes(inherit: true)
65+
.Select(attr => attr as DescriptionAttribute)
66+
.FirstOrDefault(attr => attr is not null);
67+
68+
var description = descriptionAttr?.Description;
69+
if (string.IsNullOrEmpty(description))
70+
{
71+
if (context.PropertyInfo is null)
72+
{
73+
var propertyName = ToCamelCase(context.TypeInfo.Type.Name);
74+
dics.TryGetValue(propertyName, out description);
75+
}
76+
}
15477

78+
FixType(schema);
79+
80+
// Apply description attribute to the generated schema.
81+
if (description is not null)
82+
{
83+
if (schema is not JsonObject jObj)
84+
{
85+
// Handle the case where the schema is a Boolean.
86+
JsonValueKind valueKind = schema.GetValueKind();
87+
88+
schema = jObj = new JsonObject();
89+
if (valueKind is JsonValueKind.False)
90+
{
91+
jObj.Add("not", true);
92+
}
93+
}
94+
95+
jObj.Insert(0, "description", description);
96+
}
97+
}
98+
99+
private static void FixType(JsonNode schema)
100+
{
101+
// If "type" is an array, remove "null" and collapse if it leaves only one type
102+
var typeValue = schema["type"];
103+
if (typeValue!= null && typeValue is JsonArray array)
104+
{
105+
if (array.Count == 2)
106+
{
107+
var notNullTypes = array.Where(x => x is not null && x.GetValue<string>() != "null").ToList();
108+
if (notNullTypes.Count == 1)
109+
{
110+
schema["type"] = notNullTypes[0]!.GetValue<string>();
111+
schema["nullable"] = true;
112+
}
113+
else
114+
{
115+
throw new InvalidOperationException(
116+
$"Google's API for strucutured output requires every property to have one defined type, not multiple options. Path: {schema.GetPath()} Schema: {schema.ToJsonString()}");
117+
}
118+
}
119+
else if (array.Count > 2)
120+
{
121+
throw new InvalidOperationException(
122+
$"Google's API for strucutured output requires every property to have one defined type, not multiple options. Path: {schema.GetPath()} Schema: {schema.ToJsonString()}");
123+
}
124+
}
125+
}
155126
public static string ToCamelCase(string str)
156127
{
157128
if (!string.IsNullOrEmpty(str) && str.Length > 1)
@@ -161,12 +132,4 @@ public static string ToCamelCase(string str)
161132

162133
return str.ToLowerInvariant();
163134
}
164-
165-
public static string ConvertToSchema(Type type, JsonSerializerOptions? jsonOptions)
166-
{
167-
var node = jsonOptions.GetJsonSchemaAsNode(type);
168-
var x = ConvertToCompatibleSchemaSubset(node);
169-
170-
return JsonSerializer.Serialize(x.Properties, OpenApiSchemaJsonContext.Default.IDictionaryStringOpenApiSchema);
171-
}
172135
}

src/tests/AotConsole/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
if (string.IsNullOrWhiteSpace(key))
1010
return;
1111
var prompt = "how does student john doe in senior grade is doing this year, enrollment start 01-01-2024 to 01-01-2025?";
12-
12+
//prompt = "what is written on page 96 in the book 'damdamadum'";
1313
var client = new OpenAIClient(new ApiKeyCredential(key));
1414

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

src/tests/CSharpToJsonSchema.AotTests/JsonSerializationTests.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,35 @@ public void ShouldCreateToolWithComplexStudentClass()
127127
{
128128
var service = new StudenRecordService();
129129
var tools = service.AsTools();
130+
131+
Tool tool = tools[0];
132+
133+
134+
var openApiSchema = (OpenApiSchema)tool.Parameters;
135+
tool.Description.Should().Be("Get student record for the year");
136+
tool.Name.Should().Be("GetStudentRecordAsync");
137+
openApiSchema.Properties["query"].Should().NotBeNull();
138+
139+
openApiSchema.Properties["query"].Properties["fullName"].Type.Should().Be("string");
140+
openApiSchema.Properties["query"].Properties["fullName"].Description.Should().Be("The student's full name.");
141+
142+
openApiSchema.Properties["query"].Properties["gradeFilters"].Type.Should().Be("array");
143+
openApiSchema.Properties["query"].Properties["gradeFilters"].Items.Type.Should().Be("string");
144+
openApiSchema.Properties["query"].Properties["gradeFilters"].Description.Should()
145+
.Be("Grade filters for querying specific grades, e.g., Freshman or Senior.");
146+
147+
openApiSchema.Properties["query"].Properties["enrollmentStartDate"].Type.Should().Be("string");
148+
openApiSchema.Properties["query"].Properties["enrollmentStartDate"].Format.Should().Be("date-time");
149+
openApiSchema.Properties["query"].Properties["enrollmentStartDate"].Description.Should()
150+
.Be("The start date for the enrollment date range. ISO 8601 standard date");
151+
152+
openApiSchema.Properties["query"].Properties["enrollmentEndDate"].Type.Should().Be("string");
153+
openApiSchema.Properties["query"].Properties["enrollmentEndDate"].Format.Should().Be("date-time");
154+
openApiSchema.Properties["query"].Properties["enrollmentEndDate"].Description.Should()
155+
.Be("The end date for the enrollment date range. ISO 8601 standard date");
156+
157+
openApiSchema.Properties["query"].Properties["isActive"].Type.Should().Be("boolean");
158+
openApiSchema.Properties["query"].Properties["isActive"].Description.Should()
159+
.Be("The flag indicating whether to include only active students.");
130160
}
131-
132161
}

0 commit comments

Comments
 (0)