diff --git a/Google_GenerativeAI.sln b/Google_GenerativeAI.sln index 286785a7..8ba281c5 100644 --- a/Google_GenerativeAI.sln +++ b/Google_GenerativeAI.sln @@ -48,6 +48,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwoWayAudioCommunicationWpf EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenerativeAI.Live.Tests", "tests\GenerativeAI.Live.Tests\GenerativeAI.Live.Tests.csproj", "{157399AE-E8D1-4306-8B59-0BDEB45AED03}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AotTest", "tests\AotTest\AotTest.csproj", "{5BF6737C-D3E4-4C46-ABBF-73ECAEE128AF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -126,6 +128,10 @@ Global {157399AE-E8D1-4306-8B59-0BDEB45AED03}.Debug|Any CPU.Build.0 = Debug|Any CPU {157399AE-E8D1-4306-8B59-0BDEB45AED03}.Release|Any CPU.ActiveCfg = Release|Any CPU {157399AE-E8D1-4306-8B59-0BDEB45AED03}.Release|Any CPU.Build.0 = Release|Any CPU + {5BF6737C-D3E4-4C46-ABBF-73ECAEE128AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BF6737C-D3E4-4C46-ABBF-73ECAEE128AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BF6737C-D3E4-4C46-ABBF-73ECAEE128AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BF6737C-D3E4-4C46-ABBF-73ECAEE128AF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -149,6 +155,7 @@ Global {ACB3E4E1-F967-45E7-81CD-70A4F7785AED} = {AC161F1D-EC76-48D2-86A3-B52584618D49} {8DC0FF3E-8B46-41B0-B814-6049FD80C8C3} = {61CC49B3-1325-40EB-95DF-89E18A0D041B} {157399AE-E8D1-4306-8B59-0BDEB45AED03} = {FCCDE15A-B121-4D6C-BD56-D1B043A26F18} + {5BF6737C-D3E4-4C46-ABBF-73ECAEE128AF} = {FCCDE15A-B121-4D6C-BD56-D1B043A26F18} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FFF3E8BB-BACD-4376-8E33-55D6E8A30BE0} diff --git a/samples/TwoWayAudioCommunicationWpf/TwoWayAudioCommunicationWpf.csproj b/samples/TwoWayAudioCommunicationWpf/TwoWayAudioCommunicationWpf.csproj index db5b0724..4b42db30 100644 --- a/samples/TwoWayAudioCommunicationWpf/TwoWayAudioCommunicationWpf.csproj +++ b/samples/TwoWayAudioCommunicationWpf/TwoWayAudioCommunicationWpf.csproj @@ -7,6 +7,7 @@ enable true true + diff --git a/src/GenerativeAI.Auth/GenerativeAI.Auth.csproj b/src/GenerativeAI.Auth/GenerativeAI.Auth.csproj index e7d102c4..d0800048 100644 --- a/src/GenerativeAI.Auth/GenerativeAI.Auth.csproj +++ b/src/GenerativeAI.Auth/GenerativeAI.Auth.csproj @@ -21,6 +21,7 @@ True GenerativeAI.Authenticators GenerativeAI.Authenticators + diff --git a/src/GenerativeAI.Live/GenerativeAI.Live.csproj b/src/GenerativeAI.Live/GenerativeAI.Live.csproj index 8924f4c6..62595f53 100644 --- a/src/GenerativeAI.Live/GenerativeAI.Live.csproj +++ b/src/GenerativeAI.Live/GenerativeAI.Live.csproj @@ -19,6 +19,7 @@ 2.3.1 True True + true diff --git a/src/GenerativeAI.Live/Models/MultiModalLiveClient.cs b/src/GenerativeAI.Live/Models/MultiModalLiveClient.cs index b4a932e4..896ed4fa 100644 --- a/src/GenerativeAI.Live/Models/MultiModalLiveClient.cs +++ b/src/GenerativeAI.Live/Models/MultiModalLiveClient.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using GenerativeAI.Core; using GenerativeAI.Live.Helper; using GenerativeAI.Live.Logging; @@ -183,11 +184,11 @@ private void ProcessReceivedMessage(ResponseMessage msg) BidiResponsePayload? responsePayload = null; if (msg.MessageType == WebSocketMessageType.Binary) { - responsePayload = JsonSerializer.Deserialize(msg.Binary); + responsePayload = JsonSerializer.Deserialize(msg.Binary,(JsonTypeInfo) DefaultSerializerOptions.Options.GetTypeInfo(typeof(BidiResponsePayload))); } else { - responsePayload = JsonSerializer.Deserialize(msg.Text); + responsePayload = JsonSerializer.Deserialize(msg.Text,(JsonTypeInfo) DefaultSerializerOptions.Options.GetTypeInfo(typeof(BidiResponsePayload))); } if (responsePayload == null) @@ -228,7 +229,7 @@ private void ProcessTextChunk(BidiResponsePayload responsePayload) { if (part.Text != null) { - this.TextChunkReceived.Invoke(this, + this.TextChunkReceived?.Invoke(this, new TextChunkReceivedArgs(part.Text, responsePayload.ServerContent.TurnComplete == true)); _logger?.LogInformation("Text chunk received: {Text}", part.Text); } @@ -565,7 +566,7 @@ private async Task SendAsync(BidiClientPayload payload, CancellationToken cancel try { - var json = JsonSerializer.Serialize(payload,DefaultSerializerOptions.Options); + var json = JsonSerializer.Serialize(payload,DefaultSerializerOptions.Options.GetTypeInfo(typeof(BidiClientPayload))); _logger?.LogMessageSent(json); _client.Send(json); diff --git a/src/GenerativeAI.Microsoft/Extensions/MicrosoftExtensions.cs b/src/GenerativeAI.Microsoft/Extensions/MicrosoftExtensions.cs index 40e08c62..6151d3b7 100644 --- a/src/GenerativeAI.Microsoft/Extensions/MicrosoftExtensions.cs +++ b/src/GenerativeAI.Microsoft/Extensions/MicrosoftExtensions.cs @@ -1,5 +1,5 @@ using GenerativeAI.Types; -using Json.More; + using Microsoft.Extensions.AI; using System.Text.Json; using System.Text.Json.Nodes; @@ -70,8 +70,8 @@ where p is not null /// A object constructed from the provided JSON schema, or null if deserialization fails. public static Schema? ToSchema(this JsonElement schema) { - - var serialized = JsonSerializer.Serialize(schema); + return GoogleSchemaHelper.ConvertToCompatibleSchemaSubset(schema.AsNode().AsObject()); + var serialized = JsonSerializer.Serialize(schema, DefaultSerializerOptions.Options.GetTypeInfo(schema.GetType())); return JsonSerializer.Deserialize(serialized,SchemaSourceGenerationContext.Default.Schema); } @@ -98,7 +98,7 @@ where p is not null FunctionCall = new FunctionCall() { Name = fcc.Name, - Args = fcc.Arguments!, + Args = fcc.Arguments.ToJsonNode(), } }, FunctionResultContent frc => new Part @@ -106,19 +106,61 @@ where p is not null FunctionResponse = new FunctionResponse() { Name = frc.CallId, - Response = new - { - Name = frc.CallId, - Content = JsonSerializer.SerializeToNode(frc.Result)!, - } + Response = frc.ToJsonNodeResponse() } }, _ => null, }; } - + private static JsonNode ToJsonNode(this IDictionary? args) + { + var node = new JsonObject(); + foreach (var arg in args!) + { + if (arg.Value is JsonNode nd) + node.Add(arg.Key, nd.DeepClone()); + else + { + var p = arg.Value switch + { + string s => s, + int i => i, + float f => f, + double d => d, + bool b => b, + null => null, + JsonElement e => e.AsNode()?.AsObject(), + JsonNode n => n switch + { + JsonObject o => o, + JsonArray a => a, + JsonValue v => v.GetValue().AsNode() + }, + _ => throw new Exception("Unsupported argument type") + }; + node.Add(arg.Key, p); + } + } + return node; //JsonSerializer.Deserialize(node.ToJsonString(), TypesSerializerContext.Default.JsonElement)!; + } + public static JsonNode ToJsonNodeResponse(this object? response) + { + if (response is FunctionResultContent content) + { + if (content.Result is JsonObject obj) + return obj; + else if (content.Result is JsonNode arr) + return arr; + } + if(response is JsonNode node) + { + return node; + } + else throw new Exception("Response is not a json node"); + + } /// /// Maps into a object used by GenerativeAI. /// @@ -137,7 +179,7 @@ where p is not null config.TopK = options.TopK; config.MaxOutputTokens = options.MaxOutputTokens; config.StopSequences = options.StopSequences?.ToList(); - config.Seed = (int) options.Seed!; + config.Seed = (int?) options.Seed; config.ResponseMimeType = options.ResponseFormat is ChatResponseFormatJson ? "application/json" : null; if (options.ResponseFormat is ChatResponseFormatJson jsonFormat) { @@ -145,7 +187,7 @@ where p is not null if (jsonFormat.Schema is JsonElement je && je.ValueKind == JsonValueKind.Object) { // Workaround to convert our real json schema to the format Google's api expects - var forGoogleApi = GoogleSchemaHelper.ConvertToCompatibleSchemaSubset(je.ToJsonDocument()); + var forGoogleApi = GoogleSchemaHelper.ConvertToCompatibleSchemaSubset(je.AsNode()); config.ResponseSchema = forGoogleApi; } } @@ -396,17 +438,23 @@ public static IList ToAiContents(this List? parts) /// A dictionary where the keys represent argument names and values represent their corresponding data, or null if conversion is not possible. private static IDictionary? ConvertFunctionCallArg(object? functionCallArgs) { - if (functionCallArgs != null && functionCallArgs is not JsonElement) + if (functionCallArgs is JsonElement jsonElement) { - functionCallArgs = JsonSerializer.Deserialize(JsonSerializer.Serialize(functionCallArgs)); + var obj = jsonElement.AsNode().AsObject(); + return obj?.ToDictionary(s=>s.Key,s=>(object?)s.Value?.DeepClone()); + } - if (functionCallArgs is JsonElement jsonElement) + if (functionCallArgs is JsonNode jsonElement2) { - if (jsonElement.ValueKind == JsonValueKind.Object) - { - var obj = JsonObject.Create(jsonElement); - return obj?.ToDictionary(s=>s.Key,s=>(object?)s.Value); - } + var obj = jsonElement2.AsObject(); + return obj?.ToDictionary(s=>s.Key,s=>(object?)s.Value?.DeepClone()); + } + else if (functionCallArgs != null && functionCallArgs is not JsonNode) + { + throw new Exception("Unsupported function call argument type"); + // #pragma warning disable IL2026, IL3050 + // functionCallArgs = JsonSerializer.Deserialize(JsonSerializer.Serialize(functionCallArgs)); + // #pragma warning restore IL2026, IL3050 } return null; diff --git a/src/GenerativeAI.Microsoft/GenerativeAI.Microsoft.csproj b/src/GenerativeAI.Microsoft/GenerativeAI.Microsoft.csproj index 55e0d906..ee4c5ebb 100644 --- a/src/GenerativeAI.Microsoft/GenerativeAI.Microsoft.csproj +++ b/src/GenerativeAI.Microsoft/GenerativeAI.Microsoft.csproj @@ -21,6 +21,7 @@ 2.3.1 True True + true diff --git a/src/GenerativeAI.Microsoft/GenerativeAIChatClient.cs b/src/GenerativeAI.Microsoft/GenerativeAIChatClient.cs index 1406b22b..1c071ba6 100644 --- a/src/GenerativeAI.Microsoft/GenerativeAIChatClient.cs +++ b/src/GenerativeAI.Microsoft/GenerativeAIChatClient.cs @@ -1,4 +1,6 @@ using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Nodes; using GenerativeAI.Core; using GenerativeAI.Exceptions; using GenerativeAI.Microsoft.Extensions; @@ -70,15 +72,15 @@ private async Task CallFunctionAsync(GenerateContentRequest reques var content = response.Candidates?.FirstOrDefault()?.Content; if (content != null) contents.Add(content); + var responseObject = new JsonObject(); + responseObject["name"] = functionCall.Name; + responseObject["content"] = ((JsonElement)result).AsNode().DeepClone(); + //responseObject["content"] = result as JsonNode; var functionResponse = new FunctionResponse() { Name = tool.Name, Id = functionCall.CallId, - Response = new - { - Name = tool.Name, - Content = result - } + Response = responseObject }; var funcContent = new Content() { Role = Roles.Function }; funcContent.AddPart(new Part() diff --git a/src/GenerativeAI.Tools/GenerativeAI.Tools.csproj b/src/GenerativeAI.Tools/GenerativeAI.Tools.csproj index 219c558a..e16355b4 100644 --- a/src/GenerativeAI.Tools/GenerativeAI.Tools.csproj +++ b/src/GenerativeAI.Tools/GenerativeAI.Tools.csproj @@ -22,12 +22,13 @@ 2.3.1 True True + true - + diff --git a/src/GenerativeAI.Tools/GenericFunctionTool.cs b/src/GenerativeAI.Tools/GenericFunctionTool.cs index f2bb194e..48148c13 100644 --- a/src/GenerativeAI.Tools/GenericFunctionTool.cs +++ b/src/GenerativeAI.Tools/GenericFunctionTool.cs @@ -1,7 +1,9 @@ -using System.Text.Json.Nodes; +using System.Text.Json; +using System.Text.Json.Nodes; using CSharpToJsonSchema; using GenerativeAI.Core; using GenerativeAI.Types; + using JsonSerializer = System.Text.Json.JsonSerializer; using Tool = GenerativeAI.Types.Tool; @@ -13,7 +15,7 @@ namespace GenerativeAI.Tools; /// It utilizes the code generation capabilities available in CSharpToJsonSchema for transforming /// tool definitions into executable formats and managing function invocations. /// -public class GenericFunctionTool:IFunctionTool +public class GenericFunctionTool:GoogleFunctionTool { /// /// Represents a generic functional tool that enables interaction with a set of tools and their associated functions, @@ -29,7 +31,7 @@ public GenericFunctionTool(IEnumerable tools, IReadOnly /// - public Tool AsTool() + public override Tool AsTool() { return new Tool() { @@ -44,30 +46,52 @@ public Tool AsTool() private Schema? ToSchema(object parameters) { - var param = JsonSerializer.Serialize(parameters); + var param = JsonSerializer.Serialize(parameters, OpenApiSchemaSourceGenerationContext.Default.OpenApiSchema); return JsonSerializer.Deserialize(param,SchemaSourceGenerationContext.Default.Schema); } /// - public async Task CallAsync(FunctionCall functionCall, CancellationToken cancellationToken = default) + public override async Task CallAsync(FunctionCall functionCall, CancellationToken cancellationToken = default) { + #pragma disable warning IL2026, IL3050 if (this.Calls.TryGetValue(functionCall.Name, out var call)) { - var str = JsonSerializer.Serialize(functionCall.Args); - var response = await call(str, cancellationToken).ConfigureAwait(false); + string? args = null; + if (functionCall.Args !=null) + { + args = functionCall.Args.ToJsonString(); + } + // else if (functionCall.Args is JsonNode jsonNode) + // { + // args = jsonNode.ToJsonString(); + // } + // else if (functionCall.Args is JsonObject jsonObject) + // { + // args = jsonObject.ToJsonString(); + // } + else + { + throw new NotImplementedException(); + //args = JsonSerializer.Serialize(functionCall.Args, DefaultSerializerOptions.Options.GetTypeInfo()); + } + var response = await call(args, cancellationToken).ConfigureAwait(false); var node = JsonNode.Parse(response); + var responseNode = new JsonObject(); - return new FunctionResponse() { Id = functionCall.Id, Name = functionCall.Name, Response = new { - Name = functionCall.Name, - Content = node, - } }; + responseNode["name"] = functionCall.Name; + responseNode["content"] = node; + return new FunctionResponse() { Id = functionCall.Id, Name = functionCall.Name, + + Response = responseNode, + }; +#pragma restore warning IL2026, IL3050 } return null; } /// - public bool IsContainFunction(string name) + public override bool IsContainFunction(string name) { return Tools.Any(s => s.Name == name); } diff --git a/src/GenerativeAI.Tools/Helpers/FunctionSchemaHelper.cs b/src/GenerativeAI.Tools/Helpers/FunctionSchemaHelper.cs new file mode 100644 index 00000000..9b54f054 --- /dev/null +++ b/src/GenerativeAI.Tools/Helpers/FunctionSchemaHelper.cs @@ -0,0 +1,87 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using GenerativeAI.Types; + +namespace GenerativeAI.Tools.Helpers; + +public static class FunctionSchemaHelper +{ + #if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Create Schema will perform reflection on the delegate type provided to generate Schema")] + #endif + public static FunctionDeclaration CreateFunctionDecleration(Delegate func, string? name, string? description) + { + var parameters = func.Method.GetParameters(); + Schema parametersSchema = new Schema(); + var options = DefaultSerializerOptions.GenerateObjectJsonOptions; + + parametersSchema.Properties = new Dictionary(); + parametersSchema.Required = new List(); + parametersSchema.Type = "object"; + foreach (var param in parameters) + { + + var type = param.ParameterType; + if(type.Name == "CancellationToken") + continue; + var descriptionsDics = GetDescriptionDic(type); + var desc = GetDescription(param); + descriptionsDics[param.Name.ToCamelCase()] = desc; + + var schema = GoogleSchemaHelper.ConvertToSchema(type, options, descriptionsDics); + schema.Description = desc; + parametersSchema.Properties.Add(param.Name.ToCamelCase(), schema); + parametersSchema.Required.Add(param.Name.ToCamelCase()); + } + + var functionDescription = GetDescription(func.Method); + + FunctionDeclaration functionObject = new FunctionDeclaration(); + functionObject.Description = description ?? functionDescription; + functionObject.Parameters = parametersSchema; + functionObject.Name = name ?? func.Method.Name; + + return functionObject; + } + + public static string GetDescription(ParameterInfo paramInfo) + { + var attribute = paramInfo.GetCustomAttribute(); + return attribute?.Description ?? string.Empty; + } + + private static Dictionary GetDescriptionDic(Type type, Dictionary? descriptions = null) + { + descriptions = descriptions ?? new Dictionary(); + descriptions[type.Name.ToCamelCase()] = GetDescription(type); + foreach (var member in type.GetMembers()) + { + var description = GetDescription(member); + if (!string.IsNullOrEmpty(description)) + { + descriptions[member.Name.ToCamelCase()] = description; + } + + if (member.MemberType == MemberTypes.TypeInfo || member.MemberType == MemberTypes.NestedType) + { + var nestedType = member as Type; + if (nestedType != null) + { + GetDescriptionDic(nestedType, descriptions); + } + } + } + + return descriptions; + } + + private static string GetDescription(MemberInfo member) + { + var attribute = member.GetCustomAttribute(); + return attribute?.Description ?? string.Empty; + } +} \ No newline at end of file diff --git a/src/GenerativeAI.Tools/OpenApiSchemaSourceGenerationContext.cs b/src/GenerativeAI.Tools/OpenApiSchemaSourceGenerationContext.cs new file mode 100644 index 00000000..9b2aab67 --- /dev/null +++ b/src/GenerativeAI.Tools/OpenApiSchemaSourceGenerationContext.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; +using CSharpToJsonSchema; + +namespace GenerativeAI.Tools; + +[JsonSerializable(typeof(OpenApiSchema))] +[JsonSourceGenerationOptions(WriteIndented = true)] +public partial class OpenApiSchemaSourceGenerationContext:JsonSerializerContext +{ + +} \ No newline at end of file diff --git a/src/GenerativeAI.Tools/QuickTool.cs b/src/GenerativeAI.Tools/QuickTool.cs new file mode 100644 index 00000000..c5617fb8 --- /dev/null +++ b/src/GenerativeAI.Tools/QuickTool.cs @@ -0,0 +1,176 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using GenerativeAI.Core; +using GenerativeAI.Tools.Helpers; +using GenerativeAI.Types; + +namespace GenerativeAI.Tools; + +/// +/// Quick Function Tool, +/// +/// +/// This class usages reflection and is not compatible with AOT +/// +public class QuickTool : GoogleFunctionTool +{ + /// + /// Represents the declaration of a function used within the tool. + /// + /// + /// This property contains metadata about a function, such as its name, + /// description, and parameter schema. It's primarily used for defining + /// and managing functions available to tools in the framework. + /// + public FunctionDeclaration FunctionDeclaration { get; private set; } + private Delegate _func; + +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("QuickTool usages reflection to generate function schema and function invokation. Use GenericFunctionTool for NativeAOT and Trimming support.")] +#endif + /// + /// Represents a tool capable of leveraging a delegate function as its core functionality. + /// + /// + /// QuickTool is designed to work with a provided delegate, exposing its functionality as a tool. + /// Note that this class utilizes runtime reflection and is not compatible with Ahead-Of-Time (AOT) compilation. + /// + public QuickTool(Delegate func, string? name = null, string? description = null) + { + this._func = func; + this.FunctionDeclaration = FunctionSchemaHelper.CreateFunctionDecleration(func, name, description); + } + + /// + public override Tool AsTool() + { + return new Tool() + { + FunctionDeclarations = new List() { FunctionDeclaration } + }; + } + + /// + public override async Task CallAsync(FunctionCall functionCall, + CancellationToken cancellationToken = default) + { + if (FunctionDeclaration.Name != functionCall.Name) + throw new ArgumentException("Function name does not match"); + object?[]? param = MarshalParameters(functionCall.Args, cancellationToken); + + var result = await InvokeAsTaskAsync(_func, param); + var responseNode = new JsonObject(); + responseNode["name"] = functionCall.Name; + + if (result != null) + { + var x = JsonSerializer.Serialize(result, DefaultSerializerOptions.GenerateObjectJsonOptions); + var node = JsonNode.Parse(x); + responseNode["content"] = node; + } + else + { + responseNode["content"] = string.Empty; + } + return new FunctionResponse() + { + Id = functionCall.Id, + Name = functionCall.Name, + Response = responseNode + };; + } + +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("The constructor will perform reflection on the delegate type provided")] +#endif + private async Task InvokeAsTaskAsync(Delegate function, object?[]? parameters) + { + // Dynamically invoke + var result = function.DynamicInvoke(parameters); + + // If it’s already a non-Task, just return it + if (result is not Task) + return result; + + // If the result is Task, figure out the T in Task, if any + var resultType = result.GetType(); + if (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(Task<>)) + { + // This is Task. We can reflect on its "Result" property after awaiting + var task = (Task)result; + await task.ConfigureAwait(false); + + // Retrieve the .Result property via reflection + var resultProperty = resultType.GetProperty("Result"); + return resultProperty?.GetValue(task); + } + else + { + // If it's just Task (no generic), await and return null + var task = (Task)result; + await task.ConfigureAwait(false); + return null; + } + } + + + /// + /// Marshals the parameters provided in the format into an object array suitable for method invocation. + /// + /// The JSON node containing the arguments for the function call. + /// The cancellation token to handle any asynchronous operation cancellations. + /// An object array of marshaled parameters, or null if the is null. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("The constructor will perform reflection on the delegate type provided")] +#endif + private object?[]? MarshalParameters(JsonNode? functionCallArgs, CancellationToken cancellationToken) + { + if (functionCallArgs == null) + return null; + + List objects = new List(); + + // Iterate over the parameters of the delegate function + foreach (var param in _func.Method.GetParameters()) + { + // If the parameter is a CancellationToken, directly add it + if (param.ParameterType.Name == "CancellationToken") + { + objects.Add(cancellationToken); + continue; + } + + // Retrieve the parameter value from the JSON node using its name in camelCase + var val = functionCallArgs[param.Name.ToCamelCase()]; + + // If the value is not provided, add null + if (val == null) + { + objects.Add(null); + } + else + { + // Deserialize the JSON value into the expected parameter type + var obj = val.Deserialize(param.ParameterType, DefaultSerializerOptions.GenerateObjectJsonOptions); + objects.Add(obj); + } + } + + return objects.ToArray(); + } + + /// + /// Determines whether the specified function name matches the name of the function declared in the tool's FunctionDeclaration property. + /// + /// The name of the function to check for existence within the tool's declaration. + /// A boolean value indicating whether the specified function name is contained within the FunctionDeclaration. + public override bool IsContainFunction(string name) + { + return FunctionDeclaration.Name == name; + } +} \ No newline at end of file diff --git a/src/GenerativeAI.Tools/QuickTools.cs b/src/GenerativeAI.Tools/QuickTools.cs new file mode 100644 index 00000000..ed33adbf --- /dev/null +++ b/src/GenerativeAI.Tools/QuickTools.cs @@ -0,0 +1,62 @@ +using System.Diagnostics.CodeAnalysis; +using GenerativeAI.Core; +using GenerativeAI.Types; + +namespace GenerativeAI.Tools; + +/// +/// Represents a collection of quick tools that can be used as Google function tools. +/// +public class QuickTools : GoogleFunctionTool +{ + private readonly List _tools; + + /// + /// Initializes a new instance of the class with an array of objects. + /// + /// An array of objects to initialize the tool collection. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("QuickTool usages reflection to generate function schema and function invokation. Use GenericFunctionTool for NativeAOT and Trimming support.")] +#endif + public QuickTools(QuickTool[] tools) + { + _tools = tools.ToList(); + } + + /// + /// Initializes a new instance of the class with an array of delegates. + /// + /// An array of delegates to be converted into objects. +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("QuickTool usages reflection to generate function schema and function invokation. Use GenericFunctionTool for NativeAOT and Trimming support.")] +#endif + public QuickTools(Delegate[] delegates) + { + _tools = delegates.Select(s => new QuickTool(s)).ToList(); + } + + /// + public override Tool AsTool() + { + return new Tool() + { + FunctionDeclarations = this._tools.Select(s => s.FunctionDeclaration).ToList(), + }; + } + + /// + public override async Task CallAsync(FunctionCall functionCall, + CancellationToken cancellationToken = default) + { + var ft = _tools.FirstOrDefault(s => s.FunctionDeclaration.Name == functionCall.Name); + if (ft == null) + throw new ArgumentException("Function name does not match"); + return await ft.CallAsync(functionCall, cancellationToken); + } + + /// + public override bool IsContainFunction(string name) + { + return _tools.Any(s => s.FunctionDeclaration.Name == name); + } +} \ No newline at end of file diff --git a/src/GenerativeAI/AiModels/GenerativeModel/GenerativeModel.JsonMode.cs b/src/GenerativeAI/AiModels/GenerativeModel/GenerativeModel.JsonMode.cs index e542ff22..ed8ddf92 100644 --- a/src/GenerativeAI/AiModels/GenerativeModel/GenerativeModel.JsonMode.cs +++ b/src/GenerativeAI/AiModels/GenerativeModel/GenerativeModel.JsonMode.cs @@ -1,4 +1,5 @@ -using GenerativeAI.Types; +using System.Text.Json; +using GenerativeAI.Types; namespace GenerativeAI; @@ -14,7 +15,28 @@ public partial class GenerativeModel /// public bool UseJsonMode { get; set; } = false; - + private JsonSerializerOptions _jsonSerializerOptions = DefaultSerializerOptions.GenerateObjectJsonOptions; + + /// + /// Specifies the JSON serializer options to be used when generating objects as JSON outputs. + /// These options configure the behavior of JSON serialization and deserialization + /// for object generation in the context of JSON mode. + /// + /// + /// Customize these options to adjust serialization rules, such as property naming policies, + /// handling of null values, and supported data types. Adjustments can impact the handling + /// of responses and compatibility with consuming systems. + /// + public JsonSerializerOptions GenerateObjectJsonSerializerOptions + { + get => new JsonSerializerOptions(this._jsonSerializerOptions); + set + { + this._jsonSerializerOptions = value; + } + } + + #region Generate Object /// @@ -30,7 +52,7 @@ public virtual async Task GenerateContentAsync( CancellationToken cancellationToken = default) where T : class { request.GenerationConfig ??= this.Config; - request.UseJsonMode(); + request.UseJsonMode(GenerateObjectJsonSerializerOptions); return await GenerateContentAsync(request, cancellationToken).ConfigureAwait(false); } @@ -48,7 +70,7 @@ public virtual async Task GenerateContentAsync( CancellationToken cancellationToken = default) where T : class { var response = await GenerateContentAsync(request, cancellationToken).ConfigureAwait(false); - return response.ToObject(); + return response.ToObject(GenerateObjectJsonSerializerOptions); } /// diff --git a/src/GenerativeAI/AiModels/GenerativeModel/GenerativeModel.Tools.cs b/src/GenerativeAI/AiModels/GenerativeModel/GenerativeModel.Tools.cs index 85f24a20..22f26ff2 100644 --- a/src/GenerativeAI/AiModels/GenerativeModel/GenerativeModel.Tools.cs +++ b/src/GenerativeAI/AiModels/GenerativeModel/GenerativeModel.Tools.cs @@ -132,6 +132,11 @@ public void AddFunctionTool(IFunctionTool tool, ToolConfig? toolConfig = null,Fu } } + public void AddFunctionTool(GoogleFunctionTool tool, ToolConfig? toolConfig = null,FunctionCallingBehaviour? functionCallingBehaviour=null) + { + AddFunctionTool((IFunctionTool)tool, toolConfig, functionCallingBehaviour); + } + /// /// Disable Global Functions /// @@ -228,11 +233,12 @@ protected virtual async Task CallFunctionAsync( } name = "InvalidName"; - jsonResult = "{\"error\":\"Invalid function name or function doesn't exist.\"}"; + var node = JsonNode.Parse("{\"error\":\"Invalid function name or function doesn't exist.\"}"); + functionResponse = new FunctionResponse() { Name = name, - Response = jsonResult + Response = node }; } else diff --git a/src/GenerativeAI/AiModels/SemanticRetriever/SemanticRetrieverModel.cs b/src/GenerativeAI/AiModels/SemanticRetriever/SemanticRetrieverModel.cs index cdad3880..08c5a529 100644 --- a/src/GenerativeAI/AiModels/SemanticRetriever/SemanticRetrieverModel.cs +++ b/src/GenerativeAI/AiModels/SemanticRetriever/SemanticRetrieverModel.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.Metrics; using GenerativeAI.Clients; using GenerativeAI.Core; using GenerativeAI.Exceptions; diff --git a/src/GenerativeAI/Clients/FilesClient.cs b/src/GenerativeAI/Clients/FilesClient.cs index 7b25627a..cfcf0deb 100644 --- a/src/GenerativeAI/Clients/FilesClient.cs +++ b/src/GenerativeAI/Clients/FilesClient.cs @@ -53,7 +53,7 @@ public async Task UploadFileAsync(string filePath, Action? p if (progressCallback == null) progressCallback = d => { }; - var json = JsonSerializer.Serialize(request, SerializerOptions); + var json = JsonSerializer.Serialize(request, SerializerOptions.GetTypeInfo(request.GetType())); //Upload File using var file = File.OpenRead(filePath); var httpMessage = new HttpRequestMessage(HttpMethod.Post, url); @@ -105,7 +105,7 @@ public async Task UploadStreamAsync(Stream stream, string displayNam if (progressCallback == null) progressCallback = d => { }; - var json = JsonSerializer.Serialize(request, SerializerOptions); + var json = JsonSerializer.Serialize(request, SerializerOptions.GetTypeInfo(request.GetType())); //Upload File using var httpMessage = new HttpRequestMessage(HttpMethod.Post, url); diff --git a/src/GenerativeAI/Clients/RagEngine/FileManagementClient.cs b/src/GenerativeAI/Clients/RagEngine/FileManagementClient.cs index cb394b08..5a87f218 100644 --- a/src/GenerativeAI/Clients/RagEngine/FileManagementClient.cs +++ b/src/GenerativeAI/Clients/RagEngine/FileManagementClient.cs @@ -53,7 +53,7 @@ public FileManagementClient(IPlatformAdapter platform, HttpClient? httpClient = if (progressCallback == null) progressCallback = d => { }; - var json = JsonSerializer.Serialize(request, SerializerOptions); + var json = JsonSerializer.Serialize(request, SerializerOptions.GetTypeInfo(request.GetType())); //Upload File using var file = File.OpenRead(filePath); var httpMessage = new HttpRequestMessage(HttpMethod.Post, url); diff --git a/src/GenerativeAI/Clients/SemanticRetrieval/ChunkClient.cs b/src/GenerativeAI/Clients/SemanticRetrieval/ChunkClient.cs index d80d0dab..9e8e49d0 100644 --- a/src/GenerativeAI/Clients/SemanticRetrieval/ChunkClient.cs +++ b/src/GenerativeAI/Clients/SemanticRetrieval/ChunkClient.cs @@ -133,8 +133,13 @@ public async Task DeleteChunkAsync(string name, CancellationToken cancellationTo if(string.IsNullOrEmpty(request.Parent)) request.Parent = parent; } - var requestBody = new { requests }; - return await SendAsync(url, requestBody, HttpMethod.Post, cancellationToken).ConfigureAwait(false); + + var batchRequest = new BatchCreateChunksRequest() + { + Requests = requests + }; + + return await SendAsync(url, batchRequest, HttpMethod.Post, cancellationToken).ConfigureAwait(false); } /// diff --git a/src/GenerativeAI/Constants/DefaultSerializerOptions.cs b/src/GenerativeAI/Constants/DefaultSerializerOptions.cs index c4acb919..464c074d 100644 --- a/src/GenerativeAI/Constants/DefaultSerializerOptions.cs +++ b/src/GenerativeAI/Constants/DefaultSerializerOptions.cs @@ -1,5 +1,11 @@ -using System.Text.Json; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using GenerativeAI.Types; namespace GenerativeAI; @@ -18,12 +24,99 @@ public class DefaultSerializerOptions { public static JsonSerializerOptions Options { - get => new JsonSerializerOptions + get { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - Converters = { new JsonStringEnumConverter() }, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; + if (JsonSerializer.IsReflectionEnabledByDefault) + { + #pragma disable warning IL2026, IL3050 + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = TypesSerializerContext.Default, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode + }; + options.TypeInfoResolverChain.Add(new DefaultJsonTypeInfoResolver()); + #pragma restore warning IL2026, IL3050 + + return options; + } + else + { + return new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + //Converters = { new JsonStringEnumConverter() }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = TypesSerializerContext.Default, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode + }; + } + } } + + + public static JsonSerializerOptions GenerateObjectJsonOptions + { + get + { + JsonSerializerOptions options; + + if (JsonSerializer.IsReflectionEnabledByDefault) + { + #pragma disable warning IL2026, IL3050 + // Keep in sync with the JsonSourceGenerationOptions attribute on JsonContext below. + options = new(JsonSerializerDefaults.Web) + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + Converters = { new JsonStringEnumConverter() }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = true, + }; + #pragma restore warning IL2026, IL3050 + } + else + { + options = new(GenerateObjectJsonContext.Default.Options) + { + // Compile-time encoder setting not yet available + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + } + + options.MakeReadOnly(); + return options; + } + } +} + +// Keep in sync with CreateDefaultOptions above. +[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, + UseStringEnumConverter = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true)] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(IDictionary))] +[JsonSerializable(typeof(JsonDocument))] +[JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(JsonNode))] +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(int))] +[JsonSerializable(typeof(long))] +[JsonSerializable(typeof(float))] +[JsonSerializable(typeof(double))] +[JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(TimeSpan))] +[JsonSerializable(typeof(DateTimeOffset))] + +[EditorBrowsable(EditorBrowsableState.Never)] // Never use JsonContext directly, use DefaultOptions instead. +internal sealed partial class GenerateObjectJsonContext : JsonSerializerContext +{ + } \ No newline at end of file diff --git a/src/GenerativeAI/Core/ApiBase.cs b/src/GenerativeAI/Core/ApiBase.cs index bd9bfadc..9fb4374e 100644 --- a/src/GenerativeAI/Core/ApiBase.cs +++ b/src/GenerativeAI/Core/ApiBase.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using GenerativeAI.Exceptions; using GenerativeAI.Logging; using Microsoft.Extensions.Logging; @@ -82,7 +83,7 @@ protected async Task GetAsync(string url, CancellationToken cancellationTo _logger?.LogSuccessfulGetResponse(url, content); // Deserialize and return the response - return JsonSerializer.Deserialize(content) ?? + return JsonSerializer.Deserialize(content, (JsonTypeInfo) SerializerOptions.GetTypeInfo(typeof(T))) ?? throw new InvalidOperationException("Deserialized response is null."); } catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) @@ -117,7 +118,7 @@ protected async Task SendAsync(string url, TRequ _logger?.LogHttpRequest(method.Method, url, payload); // Serialize payload and create request - var jsonPayload = JsonSerializer.Serialize(payload, SerializerOptions); + var jsonPayload = JsonSerializer.Serialize(payload, SerializerOptions.GetTypeInfo(typeof(TRequest))); using var request = new HttpRequestMessage(method, url) { Content = new StringContent(jsonPayload, System.Text.Encoding.UTF8, "application/json") @@ -211,7 +212,7 @@ protected async Task CheckAndHandleErrors(HttpResponseMessage response, string u /// The deserialized object of type T, or null if deserialization fails. protected T? Deserialize(string json) { - return JsonSerializer.Deserialize(json, SerializerOptions); + return (T?) JsonSerializer.Deserialize(json, SerializerOptions.GetTypeInfo(typeof(T))); } /// @@ -360,7 +361,7 @@ protected async IAsyncEnumerable StreamAsync( { // Serialize the request payload into a MemoryStream using var ms = new MemoryStream(); - await JsonSerializer.SerializeAsync(ms, payload, SerializerOptions, cancellationToken).ConfigureAwait(false); + await JsonSerializer.SerializeAsync(ms, payload, SerializerOptions.GetTypeInfo(typeof(TRequest)), cancellationToken).ConfigureAwait(false); ms.Seek(0, SeekOrigin.Begin); // Prepare an HTTP request message @@ -389,9 +390,9 @@ protected async IAsyncEnumerable StreamAsync( using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); #endif - await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable( + await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable( stream, - SerializerOptions, + (JsonTypeInfo)SerializerOptions.GetTypeInfo(typeof(TResponse)), cancellationToken).ConfigureAwait(false) ) { diff --git a/src/GenerativeAI/Core/GoogleFunctionTool.cs b/src/GenerativeAI/Core/GoogleFunctionTool.cs new file mode 100644 index 00000000..5e96074b --- /dev/null +++ b/src/GenerativeAI/Core/GoogleFunctionTool.cs @@ -0,0 +1,18 @@ +using GenerativeAI.Types; + +namespace GenerativeAI.Core; + + +/// +public abstract class GoogleFunctionTool : IFunctionTool +{ + /// + public abstract Tool AsTool(); + + /// + public abstract Task CallAsync(FunctionCall functionCall, + CancellationToken cancellationToken = default); + + /// + public abstract bool IsContainFunction(string name); +} \ No newline at end of file diff --git a/src/GenerativeAI/Core/JsonBlock.cs b/src/GenerativeAI/Core/JsonBlock.cs index b7b5aa5b..0244d667 100644 --- a/src/GenerativeAI/Core/JsonBlock.cs +++ b/src/GenerativeAI/Core/JsonBlock.cs @@ -1,4 +1,6 @@ using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace GenerativeAI.Core; @@ -49,11 +51,24 @@ public JsonBlock() /// /// The target type to which the JSON data will be deserialized. Must be a class. /// An instance of type if deserialization is successful, or null if an error occurs. - public T? ToObject() where T : class + public T? ToObject(JsonSerializerOptions? options = null) where T : class { try { - return JsonSerializer.Deserialize(Json, DefaultSerializerOptions.Options); + if(options == null && !JsonSerializer.IsReflectionEnabledByDefault) + throw new InvalidOperationException("JsonSerializerOptions must be provided when reflection is disabled for AOT and Trimming."); + if (options == null) + options = DefaultSerializerOptions.GenerateObjectJsonOptions; + var newOptions = new JsonSerializerOptions(options) + { + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; + + var typeInfo = newOptions.GetTypeInfo(typeof(T)); + if (typeInfo == null) + throw new InvalidOperationException("Unable to get type information for type T."); + return JsonSerializer.Deserialize(Json, typeInfo) as T; + } catch (Exception ex) { diff --git a/src/GenerativeAI/Extensions/ContentExtensions.cs b/src/GenerativeAI/Extensions/ContentExtensions.cs index 728b6317..df758799 100644 --- a/src/GenerativeAI/Extensions/ContentExtensions.cs +++ b/src/GenerativeAI/Extensions/ContentExtensions.cs @@ -184,7 +184,7 @@ public static List ExtractJsonBlocks(this Content content) /// The target type to which the JSON content will be deserialized. Must be a class. /// The object containing JSON data to be converted. /// An instance of type if conversion succeeds, or null if no valid JSON data is found or deserialization fails. - public static T? ToObject(this Content content) where T : class + public static T? ToObject(this Content content, JsonSerializerOptions? options = null) where T : class { var jsonBlocks = ExtractJsonBlocks(content); @@ -192,10 +192,9 @@ public static List ExtractJsonBlocks(this Content content) { foreach (var block in jsonBlocks) { - return block.ToObject(); + return block.ToObject(options); } } - return null; } } \ No newline at end of file diff --git a/src/GenerativeAI/Extensions/GenerateContentRequestExtensions.cs b/src/GenerativeAI/Extensions/GenerateContentRequestExtensions.cs index f20d7bed..c6d366ae 100644 --- a/src/GenerativeAI/Extensions/GenerateContentRequestExtensions.cs +++ b/src/GenerativeAI/Extensions/GenerateContentRequestExtensions.cs @@ -1,4 +1,5 @@ -using GenerativeAI.Core; +using System.Text.Json; +using GenerativeAI.Core; using GenerativeAI.Types; namespace GenerativeAI; @@ -30,11 +31,12 @@ public static void AddTool( /// The type that defines the response schema for the JSON. /// The on which JSON mode will be applied. /// Some of the complex data types are not supported such as Dictionary. So make sure to avoid these. - public static void UseJsonMode(this GenerateContentRequest request) where T : class + public static void UseJsonMode(this GenerateContentRequest request, JsonSerializerOptions? options = null) where T : class { if(request.GenerationConfig == null) request.GenerationConfig = new GenerationConfig(); request.GenerationConfig.ResponseMimeType = "application/json"; - request.GenerationConfig.ResponseSchema = typeof(T); + request.GenerationConfig.ResponseSchema = + GoogleSchemaHelper.ConvertToSchema(options); //GoogleSchemaHelper.ConvertToSchema(typeof(T)); } } \ No newline at end of file diff --git a/src/GenerativeAI/Extensions/GenerateContentResponseExtensions.cs b/src/GenerativeAI/Extensions/GenerateContentResponseExtensions.cs index d797a1a4..3cf59a05 100644 --- a/src/GenerativeAI/Extensions/GenerateContentResponseExtensions.cs +++ b/src/GenerativeAI/Extensions/GenerateContentResponseExtensions.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.Json; using GenerativeAI.Core; using GenerativeAI.Types; @@ -103,12 +104,12 @@ public static List ExtractJsonBlocks(this GenerateContentResponse res /// /// The GenerateContentResponse containing potential JSON blocks. /// A list of JsonBlock objects extracted from the response. Returns an empty list if no JSON blocks are found. - public static T? ToObject(this GenerateContentResponse response) where T : class + public static T? ToObject(this GenerateContentResponse response, JsonSerializerOptions? options = null) where T : class { var blocks = ExtractJsonBlocks(response); foreach (var block in blocks) { - T? obj = block.ToObject(); + T? obj = block.ToObject(options); if (obj != null) return obj; } @@ -122,13 +123,13 @@ public static List ExtractJsonBlocks(this GenerateContentResponse res /// The GenerateContentResponse containing JSON blocks to convert. /// The type to which the JSON blocks are converted. /// A list of objects of type T. Returns an empty list if no JSON blocks are found or successfully converted. - public static List ToObjects(this GenerateContentResponse response) where T : class + public static List ToObjects(this GenerateContentResponse response, JsonSerializerOptions? options = null) where T : class { var blocks = ExtractJsonBlocks(response); List objects = new List(); foreach (var block in blocks) { - T? obj = block.ToObject(); + T? obj = block.ToObject(options); if (obj != null) objects.Add(obj); } diff --git a/src/GenerativeAI/Extensions/JsonElementExtensions.cs b/src/GenerativeAI/Extensions/JsonElementExtensions.cs new file mode 100644 index 00000000..433a574a --- /dev/null +++ b/src/GenerativeAI/Extensions/JsonElementExtensions.cs @@ -0,0 +1,23 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace GenerativeAI; + +public static class JsonElementExtensions +{ + /// + /// Converts a to a . + /// + /// The element. + /// An equivalent node. + /// + /// This provides a single point of conversion as one is not provided by .Net. + /// See https://github.com/dotnet/runtime/issues/70427 for more information. + /// + public static JsonNode? AsNode(this JsonElement element) => element.ValueKind switch + { + JsonValueKind.Array => JsonArray.Create(element), + JsonValueKind.Object => JsonObject.Create(element), + _ => JsonValue.Create(element) + }; +} \ No newline at end of file diff --git a/src/GenerativeAI/Extensions/ObjectExtensions.cs b/src/GenerativeAI/Extensions/ObjectExtensions.cs new file mode 100644 index 00000000..87ae5d69 --- /dev/null +++ b/src/GenerativeAI/Extensions/ObjectExtensions.cs @@ -0,0 +1,24 @@ +using System.Text.Json; +using GenerativeAI.Types; + +namespace GenerativeAI; + +/// +/// Provides extension methods for working with objects in the GenerativeAI library. +/// Includes utilities for converting objects into schema representations. +/// +public static class ObjectExtensions +{ + /// + /// Converts an object to a representation. + /// This method evaluates the properties and structure of the provided object + /// and generates a corresponding schema representation. + /// + /// The object to be converted into a schema representation. + /// Optional JSON serializer options for customizing the schema generation. + /// A instance that represents the structure of the provided object. + public static Schema ToSchema(this object obj, JsonSerializerOptions? options = null) + { + return Schema.FromObject(obj, options); + } +} \ No newline at end of file diff --git a/src/GenerativeAI/Extensions/StringExtensions.cs b/src/GenerativeAI/Extensions/StringExtensions.cs index 522f1cab..9c252e35 100644 --- a/src/GenerativeAI/Extensions/StringExtensions.cs +++ b/src/GenerativeAI/Extensions/StringExtensions.cs @@ -318,4 +318,29 @@ public static string MaskApiKey(this string url) return uriBuilder.ToString(); } + + + /// + /// Converts a string into camel case format. + /// + /// The input string to be converted. + /// + /// A camel case representation of the input string, or an empty string if the input is null or whitespace. + /// + public static string ToCamelCase(this string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return string.Empty; + } + + var words = input.Split(new[] { ' ', '_', '-' }, StringSplitOptions.RemoveEmptyEntries); + + for (int i = 1; i < words.Length; i++) + { + words[i] = char.ToUpperInvariant(words[i][0]) + words[i].Substring(1); + } + + return char.ToLowerInvariant(words[0][0]) + words[0].Substring(1) + string.Join(string.Empty, words.Skip(1)); + } } \ No newline at end of file diff --git a/src/GenerativeAI/GenerativeAI.csproj b/src/GenerativeAI/GenerativeAI.csproj index 4c2a07e1..e67aa6de 100644 --- a/src/GenerativeAI/GenerativeAI.csproj +++ b/src/GenerativeAI/GenerativeAI.csproj @@ -19,6 +19,7 @@ 2.3.1 True True + true @@ -29,12 +30,10 @@ - - diff --git a/src/GenerativeAI/Types/ContentGeneration/Common/Schema.cs b/src/GenerativeAI/Types/ContentGeneration/Common/Schema.cs index e3b8a003..34f26346 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Common/Schema.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Common/Schema.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; namespace GenerativeAI.Types; @@ -85,6 +86,18 @@ public class Schema /// [JsonPropertyName("items")] public Schema? Items { get; set; } + + /// + /// Creates a object representing the structure of the specified object type. + /// This method evaluates the properties and structure of the provided object + /// and generates a corresponding schema representation. + /// + /// The object from which to generate the schema. + /// Optional JSON serializer options used for customization during schema generation. + /// A instance that represents the structure of the provided object. + public static Schema FromObject(object value, JsonSerializerOptions? options = null) => + GoogleSchemaHelper.ConvertToSchema(value.GetType(), options); + } /// diff --git a/src/GenerativeAI/Types/ContentGeneration/Config/GenerationConfig.cs b/src/GenerativeAI/Types/ContentGeneration/Config/GenerationConfig.cs index 489625e2..9da08481 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Config/GenerationConfig.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Config/GenerationConfig.cs @@ -38,8 +38,8 @@ public class GenerationConfig /// for more details. /// [JsonPropertyName("responseSchema")] - [JsonConverter(typeof(ObjectToJsonSchemaConverter))] - public object? ResponseSchema { get; set; } + //[JsonConverter(typeof(ObjectToJsonSchemaConverter))] + public Schema? ResponseSchema { get; set; } /// /// Optional. The requested modalities of the response. Represents the set of modalities diff --git a/src/GenerativeAI/Types/ContentGeneration/Config/SchemaType.cs b/src/GenerativeAI/Types/ContentGeneration/Config/SchemaType.cs index 50f5cb80..445efc2e 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Config/SchemaType.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Config/SchemaType.cs @@ -7,7 +7,7 @@ namespace GenerativeAI.Types; /// https://spec.openapis.org/oas/v3.0.3#data-types. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum SchemaType { /// diff --git a/src/GenerativeAI/Types/ContentGeneration/JsonConverters/GoogleSchemaHelper.cs b/src/GenerativeAI/Types/ContentGeneration/JsonConverters/GoogleSchemaHelper.cs index be29fcf4..6c27c7a3 100644 --- a/src/GenerativeAI/Types/ContentGeneration/JsonConverters/GoogleSchemaHelper.cs +++ b/src/GenerativeAI/Types/ContentGeneration/JsonConverters/GoogleSchemaHelper.cs @@ -1,7 +1,14 @@ -using Json.Schema; -using System.Text.Json.Nodes; +using System.Text.Json.Nodes; using System.Text.Json; +using System.Text.Json.Serialization; + +#if NET8_0_OR_GREATER +using System.Text.Json.Schema; +#else using Json.More; +using Json.Schema; +using Json.Schema.Generation; +#endif namespace GenerativeAI.Types; @@ -18,12 +25,13 @@ public static Schema ConvertToCompatibleSchemaSubset(JsonDocument constructedSch { #if NET6_0_OR_GREATER var node = constructedSchema.RootElement.AsNode(); + ConvertNullableProperties(node); var x1 = node; - var x2 = JsonSerializer.Serialize(x1); - var schema = JsonSerializer.Deserialize(x2,SchemaSourceGenerationContext.Default.Schema); + var x2 = x1.ToJsonString(); + var schema = JsonSerializer.Deserialize(x2, SchemaSourceGenerationContext.Default.Schema); return schema; #else var schema = JsonSerializer.Deserialize(constructedSchema.RootElement.GetRawText()); @@ -31,6 +39,29 @@ public static Schema ConvertToCompatibleSchemaSubset(JsonDocument constructedSch #endif } + /// + /// Converts a JSON document that contains valid json schema as e.g. + /// generated by Microsoft.Extensions.AI.AIJsonUtilities.CreateJsonSchema or JsonSchema.Net's + /// to a subset that is compatible with Google's APIs. + /// + /// Generated, valid json schema. + /// Subset of the given json schema in a google-comaptible format. + public static Schema ConvertToCompatibleSchemaSubset(JsonNode node) + { +#if NET6_0_OR_GREATER + ConvertNullableProperties(node); + + + var x1 = node; + var x2 = x1.ToJsonString(); + var schema = JsonSerializer.Deserialize(x2, SchemaSourceGenerationContext.Default.Schema); + return schema; +#else + var schema = JsonSerializer.Deserialize(node.ToJsonString()); + return schema; +#endif + } + private static void ConvertNullableProperties(JsonNode? node) { // If the node is an object, look for a "type" property or nested definitions @@ -49,12 +80,14 @@ private static void ConvertNullableProperties(JsonNode? node) } else { - throw new InvalidOperationException($"Google's API for strucutured output requires every property to have one defined type, not multiple options. Path: {obj.GetPath()} Schema: {obj.ToJsonString()}"); + throw new InvalidOperationException( + $"Google'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) + else if (array.Count > 2) { - throw new InvalidOperationException($"Google's API for strucutured output requires every property to have one defined type, not multiple options. Path: {obj.GetPath()} Schema: {obj.ToJsonString()}"); + throw new InvalidOperationException( + $"Google's API for strucutured output requires every property to have one defined type, not multiple options. Path: {obj.GetPath()} Schema: {obj.ToJsonString()}"); } } @@ -68,13 +101,14 @@ private static void ConvertNullableProperties(JsonNode? node) } } - if (obj.TryGetPropertyValue("type", out var newTypeValue) - && newTypeValue is JsonNode - && newTypeValue.GetValueKind() == JsonValueKind.String + if (obj.TryGetPropertyValue("type", out var newTypeValue) + && newTypeValue is JsonNode + && newTypeValue.GetValueKind() == JsonValueKind.String && "object".Equals(newTypeValue.GetValue(), StringComparison.OrdinalIgnoreCase) && propertiesNode is not JsonObject) { - throw new InvalidOperationException($"Google's API for strucutured output requires every object to have predefined properties. Notably, it does not support dictionaries. Path: {obj.GetPath()} Schema: {obj.ToJsonString()}"); + throw new InvalidOperationException( + $"Google'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" @@ -93,4 +127,82 @@ private static void ConvertNullableProperties(JsonNode? node) } } } -} + + public static Schema ConvertToSchema(JsonSerializerOptions? jsonOptions = null) + { +#if NET8_0_OR_GREATER + if (jsonOptions == null && !JsonSerializer.IsReflectionEnabledByDefault) + { + throw new InvalidOperationException("Please provide a JsonSerializerOptions instance to use in AOT mode."); + } + + if (jsonOptions == null) + jsonOptions = DefaultSerializerOptions.GenerateObjectJsonOptions; + + var newJsonOptions = new JsonSerializerOptions(jsonOptions) + { + NumberHandling = JsonNumberHandling.Strict + }; + + var typeInfo = newJsonOptions.GetTypeInfo(typeof(T)); + + return ConvertToCompatibleSchemaSubset(typeInfo.GetJsonSchemaAsNode()); + +#else + return ConvertToSchema(typeof(T), jsonOptions); +#endif + } + + public static Schema ConvertToSchema(Type type, JsonSerializerOptions? jsonOptions = null, + Dictionary? descriptionTable = null) + { +#if NET8_0_OR_GREATER + if (jsonOptions == null && !JsonSerializer.IsReflectionEnabledByDefault) + { + throw new InvalidOperationException("Please provide a JsonSerializerOptions instance to use in AOT mode."); + } + + if (jsonOptions == null) + jsonOptions = DefaultSerializerOptions.GenerateObjectJsonOptions; + + var newJsonOptions = new JsonSerializerOptions(jsonOptions) + { + NumberHandling = JsonNumberHandling.Strict + }; + + var typeInfo = newJsonOptions.GetTypeInfo(type); + + var dics = descriptionTable ?? new Dictionary(); + var schema = typeInfo.GetJsonSchemaAsNode(exporterOptions: new JsonSchemaExporterOptions() { TransformSchemaNode + = (a, b) => + { + if (a.TypeInfo.Type.IsEnum) + { + b["type"] = "string"; + } + + if (a.PropertyInfo == null) + return b; + var propName = a.PropertyInfo.Name.ToCamelCase(); + if (dics.ContainsKey(propName)) + { + b["description"] = dics[propName]; + } + return b; + }}); + return ConvertToCompatibleSchemaSubset(schema); + +#else + var generatorConfig = new SchemaGeneratorConfiguration(); + var builder = new JsonSchemaBuilder(); + + var constructedSchema = builder + .FromType(type, generatorConfig) + .Build().ToJsonDocument(); + + //Work around to avoid type as array + var schema = GoogleSchemaHelper.ConvertToCompatibleSchemaSubset(constructedSchema); + return schema; +#endif + } +} \ No newline at end of file diff --git a/src/GenerativeAI/Types/ContentGeneration/JsonConverters/ObjectToSchemaConverter.cs b/src/GenerativeAI/Types/ContentGeneration/JsonConverters/ObjectToSchemaConverter.cs index 08dda620..19a811d3 100644 --- a/src/GenerativeAI/Types/ContentGeneration/JsonConverters/ObjectToSchemaConverter.cs +++ b/src/GenerativeAI/Types/ContentGeneration/JsonConverters/ObjectToSchemaConverter.cs @@ -1,100 +1,101 @@ -using Json.More; -using Json.Schema; -using Json.Schema.Generation; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace GenerativeAI.Types; - -/// -/// This converter interprets objects and writes them as JSON schema definitions. -/// -sealed class ObjectToJsonSchemaConverter : JsonConverter -{ - /// - /// Reads an incoming JSON value and deserializes it as a dynamic object. - /// - /// The Utf8JsonReader instance to read from. - /// The expected type of the object being read. - /// Options for customizing the deserialization process. - /// The deserialized dynamic object or null if deserialization fails. - public override object? Read( - ref Utf8JsonReader jsonReader, - Type targetType, - JsonSerializerOptions jsonOptions) - { - return JsonSerializer.Deserialize(jsonReader.GetString()!, jsonOptions); - } - - /// - /// Writes an object as JSON output. For known structure types such as JsonDocument, - /// JsonElement, JsonNode, or Schema, it writes the JSON representation directly. - /// For other object types, it generates a JSON schema representation and writes it. - /// - /// The Utf8JsonWriter to which the value will be written. - /// The object to be serialized and written. - /// Options for customizing the serialization process. - public override void Write( - Utf8JsonWriter jsonWriter, - object valueToWrite, - JsonSerializerOptions jsonOptions) - { - var actualType = valueToWrite is Type type ? type : valueToWrite.GetType(); - - if (actualType == typeof(JsonDocument) || - actualType == typeof(JsonElement) || - actualType == typeof(JsonNode) || - actualType == typeof(Schema)) - { - var clonedOptions = new JsonSerializerOptions(jsonOptions); - clonedOptions.Converters.Remove(this); - JsonSerializer.Serialize(jsonWriter, valueToWrite, actualType, clonedOptions); - } - else - { - var naming = jsonOptions.PropertyNamingPolicy ?? JsonNamingPolicy.CamelCase; - -#if NET_6_0_OR_GREATER - var propertyResolver = PropertyNameResolvers.CamelCase; - if(naming == JsonNamingPolicy.CamelCase) - propertyResolver = PropertyNameResolvers.CamelCase; - else if (naming == JsonNamingPolicy.KebabCaseLower) - { - propertyResolver = PropertyNameResolvers.KebabCase; - } - else if (naming == JsonNamingPolicy.KebabCaseUpper) - { - propertyResolver = PropertyNameResolvers.UpperKebabCase; - } - else if (naming == JsonNamingPolicy.SnakeCaseLower) - { - propertyResolver = PropertyNameResolvers.LowerSnakeCase; - } - else if (naming == JsonNamingPolicy.SnakeCaseUpper) - { - propertyResolver = PropertyNameResolvers.UpperSnakeCase; - } -var generatorConfig = new SchemaGeneratorConfiguration - { -PropertyNameResolver = propertyResolver, - }; - -#else - var generatorConfig = new SchemaGeneratorConfiguration(); -#endif - - var builder = new JsonSchemaBuilder(); - - var constructedSchema = builder - .FromType(actualType, generatorConfig) - .Build().ToJsonDocument(); - - - //Work around to avoid type as array - var schema = GoogleSchemaHelper.ConvertToCompatibleSchemaSubset(constructedSchema); - - JsonSerializer.Serialize(jsonWriter, schema, schema.GetType(), jsonOptions); - } - } -} \ No newline at end of file +// using Json.More; +// using Json.Schema; +// using Json.Schema.Generation; +// using System.Text.Json; +// using System.Text.Json.Nodes; +// using System.Text.Json.Serialization; +// +// namespace GenerativeAI.Types; +// +// /// +// /// This converter interprets objects and writes them as JSON schema definitions. +// /// +// sealed class ObjectToJsonSchemaConverter : JsonConverter +// { +// /// +// /// Reads an incoming JSON value and deserializes it as a dynamic object. +// /// +// /// The Utf8JsonReader instance to read from. +// /// The expected type of the object being read. +// /// Options for customizing the deserialization process. +// /// The deserialized dynamic object or null if deserialization fails. +// public override Schema? Read( +// ref Utf8JsonReader jsonReader, +// Type targetType, +// JsonSerializerOptions jsonOptions) +// { +// return JsonSerializer.Deserialize(jsonReader.GetString()!, jsonOptions); +// } +// +// /// +// /// Writes an object as JSON output. For known structure types such as JsonDocument, +// /// JsonElement, JsonNode, or Schema, it writes the JSON representation directly. +// /// For other object types, it generates a JSON schema representation and writes it. +// /// +// /// The Utf8JsonWriter to which the value will be written. +// /// The object to be serialized and written. +// /// Options for customizing the serialization process. +// public override void Write( +// Utf8JsonWriter jsonWriter, +// Schema? valueToWrite, +// JsonSerializerOptions jsonOptions) +// { +// //var actualType = valueToWrite is Type type ? type : valueToWrite.GetType(); +// +// var actualType = valueToWrite.GetType(); +// if (actualType == typeof(JsonDocument) || +// actualType == typeof(JsonElement) || +// actualType == typeof(JsonNode) || +// actualType == typeof(Schema)) +// { +// var clonedOptions = new JsonSerializerOptions(jsonOptions); +// clonedOptions.Converters.Remove(this); +// JsonSerializer.Serialize(jsonWriter, valueToWrite, actualType, clonedOptions); +// } +// else +// { +// var naming = jsonOptions.PropertyNamingPolicy ?? JsonNamingPolicy.CamelCase; +// +// #if NET_6_0_OR_GREATER +// var propertyResolver = PropertyNameResolvers.CamelCase; +// if(naming == JsonNamingPolicy.CamelCase) +// propertyResolver = PropertyNameResolvers.CamelCase; +// else if (naming == JsonNamingPolicy.KebabCaseLower) +// { +// propertyResolver = PropertyNameResolvers.KebabCase; +// } +// else if (naming == JsonNamingPolicy.KebabCaseUpper) +// { +// propertyResolver = PropertyNameResolvers.UpperKebabCase; +// } +// else if (naming == JsonNamingPolicy.SnakeCaseLower) +// { +// propertyResolver = PropertyNameResolvers.LowerSnakeCase; +// } +// else if (naming == JsonNamingPolicy.SnakeCaseUpper) +// { +// propertyResolver = PropertyNameResolvers.UpperSnakeCase; +// } +// var generatorConfig = new SchemaGeneratorConfiguration +// { +// PropertyNameResolver = propertyResolver, +// }; +// +// #else +// var generatorConfig = new SchemaGeneratorConfiguration(); +// #endif +// +// var builder = new JsonSchemaBuilder(); +// +// var constructedSchema = builder +// .FromType(actualType, generatorConfig) +// .Build().ToJsonDocument(); +// +// +// //Work around to avoid type as array +// var schema = GoogleSchemaHelper.ConvertToCompatibleSchemaSubset(constructedSchema); +// +// JsonSerializer.Serialize(jsonWriter, schema, schema.GetType(), jsonOptions); +// } +// } +// } \ No newline at end of file diff --git a/src/GenerativeAI/Types/ContentGeneration/Outputs/BlockReason.cs b/src/GenerativeAI/Types/ContentGeneration/Outputs/BlockReason.cs index 8d1c80f5..3dd60f85 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Outputs/BlockReason.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Outputs/BlockReason.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// Specifies the reason why the prompt was blocked. /// /// BlockReason Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum BlockReason { /// diff --git a/src/GenerativeAI/Types/ContentGeneration/Outputs/FinishReason.cs b/src/GenerativeAI/Types/ContentGeneration/Outputs/FinishReason.cs index f1c6592e..db084d3a 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Outputs/FinishReason.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Outputs/FinishReason.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// Defines the reason why the model stopped generating tokens. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum FinishReason { /// diff --git a/src/GenerativeAI/Types/ContentGeneration/Outputs/Modality.cs b/src/GenerativeAI/Types/ContentGeneration/Outputs/Modality.cs index 575bce39..0ab5b785 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Outputs/Modality.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Outputs/Modality.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// Content Part modality. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum Modality { /// diff --git a/src/GenerativeAI/Types/ContentGeneration/Safety/HarmBlockThreshold.cs b/src/GenerativeAI/Types/ContentGeneration/Safety/HarmBlockThreshold.cs index 310acdb3..131fbbad 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Safety/HarmBlockThreshold.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Safety/HarmBlockThreshold.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// Block at and beyond a specified harm probability. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum HarmBlockThreshold { /// diff --git a/src/GenerativeAI/Types/ContentGeneration/Safety/HarmCategory.cs b/src/GenerativeAI/Types/ContentGeneration/Safety/HarmCategory.cs index 80f5995f..33f9d6c0 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Safety/HarmCategory.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Safety/HarmCategory.cs @@ -7,7 +7,7 @@ namespace GenerativeAI.Types; /// These categories cover various kinds of harms that developers may wish to adjust. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum HarmCategory { /// diff --git a/src/GenerativeAI/Types/ContentGeneration/Safety/HarmProbability.cs b/src/GenerativeAI/Types/ContentGeneration/Safety/HarmProbability.cs index f33310af..29575f91 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Safety/HarmProbability.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Safety/HarmProbability.cs @@ -8,7 +8,7 @@ namespace GenerativeAI.Types; /// This does not indicate the severity of harm for a piece of content. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum HarmProbability { /// diff --git a/src/GenerativeAI/Types/ContentGeneration/Tools/CodeExecution/Language.cs b/src/GenerativeAI/Types/ContentGeneration/Tools/CodeExecution/Language.cs index 5548f149..9a169a78 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Tools/CodeExecution/Language.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Tools/CodeExecution/Language.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// Supported programming languages for the generated code. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum Language { /// diff --git a/src/GenerativeAI/Types/ContentGeneration/Tools/CodeExecution/Outcome.cs b/src/GenerativeAI/Types/ContentGeneration/Tools/CodeExecution/Outcome.cs index 50e0b039..802e59c4 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Tools/CodeExecution/Outcome.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Tools/CodeExecution/Outcome.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// Enumeration of possible outcomes of the code execution. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum Outcome { /// diff --git a/src/GenerativeAI/Types/ContentGeneration/Tools/FunctionCalling/FunctionCall.cs b/src/GenerativeAI/Types/ContentGeneration/Tools/FunctionCalling/FunctionCall.cs index 1065c66d..3b6adf11 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Tools/FunctionCalling/FunctionCall.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Tools/FunctionCalling/FunctionCall.cs @@ -1,4 +1,6 @@ -using System.Text.Json.Serialization; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace GenerativeAI.Types; @@ -27,5 +29,5 @@ public class FunctionCall /// Optional. The function parameters and values in JSON object format. /// [JsonPropertyName("args")] - public object? Args { get; set; } + public JsonNode? Args { get; set; } } \ No newline at end of file diff --git a/src/GenerativeAI/Types/ContentGeneration/Tools/FunctionCalling/FunctionCallingMode.cs b/src/GenerativeAI/Types/ContentGeneration/Tools/FunctionCalling/FunctionCallingMode.cs index 10900585..ba142fdd 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Tools/FunctionCalling/FunctionCallingMode.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Tools/FunctionCalling/FunctionCallingMode.cs @@ -7,7 +7,7 @@ namespace GenerativeAI.Types; /// Defines the execution behavior for function calling by defining the execution mode. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum FunctionCallingMode // Renamed to FunctionCallingMode { /// diff --git a/src/GenerativeAI/Types/ContentGeneration/Tools/FunctionCalling/FunctionResponse.cs b/src/GenerativeAI/Types/ContentGeneration/Tools/FunctionCalling/FunctionResponse.cs index ff200f52..18b8fcb2 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Tools/FunctionCalling/FunctionResponse.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Tools/FunctionCalling/FunctionResponse.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Nodes; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace GenerativeAI.Types; @@ -30,5 +31,5 @@ public class FunctionResponse /// Required. The function response in JSON object format. /// [JsonPropertyName("response")] - public dynamic? Response { get; set; } + public JsonNode? Response { get; set; } } \ No newline at end of file diff --git a/src/GenerativeAI/Types/ContentGeneration/Tools/GoogleSearchRetrieval/DynamicRetrievalMode.cs b/src/GenerativeAI/Types/ContentGeneration/Tools/GoogleSearchRetrieval/DynamicRetrievalMode.cs index 9dd88527..21c98d20 100644 --- a/src/GenerativeAI/Types/ContentGeneration/Tools/GoogleSearchRetrieval/DynamicRetrievalMode.cs +++ b/src/GenerativeAI/Types/ContentGeneration/Tools/GoogleSearchRetrieval/DynamicRetrievalMode.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// The mode of the predictor to be used in dynamic retrieval. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum DynamicRetrievalMode // Renamed to DynamicRetrievalMode { /// diff --git a/src/GenerativeAI/Types/Embeddings/TaskType.cs b/src/GenerativeAI/Types/Embeddings/TaskType.cs index 36883860..94d474b7 100644 --- a/src/GenerativeAI/Types/Embeddings/TaskType.cs +++ b/src/GenerativeAI/Types/Embeddings/TaskType.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// Specifies the type of task for which the embedding will be used. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum TaskType { /// diff --git a/src/GenerativeAI/Types/Files/Source.cs b/src/GenerativeAI/Types/Files/Source.cs index fbcb7a22..bb273284 100644 --- a/src/GenerativeAI/Types/Files/Source.cs +++ b/src/GenerativeAI/Types/Files/Source.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// Source of the File. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum Source { /// diff --git a/src/GenerativeAI/Types/Files/State.cs b/src/GenerativeAI/Types/Files/State.cs index 8f66d38e..5ecc89e4 100644 --- a/src/GenerativeAI/Types/Files/State.cs +++ b/src/GenerativeAI/Types/Files/State.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// States for the lifecycle of a File. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum FileState { /// diff --git a/src/GenerativeAI/Types/Files/Status.cs b/src/GenerativeAI/Types/Files/Status.cs index 8a2b8913..80b2e0d2 100644 --- a/src/GenerativeAI/Types/Files/Status.cs +++ b/src/GenerativeAI/Types/Files/Status.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; namespace GenerativeAI.Types; @@ -35,5 +36,5 @@ public class Status /// Example: { "id": 1234, "@type": "types.example.com/standard/id" }. /// [JsonPropertyName("details")] - public List>? Details { get; set; } + public List>? Details { get; set; } } \ No newline at end of file diff --git a/src/GenerativeAI/Types/Imagen/PersonGeneration.cs b/src/GenerativeAI/Types/Imagen/PersonGeneration.cs index f5a6accc..0c30566e 100644 --- a/src/GenerativeAI/Types/Imagen/PersonGeneration.cs +++ b/src/GenerativeAI/Types/Imagen/PersonGeneration.cs @@ -4,6 +4,7 @@ namespace GenerativeAI.Types; /// Represents the allowed generation of people by the model. /// /// See Official API Documentation +[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] public enum PersonGeneration { /// diff --git a/src/GenerativeAI/Types/Imagen/SafetySetting.cs b/src/GenerativeAI/Types/Imagen/SafetySetting.cs index c9b61ecf..52fce338 100644 --- a/src/GenerativeAI/Types/Imagen/SafetySetting.cs +++ b/src/GenerativeAI/Types/Imagen/SafetySetting.cs @@ -1,9 +1,12 @@ +using System.Text.Json.Serialization; + namespace GenerativeAI.Types; /// /// Represents the safety filter level. /// /// See Official API Documentation +[JsonConverter(typeof(JsonStringEnumConverter))] public enum ImageSafetySetting { /// diff --git a/src/GenerativeAI/Types/RagEngine/CorpusStatus.cs b/src/GenerativeAI/Types/RagEngine/CorpusStatus.cs index 2c0837b9..91927cb1 100644 --- a/src/GenerativeAI/Types/RagEngine/CorpusStatus.cs +++ b/src/GenerativeAI/Types/RagEngine/CorpusStatus.cs @@ -17,6 +17,6 @@ public class CorpusStatus /// Output only. RagCorpus life state. /// [JsonPropertyName("state")] - [JsonConverter(typeof(JsonStringEnumConverter))] + public CorpusStatusState? State { get; set; } } \ No newline at end of file diff --git a/src/GenerativeAI/Types/RagEngine/CorpusStatusState.cs b/src/GenerativeAI/Types/RagEngine/CorpusStatusState.cs index f5368938..2d49e1f0 100644 --- a/src/GenerativeAI/Types/RagEngine/CorpusStatusState.cs +++ b/src/GenerativeAI/Types/RagEngine/CorpusStatusState.cs @@ -1,7 +1,9 @@ using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace GenerativeAI.Types.RagEngine; +[JsonConverter(typeof(JsonStringEnumConverter))] public enum CorpusStatusState { [EnumMember(Value = @"UNKNOWN")] diff --git a/src/GenerativeAI/Types/RagEngine/FileStatus.cs b/src/GenerativeAI/Types/RagEngine/FileStatus.cs index 05422404..f0b0e343 100644 --- a/src/GenerativeAI/Types/RagEngine/FileStatus.cs +++ b/src/GenerativeAI/Types/RagEngine/FileStatus.cs @@ -17,6 +17,6 @@ public class FileStatus /// Output only. RagFile state. /// [JsonPropertyName("state")] - [JsonConverter(typeof(JsonStringEnumConverter))] + public FileStatusState? State { get; set; } } \ No newline at end of file diff --git a/src/GenerativeAI/Types/RagEngine/FileStatusState.cs b/src/GenerativeAI/Types/RagEngine/FileStatusState.cs index 69ea283f..8e2dfd43 100644 --- a/src/GenerativeAI/Types/RagEngine/FileStatusState.cs +++ b/src/GenerativeAI/Types/RagEngine/FileStatusState.cs @@ -1,7 +1,9 @@ using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace GenerativeAI.Types.RagEngine; +[JsonConverter(typeof(JsonStringEnumConverter))] public enum FileStatusState { diff --git a/src/GenerativeAI/Types/RagEngine/GoogleDriveSourceResourceId.cs b/src/GenerativeAI/Types/RagEngine/GoogleDriveSourceResourceId.cs index 975b49bb..ad2cb0fe 100644 --- a/src/GenerativeAI/Types/RagEngine/GoogleDriveSourceResourceId.cs +++ b/src/GenerativeAI/Types/RagEngine/GoogleDriveSourceResourceId.cs @@ -17,6 +17,6 @@ public class GoogleDriveSourceResourceId /// Required. The type of the Google Drive resource. /// [JsonPropertyName("resourceType")] - [JsonConverter(typeof(JsonStringEnumConverter))] + public GoogleDriveSourceResourceIdResourceType? ResourceType { get; set; } } \ No newline at end of file diff --git a/src/GenerativeAI/Types/RagEngine/GoogleDriveSourceResourceIdResourceType.cs b/src/GenerativeAI/Types/RagEngine/GoogleDriveSourceResourceIdResourceType.cs index e9d38513..5eecda72 100644 --- a/src/GenerativeAI/Types/RagEngine/GoogleDriveSourceResourceIdResourceType.cs +++ b/src/GenerativeAI/Types/RagEngine/GoogleDriveSourceResourceIdResourceType.cs @@ -1,7 +1,9 @@ using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace GenerativeAI.Types.RagEngine; +[JsonConverter(typeof(JsonStringEnumConverter))] public enum GoogleDriveSourceResourceIdResourceType { diff --git a/src/GenerativeAI/Types/RagEngine/GoogleRpcStatus.cs b/src/GenerativeAI/Types/RagEngine/GoogleRpcStatus.cs index dbe66df5..d57c9a36 100644 --- a/src/GenerativeAI/Types/RagEngine/GoogleRpcStatus.cs +++ b/src/GenerativeAI/Types/RagEngine/GoogleRpcStatus.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; namespace GenerativeAI.Types.RagEngine; @@ -17,7 +18,7 @@ public class GoogleRpcStatus /// A list of messages that carry the error details. There is a common set of message types for APIs to use. /// [JsonPropertyName("details")] - public System.Collections.Generic.ICollection>? Details { get; set; } + public System.Collections.Generic.ICollection>? Details { get; set; } /// /// A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the google.rpc.Status.details field, or localized by the client. diff --git a/src/GenerativeAI/Types/RagEngine/RagFile.cs b/src/GenerativeAI/Types/RagEngine/RagFile.cs index abf9e8c5..81c5d923 100644 --- a/src/GenerativeAI/Types/RagEngine/RagFile.cs +++ b/src/GenerativeAI/Types/RagEngine/RagFile.cs @@ -65,7 +65,7 @@ public class RagFile /// Output only. The type of the RagFile. /// [JsonPropertyName("ragFileType")] - [JsonConverter(typeof(JsonStringEnumConverter))] + public RagFileType? RagFileType { get; set; } /// diff --git a/src/GenerativeAI/Types/RagEngine/RagFileType.cs b/src/GenerativeAI/Types/RagEngine/RagFileType.cs index 6ba80ed1..19427700 100644 --- a/src/GenerativeAI/Types/RagEngine/RagFileType.cs +++ b/src/GenerativeAI/Types/RagEngine/RagFileType.cs @@ -1,7 +1,9 @@ using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace GenerativeAI.Types.RagEngine; +[JsonConverter(typeof(JsonStringEnumConverter))] public enum RagFileType { diff --git a/src/GenerativeAI/Types/SemanticRetrieval/Chunks/State.cs b/src/GenerativeAI/Types/SemanticRetrieval/Chunks/State.cs index 319c03ab..8812aa68 100644 --- a/src/GenerativeAI/Types/SemanticRetrieval/Chunks/State.cs +++ b/src/GenerativeAI/Types/SemanticRetrieval/Chunks/State.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// States for the lifecycle of a . /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum ChunkState { /// diff --git a/src/GenerativeAI/Types/SemanticRetrieval/Corpus/Operator.cs b/src/GenerativeAI/Types/SemanticRetrieval/Corpus/Operator.cs index c3f2025a..bde08070 100644 --- a/src/GenerativeAI/Types/SemanticRetrieval/Corpus/Operator.cs +++ b/src/GenerativeAI/Types/SemanticRetrieval/Corpus/Operator.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// Defines the valid operators that can be applied to a key-value pair. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum Operator { /// diff --git a/src/GenerativeAI/Types/SemanticRetrieval/Permissions/GranteeType.cs b/src/GenerativeAI/Types/SemanticRetrieval/Permissions/GranteeType.cs index 6b526407..50470752 100644 --- a/src/GenerativeAI/Types/SemanticRetrieval/Permissions/GranteeType.cs +++ b/src/GenerativeAI/Types/SemanticRetrieval/Permissions/GranteeType.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// Defines types of the grantee of this permission. /// See Official API Documentation /// -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum GranteeType { /// diff --git a/src/GenerativeAI/Types/SemanticRetrieval/Permissions/Role.cs b/src/GenerativeAI/Types/SemanticRetrieval/Permissions/Role.cs index 0430c5d0..b091c6e1 100644 --- a/src/GenerativeAI/Types/SemanticRetrieval/Permissions/Role.cs +++ b/src/GenerativeAI/Types/SemanticRetrieval/Permissions/Role.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// Defines the role granted by this permission. /// See Official API Documentation /// -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum Role { /// diff --git a/src/GenerativeAI/Types/SemanticRetrieval/QuestionAnswering/AnswerStyle.cs b/src/GenerativeAI/Types/SemanticRetrieval/QuestionAnswering/AnswerStyle.cs index fdba63a7..642c5aa2 100644 --- a/src/GenerativeAI/Types/SemanticRetrieval/QuestionAnswering/AnswerStyle.cs +++ b/src/GenerativeAI/Types/SemanticRetrieval/QuestionAnswering/AnswerStyle.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// Style for grounded answers. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum AnswerStyle { /// diff --git a/src/GenerativeAI/Types/Tuning/TuningState.cs b/src/GenerativeAI/Types/Tuning/TuningState.cs index e5acd55b..d87b7948 100644 --- a/src/GenerativeAI/Types/Tuning/TuningState.cs +++ b/src/GenerativeAI/Types/Tuning/TuningState.cs @@ -6,7 +6,7 @@ namespace GenerativeAI.Types; /// The state of the tuned model. /// /// See Official API Documentation -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum TuningState { /// diff --git a/src/GenerativeAI/Types/TypesSerializerContext.cs b/src/GenerativeAI/Types/TypesSerializerContext.cs new file mode 100644 index 00000000..51e880da --- /dev/null +++ b/src/GenerativeAI/Types/TypesSerializerContext.cs @@ -0,0 +1,220 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using GenerativeAI.Core; +using GenerativeAI.Types.RagEngine; + +namespace GenerativeAI.Types; + +[JsonSerializable(typeof(CachedContent))] +[JsonSerializable(typeof(ListCachedContentsResponse))] +[JsonSerializable(typeof(Duration))] +[JsonSerializable(typeof(DurationJsonConverter))] +[JsonSerializable(typeof(Timestamp))] +[JsonSerializable(typeof(TimestampJsonConverter))] +[JsonSerializable(typeof(CitationMetadata))] +[JsonSerializable(typeof(CitationSource))] +[JsonSerializable(typeof(Schema))] +[JsonSerializable(typeof(GenerationConfig))] +[JsonSerializable(typeof(PrebuiltVoiceConfig))] +[JsonSerializable(typeof(SpeechConfig))] +[JsonSerializable(typeof(VoiceConfig))] +[JsonSerializable(typeof(AttributionSourceId))] +[JsonSerializable(typeof(GroundingAttribution))] +[JsonSerializable(typeof(GroundingChunk))] +[JsonSerializable(typeof(GroundingMetadata))] +[JsonSerializable(typeof(GroundingPassage))] +[JsonSerializable(typeof(GroundingPassageId))] +[JsonSerializable(typeof(GroundingPassages))] +[JsonSerializable(typeof(GroundingSource))] +[JsonSerializable(typeof(GroundingSupport))] +[JsonSerializable(typeof(RetrievalMetadata))] +[JsonSerializable(typeof(SearchEntryPoint))] +[JsonSerializable(typeof(Segment))] +[JsonSerializable(typeof(SemanticRetrieverChunk))] +[JsonSerializable(typeof(Web))] +[JsonSerializable(typeof(Blob))] +[JsonSerializable(typeof(Content))] +[JsonSerializable(typeof(FileData))] +[JsonSerializable(typeof(Part))] +[JsonSerializable(typeof(Candidate))] +[JsonSerializable(typeof(LogprobsCandidate))] +[JsonSerializable(typeof(LogprobsResult))] +[JsonSerializable(typeof(ModalityTokenCount))] +[JsonSerializable(typeof(PromptFeedback))] +[JsonSerializable(typeof(TopCandidates))] +[JsonSerializable(typeof(UsageMetadata))] +[JsonSerializable(typeof(CountTokensRequest))] +[JsonSerializable(typeof(GenerateContentRequest))] +[JsonSerializable(typeof(GenerateContentRequestForCountToken))] +[JsonSerializable(typeof(CountTokensResponse))] +[JsonSerializable(typeof(GenerateContentResponse))] +[JsonSerializable(typeof(SafetyRating))] +[JsonSerializable(typeof(SafetySetting))] +[JsonSerializable(typeof(Tool))] +[JsonSerializable(typeof(ToolConfig))] +[JsonSerializable(typeof(VertexRetrievalTool))] +[JsonSerializable(typeof(CodeExecutionResult))] +[JsonSerializable(typeof(CodeExecutionTool))] +[JsonSerializable(typeof(ExecutableCode))] +[JsonSerializable(typeof(FunctionCall))] +[JsonSerializable(typeof(FunctionCallingConfig))] +[JsonSerializable(typeof(FunctionDeclaration))] +[JsonSerializable(typeof(FunctionResponse))] +[JsonSerializable(typeof(GoogleSearchTool))] +[JsonSerializable(typeof(DynamicRetrievalConfig))] +[JsonSerializable(typeof(GoogleSearchRetrievalTool))] +[JsonSerializable(typeof(BatchEmbedContentRequest))] +[JsonSerializable(typeof(BatchEmbedContentsResponse))] +[JsonSerializable(typeof(ContentEmbedding))] +[JsonSerializable(typeof(EmbedContentRequest))] +[JsonSerializable(typeof(EmbedContentResponse))] +[JsonSerializable(typeof(RemoteFile))] +[JsonSerializable(typeof(ListFilesResponse))] +[JsonSerializable(typeof(Status))] +[JsonSerializable(typeof(UploadFileRequest))] +[JsonSerializable(typeof(UploadFileInformation))] +[JsonSerializable(typeof(UploadFileResponse))] +[JsonSerializable(typeof(VideoMetadata))] +[JsonSerializable(typeof(GenerateImageRequest))] +[JsonSerializable(typeof(GenerateImageResponse))] +[JsonSerializable(typeof(ImageCaptioningParameters))] +[JsonSerializable(typeof(ImageCaptioningRequest))] +[JsonSerializable(typeof(ImageCaptioningResponse))] +[JsonSerializable(typeof(ImageData))] +[JsonSerializable(typeof(ImageGenerationInstance))] +[JsonSerializable(typeof(ImageGenerationParameters))] +[JsonSerializable(typeof(ImageInstance))] +[JsonSerializable(typeof(ImageSource))] +[JsonSerializable(typeof(OutputOptions))] +[JsonSerializable(typeof(SafetyAttributes))] +[JsonSerializable(typeof(UpscaleConfig))] +[JsonSerializable(typeof(VisionGenerativeModelResult))] +[JsonSerializable(typeof(VqaImage))] +[JsonSerializable(typeof(VqaInstance))] +[JsonSerializable(typeof(VqaParameters))] +[JsonSerializable(typeof(VqaRequest))] +[JsonSerializable(typeof(VqaResponse))] +[JsonSerializable(typeof(ListModelsResponse))] +[JsonSerializable(typeof(Model))] +[JsonSerializable(typeof(BidiClientPayload))] +[JsonSerializable(typeof(BidiGenerateContentClientContent))] +[JsonSerializable(typeof(BidiGenerateContentRealtimeInput))] +[JsonSerializable(typeof(BidiGenerateContentServerContent))] +[JsonSerializable(typeof(BidiGenerateContentSetup))] +[JsonSerializable(typeof(BidiGenerateContentSetupComplete))] +[JsonSerializable(typeof(BidiGenerateContentToolCall))] +[JsonSerializable(typeof(BidiGenerateContentToolCallCancellation))] +[JsonSerializable(typeof(BidiGenerateContentToolResponse))] +[JsonSerializable(typeof(BidiResponsePayload))] +[JsonSerializable(typeof(GoogleLongRunningListOperationsResponse))] +[JsonSerializable(typeof(GoogleLongRunningOperation))] +[JsonSerializable(typeof(ApiAuth))] +[JsonSerializable(typeof(ApiAuthApiKeyConfig))] +[JsonSerializable(typeof(BigQueryDestination))] +[JsonSerializable(typeof(BigQuerySource))] +[JsonSerializable(typeof(CorpusStatus))] +[JsonSerializable(typeof(DirectUploadSource))] +[JsonSerializable(typeof(FileStatus))] +[JsonSerializable(typeof(GcsSource))] +[JsonSerializable(typeof(GoogleDriveSource))] +[JsonSerializable(typeof(GoogleDriveSourceResourceId))] +[JsonSerializable(typeof(GoogleRpcStatus))] +[JsonSerializable(typeof(ImportRagFilesConfig))] +[JsonSerializable(typeof(ImportRagFilesRequest))] +[JsonSerializable(typeof(JiraSource))] +[JsonSerializable(typeof(JiraSourceJiraQueries))] +[JsonSerializable(typeof(ListRagCorporaResponse))] +[JsonSerializable(typeof(ListRagFilesResponse))] +[JsonSerializable(typeof(RagContexts))] +[JsonSerializable(typeof(RagContextsContext))] +[JsonSerializable(typeof(RagCorpus))] +[JsonSerializable(typeof(RagEmbeddingModelConfig))] +[JsonSerializable(typeof(RagEmbeddingModelConfigHybridSearchConfig))] +[JsonSerializable(typeof(RagEmbeddingModelConfigSparseEmbeddingConfig))] +[JsonSerializable(typeof(RagEmbeddingModelConfigSparseEmbeddingConfigBm25))] +[JsonSerializable(typeof(RagEmbeddingModelConfigVertexPredictionEndpoint))] +[JsonSerializable(typeof(RagFile))] +[JsonSerializable(typeof(RagFileChunkingConfig))] +[JsonSerializable(typeof(RagFileChunkingConfigFixedLengthChunking))] +[JsonSerializable(typeof(RagFileParsingConfig))] +[JsonSerializable(typeof(RagFileParsingConfigAdvancedParser))] +[JsonSerializable(typeof(RagFileParsingConfigLayoutParser))] +[JsonSerializable(typeof(RagFileParsingConfigLlmParser))] +[JsonSerializable(typeof(RagFileTransformationConfig))] +[JsonSerializable(typeof(RagQuery))] +[JsonSerializable(typeof(RagQueryRanking))] +[JsonSerializable(typeof(RagRetrievalConfig))] +[JsonSerializable(typeof(RagRetrievalConfigFilter))] +[JsonSerializable(typeof(RagRetrievalConfigHybridSearch))] +[JsonSerializable(typeof(RagRetrievalConfigRanking))] +[JsonSerializable(typeof(RagRetrievalConfigRankingLlmRanker))] +[JsonSerializable(typeof(RagRetrievalConfigRankingRankService))] +[JsonSerializable(typeof(RagVectorDbConfig))] +[JsonSerializable(typeof(RagVectorDbConfigPinecone))] +[JsonSerializable(typeof(RagVectorDbConfigRagManagedDb))] +[JsonSerializable(typeof(RagVectorDbConfigVertexFeatureStore))] +[JsonSerializable(typeof(RagVectorDbConfigVertexVectorSearch))] +[JsonSerializable(typeof(RagVectorDbConfigWeaviate))] +[JsonSerializable(typeof(SharePointSources))] +[JsonSerializable(typeof(SharePointSource))] +[JsonSerializable(typeof(SlackSource))] +[JsonSerializable(typeof(SlackSourceSlackChannels))] +[JsonSerializable(typeof(SlackSourceSlackChannelsSlackChannel))] +[JsonSerializable(typeof(UploadRagFileConfig))] +[JsonSerializable(typeof(UploadRagFileRequest))] +[JsonSerializable(typeof(UploadRagFileResponse))] +[JsonSerializable(typeof(VertexAISearch))] +[JsonSerializable(typeof(VertexAiSearchConfig))] +[JsonSerializable(typeof(VertexRagStore))] +[JsonSerializable(typeof(VertexRagStoreRagResource))] +[JsonSerializable(typeof(SemanticRetrieverConfig))] +[JsonSerializable(typeof(BatchCreateChunksRequest))] +[JsonSerializable(typeof(BatchCreateChunksResponse))] +[JsonSerializable(typeof(BatchDeleteChunksRequest))] +[JsonSerializable(typeof(BatchUpdateChunksRequest))] +[JsonSerializable(typeof(BatchUpdateChunksResponse))] +[JsonSerializable(typeof(Chunk))] +[JsonSerializable(typeof(ChunkData))] +[JsonSerializable(typeof(CreateChunkRequest))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(DeleteChunkRequest))] +[JsonSerializable(typeof(ListChunksResponse))] +[JsonSerializable(typeof(UpdateChunkRequest))] +[JsonSerializable(typeof(Condition))] +[JsonSerializable(typeof(Corpus))] +[JsonSerializable(typeof(ListCorporaResponse))] +[JsonSerializable(typeof(MetadataFilter))] +[JsonSerializable(typeof(QueryCorpusRequest))] +[JsonSerializable(typeof(QueryCorpusResponse))] +[JsonSerializable(typeof(RelevantChunk))] +[JsonSerializable(typeof(CustomMetadata))] +[JsonSerializable(typeof(Document))] +[JsonSerializable(typeof(ListDocumentsResponse))] +[JsonSerializable(typeof(QueryDocumentRequest))] +[JsonSerializable(typeof(QueryDocumentResponse))] +[JsonSerializable(typeof(StringList))] +[JsonSerializable(typeof(ListPermissionsResponse))] +[JsonSerializable(typeof(Permission))] +[JsonSerializable(typeof(GenerateAnswerRequest))] +[JsonSerializable(typeof(GenerateAnswerResponse))] +[JsonSerializable(typeof(InputFeedback))] +[JsonSerializable(typeof(Dataset))] +[JsonSerializable(typeof(Hyperparameters))] +[JsonSerializable(typeof(ListTunedModelsResponse))] +[JsonSerializable(typeof(TunedModel))] +[JsonSerializable(typeof(TunedModelSource))] +[JsonSerializable(typeof(TuningExample))] +[JsonSerializable(typeof(TuningExamples))] +[JsonSerializable(typeof(TuningSnapshot))] +[JsonSerializable(typeof(TuningTask))] +[JsonSerializable(typeof(CredentialConfiguration))] +[JsonSerializable(typeof(JsonNode))] +[JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(JsonObject))] +[JsonSerializable(typeof(ClientSecrets))] + +[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, UseStringEnumConverter = true)] +public partial class TypesSerializerContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/tests/AotTest/AotTest.csproj b/tests/AotTest/AotTest.csproj new file mode 100644 index 00000000..4dfd7764 --- /dev/null +++ b/tests/AotTest/AotTest.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + true + + + + + + + + + + + + + + + + diff --git a/tests/AotTest/BookStoreService.cs b/tests/AotTest/BookStoreService.cs new file mode 100644 index 00000000..73ae7ae2 --- /dev/null +++ b/tests/AotTest/BookStoreService.cs @@ -0,0 +1,38 @@ +using System.ComponentModel; +using CSharpToJsonSchema; + +namespace AotTest; + +public class GetAuthorBook +{ + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; +} + +[GenerateJsonSchema] +public interface IBookStoreService +{ + [Description("Get books written by some author")] + public Task> GetAuthorBooksAsync2([Description("Author name")] string authorName, CancellationToken cancellationToken = default); + + [Description("Get book page content")] + public Task GetBookPageContentAsync2([Description("Book Name")] string bookName, [Description("Book Page Number")] int bookPageNumber, CancellationToken cancellationToken = default); + +} +public class BookStoreService : IBookStoreService +{ + public Task> GetAuthorBooksAsync2(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 GetBookPageContentAsync2(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/tests/AotTest/ComplexDataTypeService.cs b/tests/AotTest/ComplexDataTypeService.cs new file mode 100644 index 00000000..aaf27722 --- /dev/null +++ b/tests/AotTest/ComplexDataTypeService.cs @@ -0,0 +1,77 @@ +using System.ComponentModel; +using CSharpToJsonSchema; + +namespace AotTest; + +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; + +} + +[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; +} + +public class ComplexDataTypeService : IComplexDataTypeService +{ + [System.ComponentModel.Description("Get student record for the year")] + 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 + }; + } +} + +[GenerateJsonSchema()] +public interface IComplexDataTypeService +{ + [Description("Get student record for the year")] + public Task GetStudentRecordAsync(QueryStudentRecordRequest query, + CancellationToken cancellationToken = default); +} + diff --git a/tests/AotTest/JsonTests.cs b/tests/AotTest/JsonTests.cs new file mode 100644 index 00000000..02f93d77 --- /dev/null +++ b/tests/AotTest/JsonTests.cs @@ -0,0 +1,256 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using GenerativeAI; +using GenerativeAI.Core; +using GenerativeAI.Types; +using Shouldly; + + +namespace AotTest; + +public class JsonModeTests +{ + private const string DefaultTestModelName = GoogleAIModels.DefaultGeminiModel; + + private JsonSerializerOptions TestSerializerOptions + { + get + { + return new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + WriteIndented = true, + TypeInfoResolver = TestJsonSerializerContext.Default + }; + } + } + + /// + /// A helper method to create a GenerativeModel using the default Gemini model name, + /// matching the style used in basic tests. + /// + private GenerativeModel CreateInitializedModel() + { + var platform = GetTestGooglePlatform(); + + var model = new GenerativeModel(platform, DefaultTestModelName); + model.GenerateObjectJsonSerializerOptions = TestSerializerOptions; + return model; + } + + #region GenerateObjectAsync Overloads + + public async Task ShouldGenerateContentAsync_WithJsonMode_GenericParameter() + { + // Arrange + var model = CreateInitializedModel(); + + // We'll use a sample input request mimicking JSON-based generation + var request = new GenerateContentRequest(); + request.AddText("Give me a really good message.", false); + + // Act + var response = await model.GenerateContentAsync(request).ConfigureAwait(false); + + // Assert + response.ShouldNotBeNull(); + response.Text().ShouldNotBeNull(); + var obj = response.ToObject(TestSerializerOptions); + obj.Message.ShouldNotBeNullOrWhiteSpace(); + // Additional checks as needed for any placeholders or content + Console.WriteLine("GenerateContentAsync returned a valid GenerateContentResponse."); + } + + + public async Task ShouldGenerateObjectAsync_WithGenericParameter() + { + // Arrange + var model = CreateInitializedModel(); + + var request = new GenerateContentRequest(); + request.AddText("write a text message for my boss that I'm resigning from the job.", false); + + // Act + var result = await model.GenerateObjectAsync(request).ConfigureAwait(false); + + // Assert + result.ShouldNotBeNull(); + result.Message.ShouldNotBeNullOrWhiteSpace(); + Console.WriteLine($"GenerateObjectAsync(request) returned: {result.Message}"); + } + + + public async Task ShouldGenerateObjectAsync_WithStringPrompt() + { + // Arrange + var model = CreateInitializedModel(); + var prompt = "I need a birthday message for my wife."; + + // Act + var result = await model.GenerateObjectAsync(prompt).ConfigureAwait(false); + + // Assert + result.ShouldNotBeNull(); + result.Message.ShouldNotBeNullOrWhiteSpace(); + Console.WriteLine($"GenerateObjectAsync(string prompt) returned: {result.Message}"); + } + + + public async Task ShouldGenerateObjectAsync_WithPartsEnumerable() + { + // Arrange + var model = CreateInitializedModel(); + + // Build content parts with an imaginary scenario + var parts = new List + { + new Part() { Text = "I am very busy person. i always need AI help for my work." }, + new Part() { Text = "I need a message for my boss to provide me a paid subscription to Gemini Advanced." } + }; + + // Act + var result = await model.GenerateObjectAsync(parts).ConfigureAwait(false); + + // Assert + result.ShouldNotBeNull(); + result.Message.ShouldNotBeNullOrWhiteSpace(); + Console.WriteLine($"GenerateObjectAsync(IEnumerable parts) returned: {result.Message}"); + } + + #endregion + + + public async Task ShouldGenerateComplexObjectAsync_WithVariousDataTypes() + { + // Arrange + var model = CreateInitializedModel(); + var request = new GenerateContentRequest(); + request.AddText( + "Generate a structured object with various data types including dictionary, list, array, and nested objects.", + false); + + // Act + var response = await model.GenerateContentAsync(request).ConfigureAwait(false); + + // Assert + response.ShouldNotBeNull(); + response.Text().ShouldNotBeNullOrWhiteSpace(); + var obj = response.ToObject(TestSerializerOptions); + + obj.Title.ShouldNotBeNullOrWhiteSpace(); + // obj.Metadata.ShouldNotBeNull(); + // obj.Metadata.ShouldContainKey("key1"); + obj.Numbers.ShouldNotBeNull(); + obj.Numbers.Length.ShouldBeGreaterThan(0); + obj.Children.ShouldNotBeNull(); + obj.Children.ForEach(child => + { + child.Name.ShouldNotBeNullOrWhiteSpace(); + child.Values.ShouldNotBeNull(); + child.Values.ShouldNotBeEmpty(); + }); + + //obj.OptionalField.ShouldBeNull(); + Console.WriteLine("GenerateContentAsync with various data types returned a valid response."); + } + + + public async Task ShouldGenerateNestedObjectAsync_WithJsonMode() + { + // Arrange + var model = CreateInitializedModel(); + var request = new GenerateContentRequest(); + request.AddText("Generate a complex JSON object with nested properties.", false); + + // Act + var response = await model.GenerateContentAsync(request).ConfigureAwait(false); + + // Assert + response.ShouldNotBeNull(); + + response.Text().ShouldNotBeNullOrWhiteSpace(); + var obj = response.ToObject(TestSerializerOptions); + obj.Description.ShouldNotBeNullOrWhiteSpace(); + obj.Details.ShouldNotBeNull(); + obj.Details.Title.ShouldNotBeNullOrWhiteSpace(); + obj.Children.ShouldNotBeNull(); + obj.Children.ShouldNotBeEmpty(); + obj.Children.ForEach(child => + { + child.Name.ShouldNotBeNullOrWhiteSpace(); + child.Values.ShouldNotBeNull(); + child.Values.ShouldNotBeEmpty(); + }); + Console.WriteLine("GenerateContentAsync with nested types returned a valid response."); + } + + + protected virtual IPlatformAdapter GetTestGooglePlatform() + { + //return GetTestVertexAIPlatform(); + var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY", EnvironmentVariableTarget.User); + + return new GoogleAIPlatformAdapter(apiKey); + } +} + +/// +/// A complex sample class with nested classes and collections used for testing JSON deserialization. +/// +internal class ComplexJsonClass +{ + public string? Description { get; set; } + public Detail? Details { get; set; } + public List? Children { get; set; } + + public class Detail + { + public string? Title { get; set; } + public string? Content { get; set; } + } + + public class Child2 + { + public string? Name { get; set; } + public List? Values { get; set; } + } +} + +/// +/// A small sample class used for testing JSON deserialization. +/// The property name can be adjusted as needed for your test scenarios. +/// +internal class SampleJsonClass +{ + public string? Message { get; set; } +} + +/// +/// A sample class used to test serialization and deserialization with various data types. +/// +internal class ComplexDataTypeClass +{ + public string? Title { get; set; } + [JsonIgnore] public Dictionary? Metadata { get; set; } + public int[]? Numbers { get; set; } + public List? Children { get; set; } + public string? OptionalField { get; set; } + + public class Child + { + public string? Name { get; set; } + public List? Values { get; set; } + } +} + +[JsonSerializable(typeof(SampleJsonClass))] +[JsonSerializable(typeof(ComplexDataTypeClass.Child))] +[JsonSerializable(typeof(ComplexJsonClass.Child2))] +[JsonSerializable(typeof(ComplexJsonClass.Detail))] +[JsonSerializable(typeof(ComplexDataTypeClass))] +[JsonSerializable(typeof(ComplexJsonClass))] +[JsonSourceGenerationOptions(WriteIndented = true)] +internal partial class TestJsonSerializerContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/tests/AotTest/LiveTest.cs b/tests/AotTest/LiveTest.cs new file mode 100644 index 00000000..40f7d8dc --- /dev/null +++ b/tests/AotTest/LiveTest.cs @@ -0,0 +1,53 @@ +using GenerativeAI; +using GenerativeAI.Live; +using GenerativeAI.Types; +using Microsoft.Extensions.Logging; + +namespace AotTest; + +public class LiveTest +{ + public async Task ShouldRunMultiModalLive() + { + var exitEvent = new ManualResetEvent(false); + var multiModalLive = new MultiModalLiveClient(new GoogleAIPlatformAdapter(EnvironmentVariables.GOOGLE_API_KEY), + "gemini-2.0-flash-exp", new GenerationConfig() + { + ResponseModalities = [Modality.TEXT] + }); + multiModalLive.MessageReceived += (sender, e) => + { + if (e.Payload.SetupComplete != null) + { + System.Console.WriteLine($"Setup complete: {e.Payload.SetupComplete}"); + } + + Console.WriteLine("Payload received."); + if (e.Payload.ServerContent != null) + { + if (e.Payload.ServerContent.ModelTurn != null) + { + foreach (var s in e.Payload.ServerContent.ModelTurn?.Parts.Select(s => s.Text)) + { + System.Console.Write(s); + } + + if (e.Payload.ServerContent.TurnComplete == true) + { + System.Console.WriteLine(); + } + } + } + }; + multiModalLive.UseGoogleSearch = true; + await multiModalLive.ConnectAsync(); + var content = "write a poem about stars"; + var clientContent = new BidiGenerateContentClientContent(); + clientContent.Turns = new[] { new Content(content, Roles.User) }; + clientContent.TurnComplete = true; + await multiModalLive.SendClientContentAsync(clientContent); + + Task.WaitAll(); + await multiModalLive.DisconnectAsync(); + } +} \ No newline at end of file diff --git a/tests/AotTest/MEAITests.cs b/tests/AotTest/MEAITests.cs new file mode 100644 index 00000000..4953cd4d --- /dev/null +++ b/tests/AotTest/MEAITests.cs @@ -0,0 +1,80 @@ +using CSharpToJsonSchema; +using GenerativeAI.Microsoft; +using Microsoft.Extensions.AI; +using Shouldly; + + +namespace AotTest; + +public class MEAITests +{ + + public async Task ShouldWorkWithTools() + { + + var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY", EnvironmentVariableTarget.User); + var chatClient = new GenerativeAIChatClient(apiKey); + var chatOptions = new ChatOptions(); + + var tools = new Tools([GetCurrentWeather]); + chatOptions.Tools = tools.AsMeaiTools(); + var message = new ChatMessage(ChatRole.User, "What is the weather in New York in celsius?"); + var response = await chatClient.GetResponseAsync(message,options:chatOptions).ConfigureAwait(false); + + Console.WriteLine(response.Choices.LastOrDefault().Text); + response.Choices.LastOrDefault().Text.Contains("New York", StringComparison.InvariantCultureIgnoreCase); + } + + + public async Task ShouldWorkWith_BookStoreService() + { + + var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY", EnvironmentVariableTarget.User); + var chatClient = new GenerativeAIChatClient(apiKey); + var chatOptions = new ChatOptions(); + + + var tools = new Tools([GetBookPageContentAsync]); + chatOptions.Tools = tools.AsMeaiTools(); + + var message = new ChatMessage(ChatRole.User, "what is written on page 96 in the book 'damdamadum'"); + var response = await chatClient.GetResponseAsync(message,options:chatOptions).ConfigureAwait(false); + + response.Choices.LastOrDefault().Text.ShouldContain("damdamadum",Case.Insensitive); + } + + [FunctionTool(MeaiFunctionTool = true)] + [System.ComponentModel.Description("Get book page content")] + public static 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."); + } + + [FunctionTool(MeaiFunctionTool = true)] + [System.ComponentModel.Description("Get the current weather in a given location")] + public Weather GetCurrentWeather(string location, Unit unit = Unit.Celsius) + { + return new Weather + { + Location = location, + Temperature = 30.0, + Unit = unit, + Description = "Sunny", + }; + } + + public enum Unit + { + Celsius, + Fahrenheit, + Imperial + } + + public class Weather + { + public string Location { get; set; } = string.Empty; + public double Temperature { get; set; } + public Unit Unit { get; set; } + public string Description { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/tests/AotTest/Program.cs b/tests/AotTest/Program.cs new file mode 100644 index 00000000..3cdbfc2d --- /dev/null +++ b/tests/AotTest/Program.cs @@ -0,0 +1,26 @@ +// See https://aka.ms/new-console-template for more information + +using AotTest; + + +var testClass = new JsonModeTests(); + +await testClass.ShouldGenerateComplexObjectAsync_WithVariousDataTypes(); + await testClass.ShouldGenerateObjectAsync_WithGenericParameter(); + await testClass.ShouldGenerateObjectAsync_WithPartsEnumerable(); + await testClass.ShouldGenerateContentAsync_WithJsonMode_GenericParameter(); + await testClass.ShouldGenerateNestedObjectAsync_WithJsonMode(); + await testClass.ShouldGenerateObjectAsync_WithStringPrompt(); + + var liveTest = new LiveTest(); + await liveTest.ShouldRunMultiModalLive(); + +var toolsTest = new WeatherServiceTests(); +await toolsTest.ShouldInvokeWetherService(); +await toolsTest.ShouldWorkWith_BookStoreService(); +await toolsTest.ShouldWorkWith_ComplexDataTypes(); + +var meai = new MEAITests(); +await meai.ShouldWorkWith_BookStoreService(); +await meai.ShouldWorkWithTools(); + diff --git a/tests/AotTest/WeatherService.cs b/tests/AotTest/WeatherService.cs new file mode 100644 index 00000000..02fec9e1 --- /dev/null +++ b/tests/AotTest/WeatherService.cs @@ -0,0 +1,65 @@ +using CSharpToJsonSchema; +using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; + +namespace AotTest +{ + public enum Unit + { + Celsius, + Fahrenheit, + Imperial + } + + public class Weather + { + public string Location { get; set; } = string.Empty; + public double Temperature { get; set; } + public Unit Unit { get; set; } + public string Description { get; set; } = string.Empty; + } + + [GenerateJsonSchema()] + public interface IWeatherFunctions + { + [Description("Get the current weather in a given location")] + public Weather GetCurrentWeather2( + [Description("The city and state, e.g. San Francisco, CA")] + string location, + Unit unit = Unit.Celsius); + + [Description("Get the current weather in a given location")] + public Task GetCurrentWeatherAsync2( + [Description("The city and state, e.g. San Francisco, CA")] + string location, + Unit unit = Unit.Celsius, + CancellationToken cancellationToken = default); + } + + [Description("Weather Functions")] + public class WeatherService : IWeatherFunctions + { + [Description("Get the current weather in a given location")] + public Weather GetCurrentWeather2(string location, Unit unit = Unit.Celsius) + { + return new Weather + { + Location = location, + Temperature = 30.0, + Unit = unit, + Description = "Sunny", + }; + } + + public Task GetCurrentWeatherAsync2(string location, Unit unit = Unit.Celsius, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new Weather + { + Location = location, + Temperature = 22.0, + Unit = unit, + Description = "Sunny", + }); + } + } +} \ No newline at end of file diff --git a/tests/AotTest/WeatherServiceTests.cs b/tests/AotTest/WeatherServiceTests.cs new file mode 100644 index 00000000..46bb8631 --- /dev/null +++ b/tests/AotTest/WeatherServiceTests.cs @@ -0,0 +1,54 @@ +using GenerativeAI; +using GenerativeAI.Core; +using GenerativeAI.Tools; + +namespace AotTest; + +public class WeatherServiceTests +{ + + public async Task ShouldInvokeWetherService() + { + WeatherService service = new WeatherService(); + var tools = service.AsTools(); + var calls = service.AsCalls(); + var tool = new GenericFunctionTool(tools, calls); + + var model = new GenerativeModel(GetTestGooglePlatform(), GoogleAIModels.DefaultGeminiModel); + + model.AddFunctionTool(tool); + + var result = await model.GenerateContentAsync("What is the weather in san francisco today?").ConfigureAwait(false); + + Console.WriteLine(result.Text()); + } + + + public async Task ShouldWorkWith_BookStoreService() + { + var service = new BookStoreService(); + var tool = new GenericFunctionTool(service.AsTools(), service.AsCalls()); + var model = new GenerativeModel(GetTestGooglePlatform(), GoogleAIModels.DefaultGeminiModel); + model.AddFunctionTool(tool); + var result = await model.GenerateContentAsync("what is written on page 35 in the book 'abracadabra'").ConfigureAwait(false); + Console.WriteLine(result.Text()); + } + + public async Task ShouldWorkWith_ComplexDataTypes() + { + var service = new ComplexDataTypeService(); + var tool = new GenericFunctionTool(service.AsTools(), service.AsCalls()); + var model = new GenerativeModel(GetTestGooglePlatform(), GoogleAIModels.Gemini2Flash); + model.AddFunctionTool(tool); + var result = await model.GenerateContentAsync("how's Deepak Siwach is doing in Senior Grade for enrollment year 01-01-2024 to 01-01-2025").ConfigureAwait(false); + Console.WriteLine(result.Text()); + } + + protected virtual IPlatformAdapter GetTestGooglePlatform() + { + //return GetTestVertexAIPlatform(); + var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY", EnvironmentVariableTarget.User); + + return new GoogleAIPlatformAdapter(apiKey); + } +} \ No newline at end of file diff --git a/tests/GenerativeAI.Auth.Tests/ServiceAccount_Tests.cs b/tests/GenerativeAI.Auth.Tests/ServiceAccount_Tests.cs index c057bfb1..2197da2b 100644 --- a/tests/GenerativeAI.Auth.Tests/ServiceAccount_Tests.cs +++ b/tests/GenerativeAI.Auth.Tests/ServiceAccount_Tests.cs @@ -1,7 +1,6 @@ using GenerativeAI.Authenticators; using GenerativeAI.Core; using GenerativeAI.Tests; -using Humanizer; using Shouldly; namespace GenerativeAI.Auth; diff --git a/tests/GenerativeAI.IntegrationTests/GenerativeAI.IntegrationTests.csproj b/tests/GenerativeAI.IntegrationTests/GenerativeAI.IntegrationTests.csproj index 9a3caf7e..c40cd5ba 100644 --- a/tests/GenerativeAI.IntegrationTests/GenerativeAI.IntegrationTests.csproj +++ b/tests/GenerativeAI.IntegrationTests/GenerativeAI.IntegrationTests.csproj @@ -1,17 +1,18 @@  - net9.0 + net6.0;net9.0 enable enable latest false true - Exe + Exe + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/GenerativeAI.IntegrationTests/QuickTool_Tests.cs b/tests/GenerativeAI.IntegrationTests/QuickTool_Tests.cs new file mode 100644 index 00000000..6ef61e60 --- /dev/null +++ b/tests/GenerativeAI.IntegrationTests/QuickTool_Tests.cs @@ -0,0 +1,241 @@ +using System.ComponentModel; +using System.Text.Json.Nodes; +using GenerativeAI.Tests; +using GenerativeAI.Tools; +using GenerativeAI.Types; +using Shouldly; + +namespace GenerativeAI.IntegrationTests; + +public class QuickTool_Tests : TestBase +{ + public QuickTool_Tests(ITestOutputHelper helper) : base(helper) + { + } + + [Fact] + public async Task ShouldCreateQuickTool_Async() + { + var func = + (async ([Description("Student Name")] string studentName, + [Description("Student Grade")] GradeLevel grade) => + { + return + $"{studentName} in {grade} grade is achieving remarkable scores in math and physics, showcasing outstanding progress."; + }); + + var quickFt = new QuickTool(func, "GetStudentRecordAsync", "Return student record for the year"); + + var args = new JsonObject(); + args.Add("studentName", "John"); + args.Add("grade", "Freshman"); + var res = await quickFt.CallAsync(new FunctionCall() + { + Name = "GetStudentRecordAsync", + Args = args + }); + + (res.Response as JsonNode)["content"].GetValue().ShouldContain("John"); + } + + [Fact] + public async Task ShouldCreateQuickTool() + { + var func = + (([Description("Student Name")] string studentName, [Description("Student Grade")] GradeLevel grade) => + { + return + $"{studentName} in {grade} grade is achieving remarkable scores in math and physics, showcasing outstanding progress."; + }); + + var quickFt = new QuickTool(func, "GetStudentRecordAsync", "Return student record for the year"); + + var args = new JsonObject(); + args.Add("studentName", "John"); + args.Add("grade", "Freshman"); + var res = await quickFt.CallAsync(new FunctionCall() + { + Name = "GetStudentRecordAsync", + Args = args + }); + (res.Response as JsonNode)["content"].GetValue().ShouldContain("John"); + } + + [Fact] + public async Task ShouldCreateQuickTool_void() + { + bool invoked = false; + var func = + (([Description("Student Name")] string studentName, [Description("Student Grade")] GradeLevel grade) => + { + var str = + $"{studentName} in {grade} grade is achieving remarkable scores in math and physics, showcasing outstanding progress."; + Console.WriteLine(str); + invoked = true; + }); + + var quickFt = new QuickTool(func, "GetStudentRecordAsync", "Return student record for the year"); + + var args = new JsonObject(); + args.Add("studentName", "John"); + args.Add("grade", "Freshman"); + var res = await quickFt.CallAsync(new FunctionCall() + { + Name = "GetStudentRecordAsync", + Args = args + }); + invoked.ShouldBeTrue(); + (res.Response as JsonNode)["content"].GetValue().ShouldBeEmpty(); + } + + [Fact] + public async Task ShouldCreateQuickTool_Task() + { + bool invoked = false; + var func = (async ([Description("Student Name")] string studentName, + [Description("Student Grade")] GradeLevel grade) => + { + var str = + $"{studentName} in {grade} grade is achieving remarkable scores in math and physics, showcasing outstanding progress."; + await Task.Delay(100); + invoked = true; + }); + + var quickFt = new QuickTool(func, "GetStudentRecordAsync", "Return student record for the year"); + + var args = new JsonObject(); + args.Add("studentName", "John"); + args.Add("grade", "Freshman"); + var res = await quickFt.CallAsync(new FunctionCall() + { + Name = "GetStudentRecordAsync", + Args = args + }); + invoked.ShouldBeTrue(); + (res.Response as JsonNode)["content"].GetValue().ShouldBeEmpty(); + + quickFt.FunctionDeclaration.Parameters.ShouldSatisfyAllConditions( + parameters => + { + parameters.ShouldNotBeNull(); + parameters.Properties.Keys.ShouldContain("studentName"); + parameters.Properties.Keys.ShouldContain("grade"); + parameters.Properties["studentName"].Type.ShouldBe("string"); + parameters.Properties["studentName"].Description.ShouldBe("Student Name"); + parameters.Properties["grade"].Type.ShouldBe("string"); + parameters.Properties["grade"].Description.ShouldBe("Student Grade"); + + }); + } + + [Fact] + public async Task ShouldCreateQuickTool_ComplexDataTypes() + { + var func = (async ([Description("Request to query student record")] QueryStudentRecordRequest query) => + { + return new StudentRecord + { + StudentId = "12345", + FullName = "John Doe", + Level = GradeLevel.Freshman, + EnrolledCourses = new List { "Math", "Physics", "Chemistry" }, + Grades = new Dictionary + { + { "Math", 95.0 }, + { "Physics", 89.0 }, + { "Chemistry", 88.0 } + }, + EnrollmentDate = new DateTime(2023, 1, 10), + IsActive = true + }; + }); + + + + var quickFt = new QuickTool(func, "GetStudentRecordAsync", "Return student record for the year"); + + var args = new JsonObject(); + args.Add("studentName", "John"); + args.Add("grade", "Freshman"); + var res = await quickFt.CallAsync(new FunctionCall() + { + Name = "GetStudentRecordAsync", + Args = args + }); + + var content = res.Response as JsonNode; + content = content["content"] as JsonObject; + content["studentId"].GetValue().ShouldBe("12345"); + content["fullName"].GetValue().ShouldBe("John Doe"); + content["level"].GetValue().ShouldBe("Freshman"); + content["enrolledCourses"].AsArray().Select(x => x.GetValue()) + .ShouldBe(new List { "Math", "Physics", "Chemistry" }); + content["grades"]["Math"].GetValue().ShouldBe(95.0); + content["grades"]["Physics"].GetValue().ShouldBe(89.0); + content["grades"]["Chemistry"].GetValue().ShouldBe(88.0); + content["enrollmentDate"].GetValue().ShouldBe(new DateTime(2023, 1, 10)); + content["isActive"].GetValue().ShouldBe(true); + + quickFt.FunctionDeclaration.Parameters.ShouldSatisfyAllConditions( + schema => + { + schema.ShouldNotBeNull(); + + var querySchema = schema.Properties["query"]; + querySchema.Properties.Keys.ShouldBe(new[] + { + "fullName", "gradeFilters", "enrollmentStartDate", "enrollmentEndDate", "isActive" + }); + querySchema.Properties["fullName"].Type.ShouldBe("string"); + querySchema.Properties["fullName"].Description.ShouldBe("The student's full name."); + querySchema.Properties["gradeFilters"].Type.ShouldBe("array"); + querySchema.Properties["gradeFilters"].Description.ShouldBe("Grade filters for querying specific grades, e.g., Freshman or Senior."); + querySchema.Properties["enrollmentStartDate"].Type.ShouldBe("string"); + querySchema.Properties["enrollmentStartDate"].Format.ShouldBe("date-time"); + querySchema.Properties["enrollmentStartDate"].Description.ShouldBe("The start date for the enrollment date range. ISO 8601 standard date"); + querySchema.Properties["enrollmentEndDate"].Type.ShouldBe("string"); + querySchema.Properties["enrollmentEndDate"].Format.ShouldBe("date-time"); + querySchema.Properties["enrollmentEndDate"].Description.ShouldBe("The end date for the enrollment date range. ISO 8601 standard date"); + querySchema.Properties["isActive"].Type.ShouldBe("boolean"); + querySchema.Properties["isActive"].Description.ShouldBe("The flag indicating whether to include only active students."); + }); + } + + [Fact] + public async Task ShouldInvokeWetherService() + { + Assert.SkipUnless(IsGeminiApiKeySet,GeminiTestSkipMessage); + + var func = (async ([Description("Request to query student record")] QueryStudentRecordRequest query) => + { + return new StudentRecord + { + StudentId = "12345", + FullName = query.FullName, + Level = GradeLevel.Freshman, + EnrolledCourses = new List { "Math", "Physics", "Chemistry" }, + Grades = new Dictionary + { + { "Math", 95.0 }, + { "Physics", 89.0 }, + { "Chemistry", 88.0 } + }, + EnrollmentDate = new DateTime(2023, 1, 10), + IsActive = true + }; + }); + + var quickFt = new QuickTool(func, "GetStudentRecordAsync", "Return student record for the year"); + + var tool = quickFt; + + var model = new GenerativeModel(GetTestGooglePlatform(), GoogleAIModels.Gemini2Flash); + + model.AddFunctionTool(tool); + + var result = await model.GenerateContentAsync("How's Amit Rana is doing in Senior Grade? in enrollment year 01-01-2024 to 01-01-2025").ConfigureAwait(false); + + result.Text().ShouldContain("Amit Rana",Case.Insensitive); + Console.WriteLine(result.Text()); + } +} \ No newline at end of file diff --git a/tests/GenerativeAI.IntegrationTests/BookStoreService.cs b/tests/GenerativeAI.IntegrationTests/Services/BookStoreService.cs similarity index 100% rename from tests/GenerativeAI.IntegrationTests/BookStoreService.cs rename to tests/GenerativeAI.IntegrationTests/Services/BookStoreService.cs diff --git a/tests/GenerativeAI.IntegrationTests/Services/StudentRecord_ComplexDataTypes.cs b/tests/GenerativeAI.IntegrationTests/Services/StudentRecord_ComplexDataTypes.cs new file mode 100644 index 00000000..c4d62344 --- /dev/null +++ b/tests/GenerativeAI.IntegrationTests/Services/StudentRecord_ComplexDataTypes.cs @@ -0,0 +1,49 @@ +using System.ComponentModel; + +namespace GenerativeAI.IntegrationTests; + +public enum GradeLevel +{ + Freshman, + Sophomore, + Junior, + Senior, + Graduate +} + +public class StudentRecord +{ + + 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; +} \ No newline at end of file diff --git a/tests/GenerativeAI.IntegrationTests/WeatherService.cs b/tests/GenerativeAI.IntegrationTests/Services/WeatherService.cs similarity index 100% rename from tests/GenerativeAI.IntegrationTests/WeatherService.cs rename to tests/GenerativeAI.IntegrationTests/Services/WeatherService.cs diff --git a/tests/GenerativeAI.IntegrationTests/WeatherServiceTests.cs b/tests/GenerativeAI.IntegrationTests/WeatherServiceTests.cs index 125ef391..50eae5fd 100644 --- a/tests/GenerativeAI.IntegrationTests/WeatherServiceTests.cs +++ b/tests/GenerativeAI.IntegrationTests/WeatherServiceTests.cs @@ -21,7 +21,6 @@ public async Task ShouldInvokeWetherService() var model = new GenerativeModel(GetTestGooglePlatform(), GoogleAIModels.DefaultGeminiModel); model.AddFunctionTool(tool); - var result = await model.GenerateContentAsync("What is the weather in san francisco today?").ConfigureAwait(false); diff --git a/tests/GenerativeAI.Live.Tests/GenerativeAI.Live.Tests.csproj b/tests/GenerativeAI.Live.Tests/GenerativeAI.Live.Tests.csproj index f46d287b..13a487dd 100644 --- a/tests/GenerativeAI.Live.Tests/GenerativeAI.Live.Tests.csproj +++ b/tests/GenerativeAI.Live.Tests/GenerativeAI.Live.Tests.csproj @@ -6,12 +6,14 @@ enable false Exe + true - - + + + diff --git a/tests/GenerativeAI.Live.Tests/MultiModalLive.cs b/tests/GenerativeAI.Live.Tests/MultiModalLive.cs index 0e7cd7da..2c0327a1 100644 --- a/tests/GenerativeAI.Live.Tests/MultiModalLive.cs +++ b/tests/GenerativeAI.Live.Tests/MultiModalLive.cs @@ -1,9 +1,11 @@ using GenerativeAI.Clients; using GenerativeAI.Live; using GenerativeAI.Types; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; +using Microsoft.Extensions.Options; namespace GenerativeAI.Tests.Model; - public class MultiModalLive_Tests { public static async Task Main(string[] args) @@ -13,9 +15,17 @@ public static async Task Main(string[] args) public async Task ShouldRunMultiModalLive() { + var logger = LoggerFactory.Create((s) => + { + s.AddSimpleConsole(); + }).CreateLogger(); + var exitEvent = new ManualResetEvent(false); var multiModalLive = new MultiModalLiveClient(new GoogleAIPlatformAdapter(EnvironmentVariables.GOOGLE_API_KEY), - "gemini-2.0-flash-exp"); + "gemini-2.0-flash-exp", new GenerationConfig() + { + ResponseModalities = [Modality.TEXT] + },logger:logger); multiModalLive.MessageReceived += (sender, e) => { if (e.Payload.SetupComplete != null) @@ -23,6 +33,7 @@ public async Task ShouldRunMultiModalLive() System.Console.WriteLine($"Setup complete: {e.Payload.SetupComplete}"); } + Console.WriteLine("Payload received."); if (e.Payload.ServerContent != null) { if (e.Payload.ServerContent.ModelTurn != null) diff --git a/tests/GenerativeAI.Microsoft.Tests/MicrosoftExtension_Tests.cs b/tests/GenerativeAI.Microsoft.Tests/MicrosoftExtension_Tests.cs index 2506d4da..9f46e803 100644 --- a/tests/GenerativeAI.Microsoft.Tests/MicrosoftExtension_Tests.cs +++ b/tests/GenerativeAI.Microsoft.Tests/MicrosoftExtension_Tests.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Nodes; using Bogus; using GenerativeAI.IntegrationTests; using GenerativeAI.Microsoft.Extensions; @@ -191,7 +192,7 @@ public void ToAiContents_WithTextPart_ReturnsTextContent() public void ToAiContents_WithFunctionCallPart_ReturnsFunctionCallContent() { // Arrange - var parts = new List { new Part { FunctionCall = new FunctionCall { Name = "myFunction", Args = new { arg1 = "value1", arg2 = "value2" } } } }; + var parts = new List { new Part { FunctionCall = new FunctionCall { Name = "myFunction", Args = null } } }; // Act var result = parts.ToAiContents(); @@ -210,7 +211,7 @@ public void ToAiContents_WithFunctionCallPart_ReturnsFunctionCallContent() public void ToAiContents_WithFunctionResponsePart_ReturnsFunctionResultContent() { // Arrange - var parts = new List { new Part { FunctionResponse = new FunctionResponse { Name = "myFunction", Response = new { result = "success" } } } }; + var parts = new List { new Part { FunctionResponse = new FunctionResponse { Name = "myFunction", Response = JsonNode.Parse("{ \"result\": \"value\" }") } } }; // Act var result = parts.ToAiContents(); @@ -250,7 +251,7 @@ public void ToAiContents_WithMultipleParts_ReturnsMultipleContents() var parts = new List { new Part { Text = "Hello, world!" }, - new Part { FunctionCall = new FunctionCall { Name = "myFunction", Args = new { arg1 = "value1" } } }, + new Part { FunctionCall = new FunctionCall { Name = "myFunction", Args = null } }, new Part { InlineData = new Blob { MimeType = "image/png", Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAFAAH/8mdr1QAAAABJRU5ErkJggg==" } } }; @@ -279,14 +280,14 @@ public void ToAiContents_BogusData_HandlesVariousParts() else { o.FunctionCall = f.Random.Bool(0.5f) - ? new FunctionCall { Name = f.Internet.DomainName(), Args = new Faker().Generate()} + ? new FunctionCall { Name = f.Internet.DomainName(), Args = null } : null; } if (f.Random.Bool(0.33f)) { o.FunctionResponse = f.Random.Bool(0.5f) - ? new FunctionResponse { Name = f.Internet.DomainName(), Response = new Faker().Generate() } + ? new FunctionResponse { Name = f.Internet.DomainName(), Response = JsonNode.Parse("{ \"result\": \"value\" }") } : null; } diff --git a/tests/GenerativeAI.Microsoft.Tests/Microsoft_AIFunction_Tests.cs b/tests/GenerativeAI.Microsoft.Tests/Microsoft_AIFunction_Tests.cs index ce7124a9..afde82a5 100644 --- a/tests/GenerativeAI.Microsoft.Tests/Microsoft_AIFunction_Tests.cs +++ b/tests/GenerativeAI.Microsoft.Tests/Microsoft_AIFunction_Tests.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using System.Text.Json; using System.Text.Json.Nodes; @@ -7,7 +8,7 @@ using ChatOptions = Microsoft.Extensions.AI.ChatOptions; using GenerativeAI.Microsoft.Extensions; using GenerativeAI.Tests; -using Json.More; + using Microsoft.Extensions.AI; using Newtonsoft.Json; using Shouldly; @@ -16,8 +17,14 @@ namespace GenerativeAI.IntegrationTests; + + public class Microsoft_AIFunction_Tests:TestBase { + public Microsoft_AIFunction_Tests(ITestOutputHelper helper) : base(helper) + { + + } [Fact] public async Task ShouldWorkWithTools() { @@ -34,6 +41,23 @@ public async Task ShouldWorkWithTools() .ShouldBeTrue(); } + [Fact] + public async Task ShouldWorkWithComplexClasses() + { + Assert.SkipUnless(IsGeminiApiKeySet,GeminiTestSkipMessage); + var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY", EnvironmentVariableTarget.User); + var chatClient = new GenerativeAIChatClient(apiKey, modelName:"models/gemini-2.0-flash"); + var chatOptions = new ChatOptions(); + + chatOptions.Tools = new List{AIFunctionFactory.Create(GetStudentRecordAsync)}; + 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) + .ShouldBeTrue(); + Console.WriteLine(response.Choices.LastOrDefault().Text); + } + [Fact] public async Task ShouldWorkWith_BookStoreService() { @@ -71,6 +95,28 @@ public Weather GetCurrentWeather(string location, Unit unit = Unit.Celsius) }; } + [System.ComponentModel.Description("Get student record for the year")] + + public async Task GetStudentRecordAsync(QueryStudentRecordRequest query) + { + 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 enum Unit { Celsius, @@ -85,4 +131,50 @@ public class Weather public Unit Unit { get; set; } public string Description { get; set; } = string.Empty; } + + 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; + } + } \ No newline at end of file diff --git a/tests/GenerativeAI.Microsoft.Tests/Microsoft_ChatClient_Tests.cs b/tests/GenerativeAI.Microsoft.Tests/Microsoft_ChatClient_Tests.cs index 4d3030f0..c3f23027 100644 --- a/tests/GenerativeAI.Microsoft.Tests/Microsoft_ChatClient_Tests.cs +++ b/tests/GenerativeAI.Microsoft.Tests/Microsoft_ChatClient_Tests.cs @@ -195,20 +195,20 @@ public void ShouldReturnSelfFromGetServiceIfTypeMatches() Console.WriteLine("GetService returned the correct instance when serviceType matches the client type."); } - [Fact, TestPriority(8)] - public void ShouldReturnNullFromGetServiceIfTypeDoesNotMatch() - { - // Arrange - var adapter = GetTestGooglePlatform(); - var client = new GenerativeAIChatClient(adapter); - - // Act - var service = client.GetService(typeof(object)); - - // Assert - service.ShouldBeNull(); - Console.WriteLine("GetService returned null when the requested serviceType did not match."); - } + // [Fact, TestPriority(8)] + // public void ShouldReturnNullFromGetServiceIfTypeDoesNotMatch() + // { + // // Arrange + // var adapter = GetTestGooglePlatform(); + // var client = new GenerativeAIChatClient(adapter); + // + // // Act + // var service = client.GetService(typeof(object)); + // + // // Assert + // service.ShouldBeNull(); + // Console.WriteLine("GetService returned null when the requested serviceType did not match."); + // } #endregion @@ -231,6 +231,6 @@ public void MetadataShouldBeNullByDefault() protected override IPlatformAdapter GetTestGooglePlatform() { Assert.SkipWhen(!IsGeminiApiKeySet, GeminiTestSkipMessage); - return new GoogleAIPlatformAdapter("sldakfhklash fklasdhklf"); + return new GoogleAIPlatformAdapter(EnvironmentVariables.GOOGLE_API_KEY); } } \ No newline at end of file diff --git a/tests/GenerativeAI.SemanticRetrieval.Tests/Clients/RagEngine/VertexRagManager_Tests.cs b/tests/GenerativeAI.SemanticRetrieval.Tests/Clients/RagEngine/VertexRagManager_Tests.cs index ec7a55cc..d4f36b9f 100644 --- a/tests/GenerativeAI.SemanticRetrieval.Tests/Clients/RagEngine/VertexRagManager_Tests.cs +++ b/tests/GenerativeAI.SemanticRetrieval.Tests/Clients/RagEngine/VertexRagManager_Tests.cs @@ -45,6 +45,7 @@ public async Task ShouldCreateCorpusWithPineconeAsync() { // Arrange var client = new VertexRagManager(GetTestVertexAIPlatform(), null); + //client.Platform.Authenticator = new GoogleCloudAdcAuthenticator(); var newCorpus = new RagCorpus { DisplayName = "Test Pinecone Corpus", @@ -58,7 +59,8 @@ public async Task ShouldCreateCorpusWithPineconeAsync() { IndexName = "test-index-5" }, - apiKeyResourceName: Environment.GetEnvironmentVariable("pinecone-secret")) + apiKeyResourceName:"projects/103876794532/secrets/pinecone/versions/1") + //apiKeyResourceName: Environment.GetEnvironmentVariable("pinecone-secret")) .ConfigureAwait(false); // Assert @@ -232,23 +234,29 @@ public async Task ShouldListCorporaAsync() // Console.WriteLine($"Corpora updated: {updated.DisplayName}, "); // } - [Fact, TestPriority(100)] + //[Fact(Skip = "Not needed", Explicit = true), TestPriority(100)] + [RunnableInDebugOnly] public async Task ShouldDeleteCorporaAsync() { // Arrange var client = new VertexRagManager(GetTestVertexAIPlatform(), null); var list = await client.ListCorporaAsync().ConfigureAwait(false); - var corpusName = list.RagCorpora - .FirstOrDefault(s => s.DisplayName.Contains("test", StringComparison.OrdinalIgnoreCase)).Name; - - // Act - await client.DeleteRagCorpusAsync(corpusName).ConfigureAwait(false); + foreach (var l in list.RagCorpora) + { + await client.DeleteRagCorpusAsync(l.Name).ConfigureAwait(false); + + } + // var corpusName = list.RagCorpora + // .FirstOrDefault(s => s.DisplayName.Contains("test", StringComparison.OrdinalIgnoreCase)).Name; + // + // // Act + // await client.DeleteRagCorpusAsync(corpusName).ConfigureAwait(false); // Assert // No exception should be thrown - Console.WriteLine($"Corpus Deleted: {corpusName}"); + //Console.WriteLine($"Corpus Deleted: {corpusName}"); } [Fact, TestPriority(7)] @@ -272,7 +280,8 @@ public async Task ShouldUploadLocalFileAsync() Console.WriteLine($"Corpus Deleted: {corpusName}"); } - [Fact(Skip = "Not needed",Explicit = true), TestPriority(7)] + //[Fact(Skip = "Not needed",Explicit = true), TestPriority(7)] + [Fact, TestPriority(7)] public async Task ShouldImportFileAsync() { // Arrange @@ -283,11 +292,11 @@ public async Task ShouldImportFileAsync() .FirstOrDefault(s => s.DisplayName.Contains("test", StringComparison.OrdinalIgnoreCase)).Name; var request = new ImportRagFilesRequest(); - // request.AddGooglDriveSource(new GoogleDriveSourceResourceId() - // { - // ResourceId = "", - // ResourceType = GoogleDriveSourceResourceIdResourceType.RESOURCE_TYPE_FILE - // }); + request.AddGooglDriveSource(new GoogleDriveSourceResourceId() + { + ResourceId = "", + ResourceType = GoogleDriveSourceResourceIdResourceType.RESOURCE_TYPE_FILE + }); var file = "TestData/pg1184.txt"; // Act var result = await client.ImportFilesAsync(corpusName, request).ConfigureAwait(false); @@ -329,7 +338,8 @@ public async Task ShouldGetFileAsync() var f = await client.GetFileAsync(last.Name).ConfigureAwait(false); } - [Fact(Skip = "Not needed",Explicit = true), TestPriority(7)] + //[Fact(Skip = "Not needed",Explicit = true), TestPriority(7)] + [Fact, TestPriority(7)] public async Task ShouldQueryWithCorpusAsync() { // Arrange diff --git a/tests/GenerativeAI.Tests/GenerativeAI.Tests.csproj b/tests/GenerativeAI.Tests/GenerativeAI.Tests.csproj index 8a781246..470df0e7 100644 --- a/tests/GenerativeAI.Tests/GenerativeAI.Tests.csproj +++ b/tests/GenerativeAI.Tests/GenerativeAI.Tests.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0; + net8.0;net9.0;net6.0; enable enable latest diff --git a/tests/GenerativeAI.Tests/Model/GenerativeAI_Multimodal_Tests.cs b/tests/GenerativeAI.Tests/Model/GenerativeAI_Multimodal_Tests.cs index 3ffb9f44..2fd41fd4 100644 --- a/tests/GenerativeAI.Tests/Model/GenerativeAI_Multimodal_Tests.cs +++ b/tests/GenerativeAI.Tests/Model/GenerativeAI_Multimodal_Tests.cs @@ -96,6 +96,34 @@ public async Task ShouldProcessAudioWithFilePath() // text.ShouldContain("theological", Case.Insensitive); Console.WriteLine(result.Text()); } + + // [Fact] + // public async Task ShouldProcessRemoteFile() + // { + // //Arrange + // + // var vertex = GetTestVertexAIPlatform(); + // //var model = CreateInitializedModel(); + // + // var model = new GeminiModel(vertex, TestModel); + // string prompt = "what is this audio about?"; + // + // var request = new GenerateContentRequest(); + // request.AddRemoteFile("https://storage.googleapis.com/cloud-samples-data/generative-ai/audio/pixel.mp3","audio/mp3"); + // + // //request.AddRemoteFile("https://www.gutenberg.org/cache/epub/1184/pg1184.txt","text/plain"); + // request.AddText(prompt); + // //Act + // var result = await model.GenerateContentAsync(request).ConfigureAwait(false); + // + // //Assert + // result.ShouldNotBeNull(); + // var text = result.Text(); + // text.ShouldNotBeNull(); + // // if(!text.Contains("theological",StringComparison.InvariantCultureIgnoreCase) && !text.Contains("Friedrich",StringComparison.InvariantCultureIgnoreCase)) + // // text.ShouldContain("theological", Case.Insensitive); + // Console.WriteLine(result.Text()); + // } [Fact] public async Task ShouldIdentifyImageWithWithStreaming() diff --git a/tests/GenerativeAI.Tests/Platforms/GooglAIAdapter_Initialization_Tests.cs b/tests/GenerativeAI.Tests/Platforms/GooglAIAdapter_Initialization_Tests.cs index 9cb302c1..aac2cbab 100644 --- a/tests/GenerativeAI.Tests/Platforms/GooglAIAdapter_Initialization_Tests.cs +++ b/tests/GenerativeAI.Tests/Platforms/GooglAIAdapter_Initialization_Tests.cs @@ -110,16 +110,16 @@ public void Constructor_WithLogger_ShouldInitializeLogger() { // Arrange var loggerMock = new Mock(); - + // Act var adapter = new GoogleAIPlatformAdapter("TEST_API_KEY", logger: loggerMock.Object); - + // Assert // Using reflection because Logger is not public: var loggerProperty = adapter.GetType().GetProperty("Logger", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); loggerProperty.ShouldNotBeNull("Logger property should exist."); - + var actualLogger = loggerProperty!.GetValue(adapter) as ILogger; actualLogger.ShouldNotBeNull(); actualLogger.ShouldBe(loggerMock.Object);