Skip to content

Commit 6b36c90

Browse files
authored
fix: MeaiFunction ignoring primitive data types arguments (#28)
1 parent e538485 commit 6b36c90

File tree

50 files changed

+127
-3381
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+127
-3381
lines changed

CSharpToJsonSchema.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ Global
9797
{8AFCC30C-C59D-498D-BE68-9A328B3E5599} = {AAA11B78-2764-4520-A97E-46AA7089A588}
9898
{247C813A-9072-4DF3-B403-B35E3688DB4B} = {AAA11B78-2764-4520-A97E-46AA7089A588}
9999
{6167F915-83EB-42F9-929B-AD4719A55811} = {AAA11B78-2764-4520-A97E-46AA7089A588}
100+
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92} = {AAA11B78-2764-4520-A97E-46AA7089A588}
100101
EndGlobalSection
101102
GlobalSection(ExtensibilityGlobals) = postSolution
102103
SolutionGuid = {CED9A020-DBA5-4BE6-8096-75E528648EC1}

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

Lines changed: 0 additions & 70 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);
@@ -140,72 +136,6 @@ public static InterfaceData PrepareMethodData(
140136

141137
return string.Join(".", commonParts);
142138
}
143-
144-
145-
// private static Dictionary<string, bool> GetIsRequired(IParameterSymbol[] parameters, Dictionary<string, bool>? dics = null)
146-
// {
147-
// dics ??= new Dictionary<string, bool>();
148-
//
149-
// foreach (var parameter in parameters)
150-
// {
151-
// if (dics.TryAdd(parameter.Name, IsRequired(parameter)))
152-
// {
153-
// if (parameter is IParameterSymbol namedTypeSymbol)
154-
// {
155-
// GetIsRequired(namedTypeSymbol.Type.GetMembers().OfType<IPropertySymbol>().ToArray(),dics)
156-
// }
157-
// }
158-
// }
159-
//
160-
// return dics;
161-
// }
162-
//
163-
// private static bool IsRequired(ISymbol parameter)
164-
// {
165-
// return false;
166-
// //parameter.GetAttributes().OfType<global::System.ComponentModel.D.RequiredAttribute>()
167-
// }
168-
169-
private static List<KeyValuePair<string, string>> GetParameterDescriptions(IParameterSymbol parameters,
170-
Dictionary<string, string>? dics = null)
171-
{
172-
dics ??= new Dictionary<string, string>();
173-
174-
175-
if (dics.TryAdd(parameters.Name.ToCamelCase(), GetDescription(parameters)))
176-
{
177-
if (parameters is IParameterSymbol namedTypeSymbol)
178-
{
179-
GetParameterDescriptions(namedTypeSymbol.Type.GetMembers().OfType<IPropertySymbol>().ToArray(), dics);
180-
}
181-
}
182-
183-
return dics.Select(x => new KeyValuePair<string, string>(x.Key, x.Value)).ToList();
184-
}
185-
186-
private static Dictionary<string, string> GetParameterDescriptions(IPropertySymbol[] parameters,
187-
Dictionary<string, string>? dics = null)
188-
{
189-
dics ??= new Dictionary<string, string>();
190-
191-
foreach (var parameter in parameters)
192-
{
193-
var description = GetDescription(parameter);
194-
if (string.IsNullOrWhiteSpace(description)) continue;
195-
196-
if (dics.TryAdd(parameter.Name, description))
197-
{
198-
if (parameter is IPropertySymbol namedTypeSymbol)
199-
{
200-
GetParameterDescriptions(namedTypeSymbol.Type.GetMembers().OfType<IPropertySymbol>().ToArray(),
201-
dics);
202-
}
203-
}
204-
}
205-
206-
return dics;
207-
}
208-
209139
private static OpenApiSchema ToParameterData(ITypeSymbol typeSymbol, string? name = null,
210140
string? description = null, bool isRequired = true)
211141
{

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+
$"LLM'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+
$"LLM'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"));

0 commit comments

Comments
 (0)