From 9bf7faed05eff3e9bb6d02fbff372bcd73f9ab18 Mon Sep 17 00:00:00 2001 From: ioshm <47588077+ioshm@users.noreply.github.com> Date: Wed, 17 Sep 2025 19:05:48 +0400 Subject: [PATCH 1/3] .Net: Add `RetainArgumentTypes` support to `FunctionCallContentBuilder` Before this patch, the `FunctionCallContentBuilder` did not support retaining the argument types. We need this to support retained argument types when not using auto invoke function choices. --- .../Contents/FunctionCallContentBuilder.cs | 9 ++-- .../FunctionCallContentBuilderTests.cs | 54 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs index ff4f6af246e5..3d0d83195f84 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs @@ -18,7 +18,8 @@ public sealed class FunctionCallContentBuilder private Dictionary? _functionCallIdsByIndex = null; private Dictionary? _functionNamesByIndex = null; private Dictionary? _functionArgumentBuildersByIndex = null; - private readonly JsonSerializerOptions? _jsonSerializerOptions; + private readonly JsonSerializerOptions? _jsonSerializerOptions = null; + private readonly bool _retainArgumentTypes = false; /// /// Creates a new instance of the class. @@ -33,10 +34,12 @@ public FunctionCallContentBuilder() /// Creates a new instance of the class. /// /// The to use for deserializing function arguments. + /// A value indicating whether the types of function arguments provided by the AI model are retained by SK or not. By default . [Experimental("SKEXP0120")] - public FunctionCallContentBuilder(JsonSerializerOptions jsonSerializerOptions) + public FunctionCallContentBuilder(JsonSerializerOptions? jsonSerializerOptions = null, bool retainArgumentTypes = false) { this._jsonSerializerOptions = jsonSerializerOptions; + this._retainArgumentTypes = retainArgumentTypes; } /// @@ -146,7 +149,7 @@ public IReadOnlyList Build() arguments = JsonSerializer.Deserialize(argumentsString); } - if (arguments is { Count: > 0 }) + if (arguments is { Count: > 0 } && !this._retainArgumentTypes) { var names = arguments.Names.ToArray(); foreach (var name in names) diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallBuilder/FunctionCallContentBuilderTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallBuilder/FunctionCallContentBuilderTests.cs index e214478ce657..02dcb2c7e0f3 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallBuilder/FunctionCallContentBuilderTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallBuilder/FunctionCallContentBuilderTests.cs @@ -212,6 +212,60 @@ public void ItShouldCaptureArgumentsDeserializationException(JsonSerializerOptio Assert.NotNull(functionCall.Exception); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ItShouldRetainArgumentTypesIfSpecified(bool retain) + { + // Arrange + var sut = new FunctionCallContentBuilder(null, retainArgumentTypes: retain); + + // Act + var update1 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 1, functionCallIndex: 2, callId: "f_101", name: null, arguments: null); + sut.Append(update1); + + var update2 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 1, functionCallIndex: 2, callId: null, name: "WeatherUtils-GetTemperature", arguments: null); + sut.Append(update2); + + var update3 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 1, functionCallIndex: 2, callId: null, name: null, arguments: "{\"city\":"); + sut.Append(update3); + + var update4 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 1, functionCallIndex: 2, callId: null, name: null, arguments: "\"Seattle\","); + sut.Append(update4); + + var update5 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 1, functionCallIndex: 2, callId: null, name: null, arguments: "\"temperature\":"); + sut.Append(update5); + + var update6 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 1, functionCallIndex: 2, callId: null, name: null, arguments: "20}"); + sut.Append(update6); + + var functionCalls = sut.Build(); + + // Assert + var functionCall = Assert.Single(functionCalls); + + Assert.Equal("f_101", functionCall.Id); + Assert.Equal("WeatherUtils", functionCall.PluginName); + Assert.Equal("GetTemperature", functionCall.FunctionName); + Assert.NotNull(functionCall.Arguments); + + if (retain) + { + var city = Assert.IsType(functionCall.Arguments?["city"]); + Assert.Equal(JsonValueKind.String, city.ValueKind); + Assert.Equal("Seattle", city.GetString()); + + var temperature = Assert.IsType(functionCall.Arguments?["temperature"]); + Assert.Equal(JsonValueKind.Number, temperature.ValueKind); + Assert.Equal(20, temperature.GetInt32()); + } + else + { + Assert.Equal("Seattle", functionCall.Arguments?["city"]); + Assert.Equal("20", functionCall.Arguments?["temperature"]); + } + } + private static StreamingChatMessageContent CreateStreamingContentWithFunctionCallUpdate(int choiceIndex, int functionCallIndex, string? callId, string? name, string? arguments, int requestIndex = 0) { var content = new StreamingChatMessageContent(AuthorRole.Assistant, null); From 7a6061a353bbbc8a5979d9b82dd076bcdb8f6ad7 Mon Sep 17 00:00:00 2001 From: ioshm <47588077+ioshm@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:13:17 +0400 Subject: [PATCH 2/3] Add `CompatibilitySuppressions.xml` file --- .../CompatibilitySuppressions.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 dotnet/src/SemanticKernel.Abstractions/CompatibilitySuppressions.xml diff --git a/dotnet/src/SemanticKernel.Abstractions/CompatibilitySuppressions.xml b/dotnet/src/SemanticKernel.Abstractions/CompatibilitySuppressions.xml new file mode 100644 index 000000000000..775507df300d --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/CompatibilitySuppressions.xml @@ -0,0 +1,18 @@ + + + + + CP0002 + M:Microsoft.SemanticKernel.FunctionCallContentBuilder.#ctor(System.Text.Json.JsonSerializerOptions) + lib/net8.0/Microsoft.SemanticKernel.Abstractions.dll + lib/net8.0/Microsoft.SemanticKernel.Abstractions.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.FunctionCallContentBuilder.#ctor(System.Text.Json.JsonSerializerOptions) + lib/netstandard2.0/Microsoft.SemanticKernel.Abstractions.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Abstractions.dll + true + + \ No newline at end of file From e08e4d1dc55ab76af65e30480541e46ff67842b0 Mon Sep 17 00:00:00 2001 From: ioshm <47588077+ioshm@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:40:32 +0400 Subject: [PATCH 3/3] Fix race condition in `FunctionUsageMetricsAreCapturedByTelemetryAsExpected` --- .../Connectors.AzureOpenAI.UnitTests/KernelCore/KernelTests.cs | 3 ++- .../Connectors.OpenAI.UnitTests/KernelCore/KernelTests.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/KernelCore/KernelTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/KernelCore/KernelTests.cs index 433a5c5897db..d77c11a43aac 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/KernelCore/KernelTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/KernelCore/KernelTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.Metrics; using System.Net.Http; @@ -63,7 +64,7 @@ public async Task FunctionUsageMetricsAreCapturedByTelemetryAsExpected() using MeterListener listener = new(); var isPublished = false; - var measurements = new Dictionary> + var measurements = new Dictionary> { ["semantic_kernel.function.invocation.token_usage.prompt"] = [], ["semantic_kernel.function.invocation.token_usage.completion"] = [], diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/KernelCore/KernelTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/KernelCore/KernelTests.cs index 621f5124c8ab..a706ddd7e326 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/KernelCore/KernelTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/KernelCore/KernelTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.Metrics; using System.Net.Http; @@ -64,7 +65,7 @@ public async Task FunctionUsageMetricsAreCapturedByTelemetryAsExpected() using MeterListener listener = new(); var isPublished = false; - var measurements = new Dictionary> + var measurements = new Dictionary> { ["semantic_kernel.function.invocation.token_usage.prompt"] = [], ["semantic_kernel.function.invocation.token_usage.completion"] = [],