Skip to content

Commit 5e9dd9c

Browse files
authored
Fix: Function Tools AOT bugs (#26)
1 parent 8bce2f1 commit 5e9dd9c

File tree

68 files changed

+858
-3038
lines changed

Some content is hidden

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

68 files changed

+858
-3038
lines changed

CSharpToJsonSchema.sln

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpToJsonSchema.AotTests
4444
EndProject
4545
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpToJsonSchema.MeaiTests", "src\tests\CSharpToJsonSchema.MeaiTests\CSharpToJsonSchema.MeaiTests.csproj", "{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}"
4646
EndProject
47+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AotConsole", "src\tests\AotConsole\AotConsole.csproj", "{1942E3E5-F151-4C90-BECE-140AAD8C66DE}"
48+
EndProject
4749
Global
4850
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4951
Debug|Any CPU = Debug|Any CPU
@@ -78,10 +80,6 @@ Global
7880
{6167F915-83EB-42F9-929B-AD4719A55811}.Debug|Any CPU.Build.0 = Debug|Any CPU
7981
{6167F915-83EB-42F9-929B-AD4719A55811}.Release|Any CPU.ActiveCfg = Release|Any CPU
8082
{6167F915-83EB-42F9-929B-AD4719A55811}.Release|Any CPU.Build.0 = Release|Any CPU
81-
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
82-
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Debug|Any CPU.Build.0 = Debug|Any CPU
83-
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Release|Any CPU.ActiveCfg = Release|Any CPU
84-
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Release|Any CPU.Build.0 = Release|Any CPU
8583
EndGlobalSection
8684
GlobalSection(SolutionProperties) = preSolution
8785
HideSolutionNode = FALSE
@@ -95,7 +93,6 @@ Global
9593
{8AFCC30C-C59D-498D-BE68-9A328B3E5599} = {AAA11B78-2764-4520-A97E-46AA7089A588}
9694
{247C813A-9072-4DF3-B403-B35E3688DB4B} = {AAA11B78-2764-4520-A97E-46AA7089A588}
9795
{6167F915-83EB-42F9-929B-AD4719A55811} = {AAA11B78-2764-4520-A97E-46AA7089A588}
98-
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92} = {AAA11B78-2764-4520-A97E-46AA7089A588}
9996
EndGlobalSection
10097
GlobalSection(ExtensibilityGlobals) = postSolution
10198
SolutionGuid = {CED9A020-DBA5-4BE6-8096-75E528648EC1}

src/libs/CSharpToJsonSchema.Generators/Sources.Method.Calls.cs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using CSharpToJsonSchema.Generators.JsonGen.Helpers;
33
using CSharpToJsonSchema.Generators.Models;
44
using H.Generators.Extensions;
5+
using SymbolDisplayFormat = Microsoft.CodeAnalysis.SymbolDisplayFormat;
56

67
namespace CSharpToJsonSchema.Generators;
78

@@ -13,7 +14,7 @@ public static string GenerateFunctionCalls(InterfaceData @interface)
1314
return string.Empty;
1415
var extensionsClassName = @interface.Name;
1516
var res = @$"#nullable enable
16-
#pragma warning disable CS8602
17+
#pragma warning disable CS8602,CS8600
1718
1819
namespace {@interface.Namespace}
1920
{{
@@ -77,14 +78,14 @@ public partial class {extensionsClassName}
7778
#if NET6_0_OR_GREATER
7879
if(global::System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault)
7980
{{
80-
#pragma disable warning IL2026, IL3050
81+
#pragma warning disable IL2026, IL3050
8182
return global::System.Text.Json.JsonSerializer.Deserialize<{method.Name}Args>(json, new global::System.Text.Json.JsonSerializerOptions
8283
{{
8384
PropertyNamingPolicy = global::System.Text.Json.JsonNamingPolicy.CamelCase,
8485
Converters = {{{{ new global::System.Text.Json.Serialization.JsonStringEnumConverter(global::System.Text.Json.JsonNamingPolicy.CamelCase) }}}}
8586
}}) ??
8687
throw new global::System.InvalidOperationException(""Could not deserialize JSON."");
87-
#pragma restore warning IL2026, IL3050
88+
#pragma warning restore IL2026, IL3050
8889
}}
8990
else
9091
{{
@@ -107,19 +108,19 @@ public partial class {extensionsClassName}
107108
private string Call{method.Name}(string json)
108109
{{
109110
var args = As{method.Name}Args(json);
110-
var func = (dynamic) Delegates[""{method.Name}""];
111-
var jsonResult = func.Invoke({string.Join(", ", method.Parameters.Where(s=>s.Type.Name!="CancellationToken").Select(static parameter => $@"args.{parameter.Name.ToPropertyName()}"))});
111+
var func = Delegates[""{method.Name}""];
112+
var jsonResult = ({method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}) func.DynamicInvoke({string.Join(", ", method.Parameters.Where(s=>s.Type.Name!="CancellationToken").Select(static parameter => $@"args.{parameter.Name.ToPropertyName()}"))});
112113
113114
#if NET6_0_OR_GREATER
114115
if(global::System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault)
115116
{{
116-
#pragma disable warning IL2026, IL3050
117+
#pragma warning disable IL2026, IL3050
117118
return global::System.Text.Json.JsonSerializer.Serialize(jsonResult, new global::System.Text.Json.JsonSerializerOptions
118119
{{
119120
PropertyNamingPolicy = global::System.Text.Json.JsonNamingPolicy.CamelCase,
120121
Converters = {{ new global::System.Text.Json.Serialization.JsonStringEnumConverter(global::System.Text.Json.JsonNamingPolicy.CamelCase) }},
121122
}});
122-
#pragma restore warning IL2026, IL3050
123+
#pragma warning restore IL2026, IL3050
123124
}}
124125
else
125126
{{
@@ -137,9 +138,9 @@ public partial class {extensionsClassName}
137138
{@interface.Methods.Where(static x => x is { IsAsync: false, IsVoid: true }).Select(method => $@"
138139
private void Call{method.Name}(string json)
139140
{{
140-
var func = (dynamic) Delegates[""{method.Name}""];
141+
var func = Delegates[""{method.Name}""];
141142
var args = As{method.Name}Args(json);
142-
func.Invoke({string.Join(", ", method.Parameters.Where(s=>s.Type.Name!="CancellationToken").Select(static parameter => $@"args.{parameter.Name.ToPropertyName()}"))});
143+
func.DynamicInvoke({string.Join(", ", method.Parameters.Where(s=>s.Type.Name!="CancellationToken").Select(static parameter => $@"args.{parameter.Name.ToPropertyName()}"))});
143144
}}
144145
").Inject()}
145146
@@ -149,20 +150,20 @@ public partial class {extensionsClassName}
149150
global::System.Threading.CancellationToken cancellationToken = default)
150151
{{
151152
var args = As{method.Name}Args(json);
152-
var func = (dynamic) Delegates[""{method.Name}""];
153-
var jsonResult = await func.Invoke({string.Join(", ", method.Parameters.Where(s=>s.Type.Name!="CancellationToken")
154-
.Select(static parameter => $@"args.{parameter.Name.ToPropertyName()}").Append("cancellationToken"))});
153+
var func = Delegates[""{method.Name}""];
154+
var jsonResult = await (({method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})func.DynamicInvoke({string.Join(", ", method.Parameters.Where(s=>s.Type.Name!="CancellationToken")
155+
.Select(static parameter => $@"args.{parameter.Name.ToPropertyName()}").Append("cancellationToken"))}));
155156
156157
#if NET6_0_OR_GREATER
157158
if(global::System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault)
158159
{{
159-
#pragma disable warning IL2026, IL3050
160+
#pragma warning disable IL2026, IL3050
160161
return global::System.Text.Json.JsonSerializer.Serialize(jsonResult, new global::System.Text.Json.JsonSerializerOptions
161162
{{
162163
PropertyNamingPolicy = global::System.Text.Json.JsonNamingPolicy.CamelCase,
163164
Converters = {{ new global::System.Text.Json.Serialization.JsonStringEnumConverter(global::System.Text.Json.JsonNamingPolicy.CamelCase) }},
164165
}});
165-
#pragma restore warning IL2026, IL3050
166+
#pragma warning restore IL2026, IL3050
166167
}}
167168
else
168169
{{
@@ -186,9 +187,8 @@ public partial class {extensionsClassName}
186187
global::System.Threading.CancellationToken cancellationToken = default)
187188
{{
188189
var args = As{method.Name}Args(json);
189-
//var func = (global::System.Func<{GetInputsTypes(method)}>) Delegates[""{method.Name}""];
190-
var func = (dynamic) Delegates[""{method.Name}""];
191-
await func.Invoke({string.Join(", ", method.Parameters.Where(s=>s.Type.Name!="CancellationToken").Select(static parameter => $@"args.{parameter.Name.ToPropertyName()}"))}, cancellationToken);
190+
var func = Delegates[""{method.Name}""];
191+
await (({method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})func.DynamicInvoke({string.Join(", ", method.Parameters.Where(s=>s.Type.Name!="CancellationToken").Select(static parameter => $@"args.{parameter.Name.ToPropertyName()}"))}, cancellationToken));
192192
193193
return string.Empty;
194194
}}
@@ -210,7 +210,7 @@ public partial class {extensionsClassName}JsonSerializerContext: global::System.
210210
{{
211211
}}
212212
}}
213-
#pragma warning restore CS8602
213+
#pragma warning restore CS8602,CS8600
214214
";
215215
return res;
216216
}

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using CSharpToJsonSchema.Generators.Models;
1+
using CSharpToJsonSchema.Generators.Conversion;
2+
using CSharpToJsonSchema.Generators.Models;
23
using H.Generators.Extensions;
34
using Microsoft.CodeAnalysis;
45

@@ -28,7 +29,7 @@ public static string GenerateFunctionToolClientImplementation(InterfaceData @int
2829
namespace {@interface.Namespace}
2930
{{
3031
public partial class {extensionsClassName}
31-
{{
32+
{{
3233
static {extensionsClassName}()
3334
{{
3435
AddAllTools();
@@ -66,14 +67,16 @@ public void AddTool(global::System.Delegate tool)
6667
AvailableTools.Add(newTool);
6768
if(Delegates.ContainsKey(name))
6869
throw new global::System.Exception({"$\"Function {name} is already registered\""});
69-
Delegates.Add(name, tool);
70+
Delegates.Add(name, tool);
71+
7072
}}
7173
7274
public Tools(global::System.Delegate[] tools)
7375
{{
74-
foreach (var tool in tools)
76+
for(int i = 0; i < tools.Length; i++)
7577
{{
76-
AddTool(tool);
78+
var x = tools[i];
79+
AddTool(x);
7780
}}
7881
}}
7982
@@ -86,7 +89,7 @@ public Tools(global::System.Delegate[] tools)
8689
{{
8790
return (global::System.Collections.Generic.List<global::CSharpToJsonSchema.Tool>) (this.AvailableTools??= new global::System.Collections.Generic.List<global::CSharpToJsonSchema.Tool>());
8891
}}
89-
}}
92+
}}
9093
}}";
9194
}
9295

@@ -97,4 +100,12 @@ private static string GetInputsTypes(MethodData first, bool addReturnType = true
97100
f.Add(first.ReturnType.ToDisplayString());
98101
return string.Join(", ", f);
99102
}
103+
104+
private static string GetDelegateInputsTypes(MethodData first, bool addReturnType = true)
105+
{
106+
var f = first.Parameters.Select(s => $"{s.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {s.Name}"+(s.HasExplicitDefaultValue? $" = " + (s.Type.Name == "CancellationToken"?"default":s.ExplicitDefaultValue?.ToString()) :"")).ToList();
107+
if(addReturnType)
108+
f.Add(first.ReturnType.ToDisplayString());
109+
return string.Join(", ", f);
110+
}
100111
}

src/libs/CSharpToJsonSchema/MeaiFunction.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ private string GetArgsString(IEnumerable<KeyValuePair<string, object?>> argument
9191
}
9292
else if (args.Value is JsonNode node)
9393
{
94-
jsonObject[args.Key] = node;
9594
}
9695
}
9796

src/libs/CSharpToJsonSchema/TypeToSchemaHelpers.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public static OpenApiSchema AsJsonSchema(
5252
JsonSerializerOptions? options = null)
5353
{
5454
type = type ?? throw new ArgumentNullException(nameof(type));
55-
55+
#pragma warning disable IL2026
5656
var node = new JsonSerializerOptions
5757
{
5858
TypeInfoResolver = jsonTypeInfoResolver ?? new DefaultJsonTypeInfoResolver(),
@@ -61,7 +61,7 @@ public static OpenApiSchema AsJsonSchema(
6161
TransformSchemaNode = (context, node) => node,
6262
TreatNullObliviousAsNonNullable = true,
6363
});
64-
64+
#pragma warning restore IL2026
6565
var schema = Create(type, strict);
6666
if (schema.Type == "object")
6767
{
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net9.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<PublishAot>true</PublishAot>
9+
<LangVersion>latest</LangVersion>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="..\..\libs\CSharpToJsonSchema.Generators\CSharpToJsonSchema.Generators.csproj" OutputItemType="analyzer" />
14+
<ProjectReference Include="..\..\libs\CSharpToJsonSchema\CSharpToJsonSchema.csproj" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<PackageReference Include="Microsoft.Extensions.AI" Version="9.3.0-preview.1.25114.11" />
19+
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.3.0-preview.1.25114.11" />
20+
<PackageReference Include="OpenAI" Version="2.2.0-beta.1" />
21+
</ItemGroup>
22+
23+
</Project>

src/tests/AotConsole/Program.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// See https://aka.ms/new-console-template for more information
2+
3+
using System.ClientModel;
4+
using CSharpToJsonSchema.MeaiTests.Services;
5+
using Microsoft.Extensions.AI;
6+
using OpenAI;
7+
8+
var key = Environment.GetEnvironmentVariable("OPEN_AI_APIKEY",EnvironmentVariableTarget.User);
9+
if (string.IsNullOrWhiteSpace(key))
10+
return;
11+
var prompt = "how does student john doe in senior grade is doing this year, enrollment start 01-01-2024 to 01-01-2025?";
12+
13+
var client = new OpenAIClient(new ApiKeyCredential(key));
14+
15+
Microsoft.Extensions.AI.OpenAIChatClient openAiClient = new OpenAIChatClient(client.GetChatClient("gpt-4o-mini"));
16+
17+
var chatClient = new FunctionInvokingChatClient(openAiClient);
18+
var chatOptions = new ChatOptions();
19+
20+
var service = new StudentRecordService();
21+
22+
var tools = new Tools([service.GetStudentRecordAsync]);
23+
chatOptions.Tools = tools.AsMeaiTools();
24+
var message = new ChatMessage(ChatRole.User, prompt);
25+
var response = await chatClient.GetResponseAsync(message,options:chatOptions).ConfigureAwait(false);
26+
27+
Console.WriteLine(response.Choices.LastOrDefault().Text);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using DescriptionAttribute = System.ComponentModel.DescriptionAttribute;
2+
3+
4+
namespace CSharpToJsonSchema.MeaiTests.Services;
5+
6+
public class GetAuthorBook
7+
{
8+
public string Title { get; set; } = string.Empty;
9+
public string Description { get; set; } = string.Empty;
10+
}
11+
12+
[GenerateJsonSchema(MeaiFunctionTool = true)]
13+
public interface IBookStoreService
14+
{
15+
[Description("Get books written by some author")]
16+
public Task<List<GetAuthorBook>> GetAuthorBooksAsync([Description("Author name")] string authorName, CancellationToken cancellationToken = default);
17+
18+
[Description("Get book page content")]
19+
public Task<string> GetBookPageContentAsync([Description("Book Name")] string bookName, [Description("Book Page Number")] int bookPageNumber, CancellationToken cancellationToken = default);
20+
21+
}
22+
public class BookStoreService : IBookStoreService
23+
{
24+
public Task<List<GetAuthorBook>> GetAuthorBooksAsync(string authorName, CancellationToken cancellationToken = default)
25+
{
26+
return Task.FromResult(new List<GetAuthorBook>([
27+
new GetAuthorBook
28+
{ Title = "Five point someone", Description = "This book is about 3 college friends" },
29+
new GetAuthorBook
30+
{ Title = "Two States", Description = "This book is about intercast marriage in India" }
31+
]));
32+
}
33+
34+
public Task<string> GetBookPageContentAsync(string bookName, int bookPageNumber, CancellationToken cancellationToken = default)
35+
{
36+
return Task.FromResult("this is a cool weather out there, and I am stuck at home.");
37+
}
38+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
namespace CSharpToJsonSchema.MeaiTests.Services;
2+
using DescriptionAttribute = System.ComponentModel.DescriptionAttribute;
3+
4+
public class StudentRecordService
5+
{
6+
[System.ComponentModel.Description("Get student record for the year")]
7+
[FunctionTool(MeaiFunctionTool = true)]
8+
9+
public async Task<StudentRecord> GetStudentRecordAsync(QueryStudentRecordRequest query, CancellationToken cancellationToken = default)
10+
{
11+
return new StudentRecord
12+
{
13+
StudentId = "12345",
14+
FullName = query.FullName,
15+
Level = StudentRecord.GradeLevel.Senior,
16+
EnrolledCourses = new List<string> { "Math 101", "Physics 202", "History 303" },
17+
Grades = new Dictionary<string, double>
18+
{
19+
{ "Math 101", 3.5 },
20+
{ "Physics 202", 3.8 },
21+
{ "History 303", 3.9 }
22+
},
23+
EnrollmentDate = new DateTime(2020, 9, 1),
24+
IsActive = true
25+
};
26+
}
27+
}
28+
29+
public class StudentRecord
30+
{
31+
public enum GradeLevel
32+
{
33+
Freshman,
34+
Sophomore,
35+
Junior,
36+
Senior,
37+
Graduate
38+
}
39+
40+
public string StudentId { get; set; } = string.Empty;
41+
public string FullName { get; set; } = string.Empty;
42+
public GradeLevel Level { get; set; } = GradeLevel.Freshman;
43+
public List<string> EnrolledCourses { get; set; } = new List<string>();
44+
public Dictionary<string, double> Grades { get; set; } = new Dictionary<string, double>();
45+
public DateTime EnrollmentDate { get; set; } = DateTime.Now;
46+
public bool IsActive { get; set; } = true;
47+
48+
public double CalculateGPA()
49+
{
50+
if (Grades.Count == 0) return 0.0;
51+
return Grades.Values.Average();
52+
}
53+
}
54+
55+
[Description("Request class containing filters for querying student records.")]
56+
public class QueryStudentRecordRequest
57+
{
58+
[Description("The student's full name.")]
59+
public string FullName { get; set; } = string.Empty;
60+
61+
[Description("Grade filters for querying specific grades, e.g., Freshman or Senior.")]
62+
public List<StudentRecord.GradeLevel> GradeFilters { get; set; } = new();
63+
64+
[Description("The start date for the enrollment date range. ISO 8601 standard date")]
65+
public DateTime EnrollmentStartDate { get; set; }
66+
67+
[Description("The end date for the enrollment date range. ISO 8601 standard date")]
68+
public DateTime EnrollmentEndDate { get; set; }
69+
70+
[Description("The flag indicating whether to include only active students.")]
71+
public bool? IsActive { get; set; } = true;
72+
}

0 commit comments

Comments
 (0)