From ef8251c91465e688c24090cd859ba12c58f97a6b Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:25:12 +0100 Subject: [PATCH] .Net: Add JsonElement String to Primitive Implicit Conversion Support (SLM Function Calling) (#9784) ### Motivation and Context In the original logic the conversion was giving priority for existence of converters when the parameter value was a JsonElement/JsonDocument/JsonNode, this change checks if the argument is one of those types first and use the proper JSON conversion. This change also bring some resiliency when the `JsonElement` provided is a `string` for primitive types like `boolean` and C# numeric types. This change improves function calling experience when using local models that send JSON string argument values ("1" or "true") instead of the expected JSON type (1, true) for calling functions. i.e: `Llama 3.1, Llama 3.2` Added Unit Tests covering the added JsonElement arguments support. - Fixes #9711 - Extra (Remove of Warning for ONNX connectors SYSLIB1222) --------- Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- .../Demos/OllamaFunctionCalling/Program.cs | 14 +++-- .../Functions/KernelFunctionFromMethod.cs | 50 ++++++++++++---- .../KernelFunctionFromMethodTests1.cs | 60 +++++++++++++++++++ 3 files changed, 105 insertions(+), 19 deletions(-) diff --git a/dotnet/samples/Demos/OllamaFunctionCalling/Program.cs b/dotnet/samples/Demos/OllamaFunctionCalling/Program.cs index cb1a801ec204..3d52b6ea45c1 100644 --- a/dotnet/samples/Demos/OllamaFunctionCalling/Program.cs +++ b/dotnet/samples/Demos/OllamaFunctionCalling/Program.cs @@ -21,12 +21,14 @@ var chatCompletionService = kernel.GetRequiredService(); var settings = new OllamaPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; -Console.WriteLine("Ask questions or give instructions to the copilot such as:\n" + - "- Change the alarm to 8\n" + - "- What is the current alarm set?\n" + - "- Is the light on?\n" + - "- Turn the light off please.\n" + - "- Set an alarm for 6:00 am.\n"); +Console.WriteLine(""" + Ask questions or give instructions to the copilot such as: + - Change the alarm to 8 + - What is the current alarm set? + - Is the light on? + - Turn the light off please. + - Set an alarm for 6:00 am. + """); Console.Write("> "); diff --git a/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs b/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs index 6afbcef6ec3a..80e9652519b6 100644 --- a/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs +++ b/dotnet/src/SemanticKernel.Core/Functions/KernelFunctionFromMethod.cs @@ -29,6 +29,22 @@ namespace Microsoft.SemanticKernel; [DebuggerDisplay("{DebuggerDisplay,nq}")] internal sealed partial class KernelFunctionFromMethod : KernelFunction { + private static readonly Dictionary> s_jsonStringParsers = new(12) + { + { typeof(bool), s => bool.Parse(s) }, + { typeof(int), s => int.Parse(s) }, + { typeof(uint), s => uint.Parse(s) }, + { typeof(long), s => long.Parse(s) }, + { typeof(ulong), s => ulong.Parse(s) }, + { typeof(float), s => float.Parse(s) }, + { typeof(double), s => double.Parse(s) }, + { typeof(decimal), s => decimal.Parse(s) }, + { typeof(short), s => short.Parse(s) }, + { typeof(ushort), s => ushort.Parse(s) }, + { typeof(byte), s => byte.Parse(s) }, + { typeof(sbyte), s => sbyte.Parse(s) } + }; + /// /// Creates a instance for a method, specified via an instance /// and an optional target object if the method is an instance method. @@ -710,26 +726,34 @@ private static (Func((long)1), //Passed to parameter of type long + ["i"] = JsonSerializer.Deserialize((byte)1), //Passed to parameter of type int + ["d"] = JsonSerializer.Deserialize((float)1.0), //Passed to parameter of type double + ["f"] = JsonSerializer.Deserialize((uint)1.0), //Passed to parameter of type float + ["g"] = JsonSerializer.Deserialize(JsonSerializer.Serialize(new Guid("35626209-b0ab-458c-bfc4-43e6c7bd13dc"))), //Passed to parameter of type string + ["dof"] = JsonSerializer.Deserialize(JsonSerializer.Serialize(DayOfWeek.Thursday)), //Passed to parameter of type int + ["b"] = JsonSerializer.Deserialize(JsonSerializer.Serialize("true")), //Passed to parameter of type bool + }; + + var function = KernelFunctionFactory.CreateFromMethod((long l, int i, double d, float f, string g, int dof, bool b) => + { + Assert.Equal(1, l); + Assert.Equal(1, i); + Assert.Equal(1.0, d); + Assert.Equal("35626209-b0ab-458c-bfc4-43e6c7bd13dc", g); + Assert.Equal(4, dof); + Assert.True(b); + }, + functionName: "Test"); + + await function.InvokeAsync(this._kernel, arguments); + await function.AsAIFunction().InvokeAsync(arguments); + } + + [Fact] + public async Task ItSupportsStringJsonElementArgumentsImplicitConversionAsync() + { + //Arrange + var arguments = new KernelArguments() + { + ["l"] = JsonSerializer.Deserialize(JsonSerializer.Serialize("1")), //Passed to parameter of type long + ["i"] = JsonSerializer.Deserialize(JsonSerializer.Serialize("1")), //Passed to parameter of type int + ["d"] = JsonSerializer.Deserialize(JsonSerializer.Serialize("1.0")), //Passed to parameter of type double + ["f"] = JsonSerializer.Deserialize(JsonSerializer.Serialize("1.0")), //Passed to parameter of type float + ["g"] = JsonSerializer.Deserialize(JsonSerializer.Serialize("35626209-b0ab-458c-bfc4-43e6c7bd13dc")), //Passed to parameter of type Guid + ["dof"] = JsonSerializer.Deserialize(JsonSerializer.Serialize("4")), //Passed to parameter of type int + ["b"] = JsonSerializer.Deserialize(JsonSerializer.Serialize("false")), //Passed to parameter of type bool + }; + + var function = KernelFunctionFactory.CreateFromMethod((long l, int i, double d, float f, Guid g, int dof, bool b) => + { + Assert.Equal(1, l); + Assert.Equal(1, i); + Assert.Equal(1.0, d); + Assert.Equal(new Guid("35626209-b0ab-458c-bfc4-43e6c7bd13dc"), g); + Assert.Equal(4, dof); + Assert.False(b); + }, + functionName: "Test"); + + await function.InvokeAsync(this._kernel, arguments); + await function.AsAIFunction().InvokeAsync(arguments); + } + [Fact] public async Task ItSupportsParametersWithDefaultValuesAsync() {