diff --git a/CSharpToJsonSchema.sln b/CSharpToJsonSchema.sln index c3e83b9..f7b8bc8 100755 --- a/CSharpToJsonSchema.sln +++ b/CSharpToJsonSchema.sln @@ -42,6 +42,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpToJsonSchema.Integrat EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpToJsonSchema.AotTests", "src\tests\CSharpToJsonSchema.AotTests\CSharpToJsonSchema.AotTests.csproj", "{6167F915-83EB-42F9-929B-AD4719A55811}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpToJsonSchema.MeaiTests", "src\tests\CSharpToJsonSchema.MeaiTests\CSharpToJsonSchema.MeaiTests.csproj", "{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -76,6 +78,10 @@ Global {6167F915-83EB-42F9-929B-AD4719A55811}.Debug|Any CPU.Build.0 = Debug|Any CPU {6167F915-83EB-42F9-929B-AD4719A55811}.Release|Any CPU.ActiveCfg = Release|Any CPU {6167F915-83EB-42F9-929B-AD4719A55811}.Release|Any CPU.Build.0 = Release|Any CPU + {DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -89,6 +95,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} diff --git a/src/libs/CSharpToJsonSchema.Generators/Conversion/ToModels.cs b/src/libs/CSharpToJsonSchema.Generators/Conversion/ToModels.cs index f310202..33f7271 100644 --- a/src/libs/CSharpToJsonSchema.Generators/Conversion/ToModels.cs +++ b/src/libs/CSharpToJsonSchema.Generators/Conversion/ToModels.cs @@ -18,6 +18,8 @@ public static InterfaceData PrepareData( strict; var generateGoogleFunctionTool = attributeData.NamedArguments.FirstOrDefault(x => x.Key == "GoogleFunctionTool").Value.Value is bool googleFunctionTool && googleFunctionTool; + var meaiFunctionTool= attributeData.NamedArguments.FirstOrDefault(x => x.Key == "MeaiFunctionTool").Value.Value is bool meaift && + meaift; var methods = interfaceSymbol .GetMembers() .OfType() @@ -46,6 +48,7 @@ public static InterfaceData PrepareData( Namespace: interfaceSymbol.ContainingNamespace.ToDisplayString(), Name: interfaceSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), GoogleFunctionTool:generateGoogleFunctionTool, + MeaiFunctionTool:meaiFunctionTool, Methods: methods); } @@ -60,6 +63,7 @@ public static InterfaceData PrepareMethodData( List methodList = new(); List namespaces = new(); bool generateGoogleFunctionTools = false; + bool meaiFunctionTools = false; foreach (var l in list) { var (interfaceSymbol, attributeData) = l; @@ -70,6 +74,11 @@ public static InterfaceData PrepareMethodData( if(ggft) generateGoogleFunctionTools = true; + var meai = attributeData.NamedArguments.FirstOrDefault(x => x.Key == "MeaiFunctionTool").Value.Value is bool meaift && + meaift; + if(meai) + meaiFunctionTools = true; + var x = interfaceSymbol; var parameters = x.Parameters //.Where(static x => x.Type.MetadataName != "CancellationToken") @@ -94,6 +103,7 @@ public static InterfaceData PrepareMethodData( Namespace: GetCommonRootNamespace(namespaces)??namespaceName, Name: className, GoogleFunctionTool: generateGoogleFunctionTools, + MeaiFunctionTool:meaiFunctionTools, Methods: methodList.ToArray()); } public static string? GetCommonRootNamespace(IEnumerable namespaces) diff --git a/src/libs/CSharpToJsonSchema.Generators/JsonSchemaGenerator.cs b/src/libs/CSharpToJsonSchema.Generators/JsonSchemaGenerator.cs index 2a96b6d..d0594ce 100755 --- a/src/libs/CSharpToJsonSchema.Generators/JsonSchemaGenerator.cs +++ b/src/libs/CSharpToJsonSchema.Generators/JsonSchemaGenerator.cs @@ -50,6 +50,9 @@ private void ProcessMethods(IncrementalGeneratorInitializationContext context) attributes .SelectAndReportExceptions(AsGoogleFunctionToolsForMethods, context, Id) .AddSource(context); + attributes + .SelectAndReportExceptions(AsMeaiFunctionToolsForMethods, context, Id) + .AddSource(context); var generator = new JsonSourceGenerator(); generator.InitializeForFunctionTools(context); @@ -73,6 +76,9 @@ private void ProcessInterfaces(IncrementalGeneratorInitializationContext context attributes .SelectAndReportExceptions(AsGoogleFunctionToolsForInterface, context, Id) .AddSource(context); + attributes + .SelectAndReportExceptions(AsMeaiFunctionToolsForInterface, context, Id) + .AddSource(context); var generator = new JsonSourceGenerator(); generator.Initialize2(context); @@ -119,7 +125,7 @@ private static FileWithName AsFunctionCalls(InterfaceData @interface) Name: $"{@interface.Name}.FunctionCalls.generated.cs", Text: Sources.GenerateFunctionCalls(@interface)); } - + private static FileWithName AsGoogleFunctionToolsForMethods(InterfaceData @interface) { return new FileWithName( @@ -127,6 +133,20 @@ private static FileWithName AsGoogleFunctionToolsForMethods(InterfaceData @inter Text: Sources.GenerateGoogleFunctionToolForMethods(@interface)); } + private static FileWithName AsMeaiFunctionToolsForMethods(InterfaceData @interface) + { + return new FileWithName( + Name: $"{@interface.Name}.MeaiTools.generated.cs", + Text: Sources.GenerateMeaiFunctionToolForMethods(@interface)); + } + + private static FileWithName AsMeaiFunctionToolsForInterface(InterfaceData @interface) + { + return new FileWithName( + Name: $"{@interface.Name}.MeaiToolsExtensions.generated.cs", + Text: Sources.GenerateMeaiFunctionToolForInterface(@interface)); + } + private static FileWithName AsGoogleFunctionToolsForInterface(InterfaceData @interface) { return new FileWithName( diff --git a/src/libs/CSharpToJsonSchema.Generators/Models/InterfaceData.cs b/src/libs/CSharpToJsonSchema.Generators/Models/InterfaceData.cs index 2a773a8..01be3b2 100644 --- a/src/libs/CSharpToJsonSchema.Generators/Models/InterfaceData.cs +++ b/src/libs/CSharpToJsonSchema.Generators/Models/InterfaceData.cs @@ -4,4 +4,5 @@ public readonly record struct InterfaceData( string Namespace, string Name, bool GoogleFunctionTool, + bool MeaiFunctionTool, IReadOnlyCollection Methods); \ No newline at end of file diff --git a/src/libs/CSharpToJsonSchema.Generators/Sources.Method.Calls.cs b/src/libs/CSharpToJsonSchema.Generators/Sources.Method.Calls.cs index ddca22c..93715ff 100644 --- a/src/libs/CSharpToJsonSchema.Generators/Sources.Method.Calls.cs +++ b/src/libs/CSharpToJsonSchema.Generators/Sources.Method.Calls.cs @@ -17,6 +17,14 @@ public static string GenerateFunctionCalls(InterfaceData @interface) namespace {@interface.Namespace} {{ + + + + + + + + {@interface.Methods.Select(static method => $@" public class {method.Name}Args {{ diff --git a/src/libs/CSharpToJsonSchema.Generators/Sources.Method.GoogleFunctionTools.cs b/src/libs/CSharpToJsonSchema.Generators/Sources.Method.GoogleFunctionTools.cs index 00cbed9..92ff263 100644 --- a/src/libs/CSharpToJsonSchema.Generators/Sources.Method.GoogleFunctionTools.cs +++ b/src/libs/CSharpToJsonSchema.Generators/Sources.Method.GoogleFunctionTools.cs @@ -18,12 +18,12 @@ namespace {@interface.Namespace} {{ public partial class {extensionsClassName} {{ - public static implicit operator global::GenerativeAI.Tools.GenericFunctionTool ({@interface.Namespace}.{extensionsClassName} tools) + public static implicit operator global::GenerativeAI.Core.GoogleFunctionTool ({@interface.Namespace}.{extensionsClassName} tools) {{ return tools.AsGoogleFunctionTool(); }} - public global::GenerativeAI.Tools.GenericFunctionTool AsGoogleFunctionTool() + public global::GenerativeAI.Core.GoogleFunctionTool AsGoogleFunctionTool() {{ return new global::GenerativeAI.Tools.GenericFunctionTool(this.AsTools(), this.AsCalls()); }} @@ -42,9 +42,9 @@ public static string GenerateGoogleFunctionToolForInterface(InterfaceData @inter namespace {@interface.Namespace} {{ - public partial class {extensionsClassName} + public static partial class {extensionsClassName} {{ - public global::GenerativeAI.Core.IFunctionTool AsGoogleFunctionTool(this {@interface.Name} service) + public static global::GenerativeAI.Core.IFunctionTool AsGoogleFunctionTool(this {@interface.Name} service) {{ return new global::GenerativeAI.Tools.GenericFunctionTool(service.AsTools(), service.AsCalls()); }} diff --git a/src/libs/CSharpToJsonSchema.Generators/Sources.Method.MeaiTools.cs b/src/libs/CSharpToJsonSchema.Generators/Sources.Method.MeaiTools.cs new file mode 100644 index 0000000..e785c8f --- /dev/null +++ b/src/libs/CSharpToJsonSchema.Generators/Sources.Method.MeaiTools.cs @@ -0,0 +1,69 @@ +using CSharpToJsonSchema.Generators.Models; + +namespace CSharpToJsonSchema.Generators; + +internal static partial class Sources +{ + public static string GenerateMeaiFunctionToolForMethods(InterfaceData @interface) + { + if(@interface.Methods.Count == 0 || !@interface.MeaiFunctionTool) + return string.Empty; + var extensionsClassName = @interface.Name; + + return @$" +#nullable enable + +namespace {@interface.Namespace} +{{ + public partial class {extensionsClassName} + {{ + public static implicit operator global::System.Collections.Generic.List? ({@interface.Namespace}.{extensionsClassName} tools) + {{ + return tools.AsMeaiTools(); + }} + + public global::System.Collections.Generic.List AsMeaiTools() + {{ + var lst = new global::System.Collections.Generic.List(); + var tools = this.AsTools(); + var calls = this.AsCalls(); + foreach (var tool in tools) + {{ + var call = calls[tool.Name]; + lst.Add(new global::CSharpToJsonSchema.MeaiFunction(tool, call)); + }} + return lst; + }} + }} +}}"; + } + + public static string GenerateMeaiFunctionToolForInterface(InterfaceData @interface) + { + if(!@interface.MeaiFunctionTool) + return string.Empty; + var extensionsClassName = @interface.Name.Substring(startIndex: 1) + "Extensions"; + + return @$" +#nullable enable + +namespace {@interface.Namespace} +{{ + public partial class {extensionsClassName} + {{ + public static global::System.Collections.Generic.List AsMeaiTools(this {@interface.Name} service) + {{ + var lst = new global::System.Collections.Generic.List(); + var tools = service.AsTools(); + var calls = service.AsCalls(); + foreach (var tool in tools) + {{ + var call = calls[tool.Name]; + lst.Add(new global::CSharpToJsonSchema.MeaiFunction(tool, call)); + }} + return lst; + }} + }} +}}"; + } +} \ No newline at end of file diff --git a/src/libs/CSharpToJsonSchema/CSharpToJsonSchema.csproj b/src/libs/CSharpToJsonSchema/CSharpToJsonSchema.csproj index 36a9b03..20766c1 100644 --- a/src/libs/CSharpToJsonSchema/CSharpToJsonSchema.csproj +++ b/src/libs/CSharpToJsonSchema/CSharpToJsonSchema.csproj @@ -17,6 +17,7 @@ + diff --git a/src/libs/CSharpToJsonSchema/FunctionToolAttribute.cs b/src/libs/CSharpToJsonSchema/FunctionToolAttribute.cs index ae33c0d..7636e61 100644 --- a/src/libs/CSharpToJsonSchema/FunctionToolAttribute.cs +++ b/src/libs/CSharpToJsonSchema/FunctionToolAttribute.cs @@ -16,4 +16,9 @@ public sealed class FunctionToolAttribute : Attribute /// Generate Google Function Tools extensions for Google_GenerativeAI SDK /// public bool GoogleFunctionTool { get; set; } + + /// + /// Generate Microsoft.Extension.AI compatible function tools + /// + public bool MeaiFunctionTool { get; set; } } \ No newline at end of file diff --git a/src/libs/CSharpToJsonSchema/GenerateJsonSchemaAttribute.cs b/src/libs/CSharpToJsonSchema/GenerateJsonSchemaAttribute.cs index 07e21c7..62465e0 100644 --- a/src/libs/CSharpToJsonSchema/GenerateJsonSchemaAttribute.cs +++ b/src/libs/CSharpToJsonSchema/GenerateJsonSchemaAttribute.cs @@ -17,4 +17,9 @@ public sealed class GenerateJsonSchemaAttribute : Attribute /// Generate Google Function Tools extensions for Google_GenerativeAI SDK /// public bool GoogleFunctionTool { get; set; } + + /// + /// Generate Microsoft.Extension.AI compatible function tools + /// + public bool MeaiFunctionTool { get; set; } } \ No newline at end of file diff --git a/src/libs/CSharpToJsonSchema/MeaiFunction.cs b/src/libs/CSharpToJsonSchema/MeaiFunction.cs new file mode 100644 index 0000000..d4bc318 --- /dev/null +++ b/src/libs/CSharpToJsonSchema/MeaiFunction.cs @@ -0,0 +1,100 @@ +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.AI; + +namespace CSharpToJsonSchema; + +/// +/// Represents a function that wraps a tool and provides functionality for executing the tool with specified arguments. +/// +public partial class MeaiFunction : AIFunction +{ + private readonly Tool _tool; + private readonly Func> _call; + + private JsonElement _jsonSchema; + + /// + /// Gets the JSON schema representing the parameters of the tool. + /// + public override JsonElement JsonSchema => _jsonSchema; + + /// + /// Gets the name of the tool. + /// + public override string Name => _tool.Name; + + /// + /// Gets the description of the tool. + /// + public override string Description => _tool.Description; + + /// + /// Gets additional properties associated with the tool. + /// + public override IReadOnlyDictionary AdditionalProperties => new ReadOnlyDictionary(_tool.AdditionalProperties); + + /// + /// Initializes a new instance of the class. + /// + /// The tool associated with this function. + /// The function to execute the tool with input arguments. + public MeaiFunction(Tool tool, Func> call) + { + this._tool = tool; + this._call = call; + + _jsonSchema = JsonSerializer.Deserialize( + JsonSerializer.Serialize(tool.Parameters, OpenApiSchemaJsonContext.Default.OpenApiSchema), + OpenApiSchemaJsonContext.Default.JsonElement); + + if (tool.Strict == true) + { + tool.AdditionalProperties.Add("Strict", true); + } + } + + /// + /// Invokes the tool with the given arguments asynchronously. + /// + /// The arguments to pass to the tool. + /// A cancellation token to cancel the operation, if needed. + /// The result of the tool's execution as a deserialized object. + protected override async Task InvokeCoreAsync(IEnumerable> arguments, + CancellationToken cancellationToken) + { + var json = GetArgsString(arguments); + + var call = await _call(json, cancellationToken); + + return JsonSerializer.Deserialize(call, OpenApiSchemaJsonContext.Default.JsonElement); + } + + /// + /// Converts a collection of arguments into a JSON string. + /// + /// The arguments to be converted into a JSON string. + /// A JSON string representation of the arguments. + private string GetArgsString(IEnumerable> arguments) + { + var jsonObject = new JsonObject(); + + foreach (var args in arguments) + { + if (args.Value is JsonElement element) + { + if (element.ValueKind == JsonValueKind.Array) + jsonObject[args.Key] = JsonArray.Create(element); + else if (element.ValueKind == JsonValueKind.Object) + jsonObject[args.Key] = JsonObject.Create(element); + } + else if (args.Value is JsonNode node) + { + jsonObject[args.Key] = node; + } + } + + return jsonObject.ToJsonString(); + } +} \ No newline at end of file diff --git a/src/libs/CSharpToJsonSchema/OpenApiSchemaJsonContext.cs b/src/libs/CSharpToJsonSchema/OpenApiSchemaJsonContext.cs index d612c1d..3ff76e9 100644 --- a/src/libs/CSharpToJsonSchema/OpenApiSchemaJsonContext.cs +++ b/src/libs/CSharpToJsonSchema/OpenApiSchemaJsonContext.cs @@ -5,6 +5,10 @@ namespace CSharpToJsonSchema; [JsonSerializable(typeof(OpenApiSchema))] [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(IDictionary))] +[JsonSerializable(typeof(Tool))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(JsonElement))] +[JsonSourceGenerationOptions(NumberHandling = JsonNumberHandling.Strict, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,WriteIndented = false)] public partial class OpenApiSchemaJsonContext:JsonSerializerContext { diff --git a/src/libs/CSharpToJsonSchema/SchemaSubsetHelper.cs b/src/libs/CSharpToJsonSchema/SchemaSubsetHelper.cs index 8c0f044..62198a0 100644 --- a/src/libs/CSharpToJsonSchema/SchemaSubsetHelper.cs +++ b/src/libs/CSharpToJsonSchema/SchemaSubsetHelper.cs @@ -110,39 +110,48 @@ public static OpenApiSchema ConvertToSchema(JsonSerializerOptions? jsonOption public static OpenApiSchema ConvertToSchema(JsonTypeInfo type, string descriptionString) { var typeInfo = type; - - var dics = JsonSerializer.Deserialize(descriptionString,OpenApiSchemaJsonContext.Default.IDictionaryStringString); + + var dics = JsonSerializer.Deserialize(descriptionString, + OpenApiSchemaJsonContext.Default.IDictionaryStringString); List required = new List(); - var x = ConvertToCompatibleSchemaSubset(typeInfo.GetJsonSchemaAsNode(exporterOptions:new JsonSchemaExporterOptions() - { - TransformSchemaNode = (a, b) => + var x = ConvertToCompatibleSchemaSubset(typeInfo.GetJsonSchemaAsNode( + exporterOptions: new JsonSchemaExporterOptions() { - if (a.PropertyInfo == null) - return b; - var propName = ToCamelCase(a.PropertyInfo.Name); - if (dics.ContainsKey(propName)) + TransformSchemaNode = (a, b) => { - b["description"] = dics[propName]; - } - return b; - }, - })); + if (a.TypeInfo.Type.IsEnum) + { + b["type"] = "string"; + } + + if (a.PropertyInfo == null) + return b; + var propName = ToCamelCase(a.PropertyInfo.Name); + if (dics.ContainsKey(propName)) + { + b["description"] = dics[propName]; + } + + return b; + }, + })); + - foreach (var re in x.Properties) { required.Add(re.Key); } - - var mainDescription =x.Description ?? dics["mainFunction_Desc"]; + + var mainDescription = x.Description ?? (dics.TryGetValue("mainFunction_Desc", out var desc) ? desc : ""); return new OpenApiSchema() { Description = mainDescription, Properties = x.Properties, - Required = required + Required = required, + Type = "object" }; } - + public static string ToCamelCase(string str) { if (!string.IsNullOrEmpty(str) && str.Length > 1) diff --git a/src/tests/CSharpToJsonSchema.MeaiTests/CSharpToJsonSchema.MeaiTests.csproj b/src/tests/CSharpToJsonSchema.MeaiTests/CSharpToJsonSchema.MeaiTests.csproj new file mode 100644 index 0000000..c6b2a8b --- /dev/null +++ b/src/tests/CSharpToJsonSchema.MeaiTests/CSharpToJsonSchema.MeaiTests.csproj @@ -0,0 +1,42 @@ + + + + net9.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/tests/CSharpToJsonSchema.MeaiTests/Meai_Tests.cs b/src/tests/CSharpToJsonSchema.MeaiTests/Meai_Tests.cs new file mode 100644 index 0000000..df6c4a6 --- /dev/null +++ b/src/tests/CSharpToJsonSchema.MeaiTests/Meai_Tests.cs @@ -0,0 +1,68 @@ +using System.ClientModel; +using CSharpToJsonSchema.MeaiTests.Services; +using Microsoft.Extensions.AI; +using OpenAI; +using OpenAI.Models; + +namespace CSharpToJsonSchema.MeaiTests; + +[TestClass] +public class Meai_Tests +{ + //[TestMethod] + public async Task ShouldInvokeTheFunctions() + { + var key = Environment.GetEnvironmentVariable("OPEN_AI_APIKEY",EnvironmentVariableTarget.User); + if (string.IsNullOrWhiteSpace(key)) + return; + + var client = new OpenAIClient(new ApiKeyCredential(key)); + + Microsoft.Extensions.AI.OpenAIChatClient openAiClient = new OpenAIChatClient(client.GetChatClient("gpt-4o-mini")); + + var chatClient = new Microsoft.Extensions.AI.FunctionInvokingChatClient(openAiClient); + var chatOptions = new ChatOptions(); + + var service = new StudentRecordService(); + var tools = new Tools([service.GetStudentRecordAsync]); + chatOptions.Tools = tools.AsMeaiTools(); + + var message = new ChatMessage(ChatRole.User, "How does student john doe in senior grade is doing this year, enrollment start 01-01-2024 to 01-01-2025?"); + var response = await chatClient.GetResponseAsync(message,options:chatOptions).ConfigureAwait(false); + + response.Choices.LastOrDefault().Text.Contains("John", StringComparison.InvariantCultureIgnoreCase).Should() + .Be(true); + + Console.WriteLine(response.Choices.LastOrDefault().Text); + + } + + //[TestMethod] + public async Task ShouldInvokeTheBookService() + { + var key = Environment.GetEnvironmentVariable("OPEN_AI_APIKEY",EnvironmentVariableTarget.User); + if (string.IsNullOrWhiteSpace(key)) + return; + var 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")); + + var chatClient = new Microsoft.Extensions.AI.FunctionInvokingChatClient(openAiClient); + var chatOptions = new ChatOptions(); + + var service = new BookStoreService(); + + chatOptions.Tools = service.AsMeaiTools(); + + var message = new ChatMessage(ChatRole.User, prompt); + var response = await chatClient.GetResponseAsync(message,options:chatOptions).ConfigureAwait(false); + + response.Choices.LastOrDefault().Text.Contains("damdamadum", StringComparison.InvariantCultureIgnoreCase).Should() + .Be(true); + + Console.WriteLine(response.Choices.LastOrDefault().Text); + + } +} \ No newline at end of file diff --git a/src/tests/CSharpToJsonSchema.MeaiTests/Services/BookService.cs b/src/tests/CSharpToJsonSchema.MeaiTests/Services/BookService.cs new file mode 100644 index 0000000..ec02ff8 --- /dev/null +++ b/src/tests/CSharpToJsonSchema.MeaiTests/Services/BookService.cs @@ -0,0 +1,38 @@ +using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; + + +namespace CSharpToJsonSchema.MeaiTests.Services; + +public class GetAuthorBook +{ + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; +} + +[GenerateJsonSchema(MeaiFunctionTool = true)] +public interface IBookStoreService +{ + [Description("Get books written by some author")] + public Task> GetAuthorBooksAsync([Description("Author name")] string authorName, CancellationToken cancellationToken = default); + + [Description("Get book page content")] + public Task GetBookPageContentAsync([Description("Book Name")] string bookName, [Description("Book Page Number")] int bookPageNumber, CancellationToken cancellationToken = default); + +} +public class BookStoreService : IBookStoreService +{ + public Task> GetAuthorBooksAsync(string authorName, CancellationToken cancellationToken = default) + { + return Task.FromResult(new List([ + new GetAuthorBook + { Title = "Five point someone", Description = "This book is about 3 college friends" }, + new GetAuthorBook + { Title = "Two States", Description = "This book is about intercast marriage in India" } + ])); + } + + public Task GetBookPageContentAsync(string bookName, int bookPageNumber, CancellationToken cancellationToken = default) + { + return Task.FromResult("this is a cool weather out there, and I am stuck at home."); + } +} \ No newline at end of file diff --git a/src/tests/CSharpToJsonSchema.MeaiTests/Services/StudentRecordService.cs b/src/tests/CSharpToJsonSchema.MeaiTests/Services/StudentRecordService.cs new file mode 100644 index 0000000..cd00df8 --- /dev/null +++ b/src/tests/CSharpToJsonSchema.MeaiTests/Services/StudentRecordService.cs @@ -0,0 +1,72 @@ +namespace CSharpToJsonSchema.MeaiTests.Services; +using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; + +public class StudentRecordService +{ + [System.ComponentModel.Description("Get student record for the year")] + [FunctionTool(MeaiFunctionTool = true)] + + public async Task GetStudentRecordAsync(QueryStudentRecordRequest query, CancellationToken cancellationToken = default) + { + return new StudentRecord + { + StudentId = "12345", + FullName = query.FullName, + Level = StudentRecord.GradeLevel.Senior, + EnrolledCourses = new List { "Math 101", "Physics 202", "History 303" }, + Grades = new Dictionary + { + { "Math 101", 3.5 }, + { "Physics 202", 3.8 }, + { "History 303", 3.9 } + }, + EnrollmentDate = new DateTime(2020, 9, 1), + IsActive = true + }; + } +} + +public class StudentRecord +{ + public enum GradeLevel + { + Freshman, + Sophomore, + Junior, + Senior, + Graduate + } + + public string StudentId { get; set; } = string.Empty; + public string FullName { get; set; } = string.Empty; + public GradeLevel Level { get; set; } = GradeLevel.Freshman; + public List EnrolledCourses { get; set; } = new List(); + public Dictionary Grades { get; set; } = new Dictionary(); + public DateTime EnrollmentDate { get; set; } = DateTime.Now; + public bool IsActive { get; set; } = true; + + public double CalculateGPA() + { + if (Grades.Count == 0) return 0.0; + return Grades.Values.Average(); + } +} + +[Description("Request class containing filters for querying student records.")] +public class QueryStudentRecordRequest +{ + [Description("The student's full name.")] + public string FullName { get; set; } = string.Empty; + + [Description("Grade filters for querying specific grades, e.g., Freshman or Senior.")] + public List GradeFilters { get; set; } = new(); + + [Description("The start date for the enrollment date range. ISO 8601 standard date")] + public DateTime EnrollmentStartDate { get; set; } + + [Description("The end date for the enrollment date range. ISO 8601 standard date")] + public DateTime EnrollmentEndDate { get; set; } + + [Description("The flag indicating whether to include only active students.")] + public bool? IsActive { get; set; } = true; +} diff --git a/src/tests/CSharpToJsonSchema.SnapshotTests/Snapshots/ToolTests.VariousTypes#IVariousTypesTools.g.received.cs b/src/tests/CSharpToJsonSchema.SnapshotTests/Snapshots/ToolTests.VariousTypes#IVariousTypesTools.g.received.cs new file mode 100644 index 0000000..6650673 --- /dev/null +++ b/src/tests/CSharpToJsonSchema.SnapshotTests/Snapshots/ToolTests.VariousTypes#IVariousTypesTools.g.received.cs @@ -0,0 +1,111 @@ +//HintName: IVariousTypesTools.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace CSharpToJsonSchema.IntegrationTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CSharpToJsonSchema.Generators", "0.0.0.0")] + public partial class VariousTypesToolsExtensionsJsonSerializerContext + { + private readonly static global::System.Text.Json.JsonSerializerOptions s_defaultOptions = new(global::System.Text.Json.JsonSerializerDefaults.Web) + { + AllowOutOfOrderMetadataProperties = true, + AllowTrailingCommas = false, + DefaultBufferSize = 1024, + DefaultIgnoreCondition = global::System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + RespectNullableAnnotations = true, + RespectRequiredConstructorParameters = false, + IgnoreReadOnlyFields = false, + IgnoreReadOnlyProperties = false, + IncludeFields = false, + MaxDepth = 64, + NewLine = "\n", + NumberHandling = global::System.Text.Json.Serialization.JsonNumberHandling.Strict, + PreferredObjectCreationHandling = global::System.Text.Json.Serialization.JsonObjectCreationHandling.Replace, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = global::System.Text.Json.JsonNamingPolicy.CamelCase, + ReadCommentHandling = global::System.Text.Json.JsonCommentHandling.Disallow, + UnknownTypeHandling = global::System.Text.Json.Serialization.JsonUnknownTypeHandling.JsonElement, + UnmappedMemberHandling = global::System.Text.Json.Serialization.JsonUnmappedMemberHandling.Skip, + WriteIndented = false, + IndentCharacter = ' ', + IndentSize = 2, + }; + + private const global::System.Reflection.BindingFlags InstanceMemberBindingFlags = + global::System.Reflection.BindingFlags.Instance | + global::System.Reflection.BindingFlags.Public | + global::System.Reflection.BindingFlags.NonPublic; + + /// + /// The default associated with a default instance. + /// + public static global::CSharpToJsonSchema.IntegrationTests.VariousTypesToolsExtensionsJsonSerializerContext Default { get; } = new global::CSharpToJsonSchema.IntegrationTests.VariousTypesToolsExtensionsJsonSerializerContext(new global::System.Text.Json.JsonSerializerOptions(s_defaultOptions)); + + /// + /// The source-generated options associated with this context. + /// + protected override global::System.Text.Json.JsonSerializerOptions? GeneratedSerializerOptions { get; } = s_defaultOptions; + + /// + public VariousTypesToolsExtensionsJsonSerializerContext() : base(null) + { + } + + /// + public VariousTypesToolsExtensionsJsonSerializerContext(global::System.Text.Json.JsonSerializerOptions options) : base(options) + { + } + + private static bool TryGetTypeInfoForRuntimeCustomConverter(global::System.Text.Json.JsonSerializerOptions options, out global::System.Text.Json.Serialization.Metadata.JsonTypeInfo jsonTypeInfo) + { + global::System.Text.Json.Serialization.JsonConverter? converter = GetRuntimeConverterForType(typeof(TJsonMetadataType), options); + if (converter != null) + { + jsonTypeInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateValueInfo(options, converter); + return true; + } + + jsonTypeInfo = null; + return false; + } + + private static global::System.Text.Json.Serialization.JsonConverter? GetRuntimeConverterForType(global::System.Type type, global::System.Text.Json.JsonSerializerOptions options) + { + for (int i = 0; i < options.Converters.Count; i++) + { + global::System.Text.Json.Serialization.JsonConverter? converter = options.Converters[i]; + if (converter?.CanConvert(type) == true) + { + return ExpandConverter(type, converter, options, validateCanConvert: false); + } + } + + return null; + } + + private static global::System.Text.Json.Serialization.JsonConverter ExpandConverter(global::System.Type type, global::System.Text.Json.Serialization.JsonConverter converter, global::System.Text.Json.JsonSerializerOptions options, bool validateCanConvert = true) + { + if (validateCanConvert && !converter.CanConvert(type)) + { + throw new global::System.InvalidOperationException(string.Format("The converter '{0}' is not compatible with the type '{1}'.", converter.GetType(), type)); + } + + if (converter is global::System.Text.Json.Serialization.JsonConverterFactory factory) + { + converter = factory.CreateConverter(type, options); + if (converter is null || converter is global::System.Text.Json.Serialization.JsonConverterFactory) + { + throw new global::System.InvalidOperationException(string.Format("The converter '{0}' cannot return null or a JsonConverterFactory instance.", factory.GetType())); + } + } + + return converter; + } + } +} diff --git a/src/tests/CSharpToJsonSchema.SnapshotTests/Snapshots/ToolTests.Weather#IWeatherTools.g.received.cs b/src/tests/CSharpToJsonSchema.SnapshotTests/Snapshots/ToolTests.Weather#IWeatherTools.g.received.cs new file mode 100644 index 0000000..f72b82b --- /dev/null +++ b/src/tests/CSharpToJsonSchema.SnapshotTests/Snapshots/ToolTests.Weather#IWeatherTools.g.received.cs @@ -0,0 +1,111 @@ +//HintName: IWeatherTools.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace CSharpToJsonSchema.IntegrationTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CSharpToJsonSchema.Generators", "0.0.0.0")] + public partial class WeatherToolsExtensionsJsonSerializerContext + { + private readonly static global::System.Text.Json.JsonSerializerOptions s_defaultOptions = new(global::System.Text.Json.JsonSerializerDefaults.Web) + { + AllowOutOfOrderMetadataProperties = true, + AllowTrailingCommas = false, + DefaultBufferSize = 1024, + DefaultIgnoreCondition = global::System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + RespectNullableAnnotations = true, + RespectRequiredConstructorParameters = false, + IgnoreReadOnlyFields = false, + IgnoreReadOnlyProperties = false, + IncludeFields = false, + MaxDepth = 64, + NewLine = "\n", + NumberHandling = global::System.Text.Json.Serialization.JsonNumberHandling.Strict, + PreferredObjectCreationHandling = global::System.Text.Json.Serialization.JsonObjectCreationHandling.Replace, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = global::System.Text.Json.JsonNamingPolicy.CamelCase, + ReadCommentHandling = global::System.Text.Json.JsonCommentHandling.Disallow, + UnknownTypeHandling = global::System.Text.Json.Serialization.JsonUnknownTypeHandling.JsonElement, + UnmappedMemberHandling = global::System.Text.Json.Serialization.JsonUnmappedMemberHandling.Skip, + WriteIndented = false, + IndentCharacter = ' ', + IndentSize = 2, + }; + + private const global::System.Reflection.BindingFlags InstanceMemberBindingFlags = + global::System.Reflection.BindingFlags.Instance | + global::System.Reflection.BindingFlags.Public | + global::System.Reflection.BindingFlags.NonPublic; + + /// + /// The default associated with a default instance. + /// + public static global::CSharpToJsonSchema.IntegrationTests.WeatherToolsExtensionsJsonSerializerContext Default { get; } = new global::CSharpToJsonSchema.IntegrationTests.WeatherToolsExtensionsJsonSerializerContext(new global::System.Text.Json.JsonSerializerOptions(s_defaultOptions)); + + /// + /// The source-generated options associated with this context. + /// + protected override global::System.Text.Json.JsonSerializerOptions? GeneratedSerializerOptions { get; } = s_defaultOptions; + + /// + public WeatherToolsExtensionsJsonSerializerContext() : base(null) + { + } + + /// + public WeatherToolsExtensionsJsonSerializerContext(global::System.Text.Json.JsonSerializerOptions options) : base(options) + { + } + + private static bool TryGetTypeInfoForRuntimeCustomConverter(global::System.Text.Json.JsonSerializerOptions options, out global::System.Text.Json.Serialization.Metadata.JsonTypeInfo jsonTypeInfo) + { + global::System.Text.Json.Serialization.JsonConverter? converter = GetRuntimeConverterForType(typeof(TJsonMetadataType), options); + if (converter != null) + { + jsonTypeInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateValueInfo(options, converter); + return true; + } + + jsonTypeInfo = null; + return false; + } + + private static global::System.Text.Json.Serialization.JsonConverter? GetRuntimeConverterForType(global::System.Type type, global::System.Text.Json.JsonSerializerOptions options) + { + for (int i = 0; i < options.Converters.Count; i++) + { + global::System.Text.Json.Serialization.JsonConverter? converter = options.Converters[i]; + if (converter?.CanConvert(type) == true) + { + return ExpandConverter(type, converter, options, validateCanConvert: false); + } + } + + return null; + } + + private static global::System.Text.Json.Serialization.JsonConverter ExpandConverter(global::System.Type type, global::System.Text.Json.Serialization.JsonConverter converter, global::System.Text.Json.JsonSerializerOptions options, bool validateCanConvert = true) + { + if (validateCanConvert && !converter.CanConvert(type)) + { + throw new global::System.InvalidOperationException(string.Format("The converter '{0}' is not compatible with the type '{1}'.", converter.GetType(), type)); + } + + if (converter is global::System.Text.Json.Serialization.JsonConverterFactory factory) + { + converter = factory.CreateConverter(type, options); + if (converter is null || converter is global::System.Text.Json.Serialization.JsonConverterFactory) + { + throw new global::System.InvalidOperationException(string.Format("The converter '{0}' cannot return null or a JsonConverterFactory instance.", factory.GetType())); + } + } + + return converter; + } + } +}