From 9858a75e678bd3fcebc502a07c4e4d4a9334a6ca Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Sat, 19 Apr 2025 17:28:49 +0100
Subject: [PATCH 01/24] Created potential extensible
FunctionInvokingChatClientV2
---
.../FunctionInvokingChatClientV2.Extra.cs | 123 ++
.../FunctionInvokingChatClientV2.cs | 886 ++++++++++++++
.../KernelFunctionInvokingChatClient.cs | 754 +-----------
.../KernelFunctionInvokingChatClientOld.cs | 1023 +++++++++++++++++
4 files changed, 2077 insertions(+), 709 deletions(-)
create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs
create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClientOld.cs
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs
new file mode 100644
index 000000000000..12edbbb4d15e
--- /dev/null
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs
@@ -0,0 +1,123 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// Intended for deletion
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace Microsoft.Extensions.AI;
+
+public partial class FunctionInvokingChatClientV2 : DelegatingChatClient
+{
+ private static class Throw
+ {
+ ///
+ /// Throws an if the specified argument is .
+ ///
+ /// Argument type to be checked for .
+ /// Object to be checked for .
+ /// The name of the parameter being checked.
+ /// The original value of .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [return: NotNull]
+ public static T IfNull([NotNull] T argument, [CallerArgumentExpression(nameof(argument))] string paramName = "")
+ {
+ if (argument is null)
+ {
+ throw new ArgumentNullException(paramName);
+ }
+
+ return argument;
+ }
+
+ ///
+ /// Throws an .
+ ///
+ /// A message that describes the error.
+#if !NET6_0_OR_GREATER
+ [MethodImpl(MethodImplOptions.NoInlining)]
+#endif
+ [DoesNotReturn]
+ public static void InvalidOperationException(string message)
+ => throw new InvalidOperationException(message);
+
+ ///
+ /// Throws an .
+ ///
+ /// The name of the parameter that caused the exception.
+#if !NET6_0_OR_GREATER
+ [MethodImpl(MethodImplOptions.NoInlining)]
+#endif
+ [DoesNotReturn]
+ public static void ArgumentOutOfRangeException(string paramName)
+ => throw new ArgumentOutOfRangeException(paramName);
+
+ ///
+ /// Throws an .
+ ///
+ /// The name of the parameter that caused the exception.
+ /// A message that describes the error.
+#if !NET6_0_OR_GREATER
+ [MethodImpl(MethodImplOptions.NoInlining)]
+#endif
+ [DoesNotReturn]
+ public static void ArgumentOutOfRangeException(string paramName, string? message)
+ => throw new ArgumentOutOfRangeException(paramName, message);
+
+ ///
+ /// Throws an .
+ ///
+ /// The name of the parameter that caused the exception.
+ /// The value of the argument that caused this exception.
+ /// A message that describes the error.
+#if !NET6_0_OR_GREATER
+ [MethodImpl(MethodImplOptions.NoInlining)]
+#endif
+ [DoesNotReturn]
+ public static void ArgumentOutOfRangeException(string paramName, object? actualValue, string? message)
+ => throw new ArgumentOutOfRangeException(paramName, actualValue, message);
+
+ ///
+ /// Throws an if the specified number is less than min.
+ ///
+ /// Number to be expected being less than min.
+ /// The number that must be less than the argument.
+ /// The name of the parameter being checked.
+ /// The original value of .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int IfLessThan(int argument, int min, [CallerArgumentExpression(nameof(argument))] string paramName = "")
+ {
+ if (argument < min)
+ {
+ ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}");
+ }
+
+ return argument;
+ }
+ }
+
+ private static class LoggingHelpers
+ {
+ /// Serializes as JSON for logging purposes.
+ public static string AsJson(T value, System.Text.Json.JsonSerializerOptions? options)
+ {
+ if (options?.TryGetTypeInfo(typeof(T), out var typeInfo) is true ||
+ AIJsonUtilities.DefaultOptions.TryGetTypeInfo(typeof(T), out typeInfo))
+ {
+ try
+ {
+ return System.Text.Json.JsonSerializer.Serialize(value, typeInfo);
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch
+#pragma warning restore CA1031 // Do not catch general exception types
+ {
+ }
+ }
+
+ return "{}";
+ }
+ }
+}
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
new file mode 100644
index 000000000000..f2f3d9f12a62
--- /dev/null
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
@@ -0,0 +1,886 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+#pragma warning disable CA2213 // Disposable fields should be disposed
+#pragma warning disable EA0002 // Use 'System.TimeProvider' to make the code easier to test
+#pragma warning disable SA1202 // 'protected' members should come before 'private' members
+#pragma warning disable VSTHRD111 // Use ConfigureAwait(bool)
+#pragma warning disable CA2007 // Use ConfigureAwait
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// A delegating chat client that invokes functions defined on .
+/// Include this in a chat pipeline to resolve function calls automatically.
+///
+///
+///
+/// When this client receives a in a chat response, it responds
+/// by calling the corresponding defined in ,
+/// producing a that it sends back to the inner client. This loop
+/// is repeated until there are no more function calls to make, or until another stop condition is met,
+/// such as hitting .
+///
+///
+/// The provided implementation of is thread-safe for concurrent use so long as the
+/// instances employed as part of the supplied are also safe.
+/// The property can be used to control whether multiple function invocation
+/// requests as part of the same request are invocable concurrently, but even with that set to
+/// (the default), multiple concurrent requests to this same instance and using the same tools could result in those
+/// tools being used concurrently (one per request). For example, a function that accesses the HttpContext of a specific
+/// ASP.NET web request should only be used as part of a single at a time, and only with
+/// set to , in case the inner client decided to issue multiple
+/// invocation requests to that same function.
+///
+///
+public partial class FunctionInvokingChatClientV2 : DelegatingChatClient
+{
+ /// The for the current function invocation.
+ private static readonly AsyncLocal _currentContext = new();
+
+ /// Optional services used for function invocation.
+ private readonly IServiceProvider? _functionInvocationServices;
+
+ /// The logger to use for logging information about function invocation.
+ protected readonly ILogger _logger;
+
+ /// The to use for telemetry.
+ /// This component does not own the instance and should not dispose it.
+ protected readonly ActivitySource? _activitySource;
+
+ /// Maximum number of roundtrips allowed to the inner client.
+ private int _maximumIterationsPerRequest = 10;
+
+ /// Maximum number of consecutive iterations that are allowed contain at least one exception result. If the limit is exceeded, we rethrow the exception instead of continuing.
+ private int _maximumConsecutiveErrorsPerRequest = 3;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The underlying , or the next instance in a chain of clients.
+ /// An to use for logging information about function invocation.
+ /// An optional to use for resolving services required by the instances being invoked.
+ public FunctionInvokingChatClientV2(IChatClient innerClient, ILoggerFactory? loggerFactory = null, IServiceProvider? functionInvocationServices = null)
+ : base(innerClient)
+ {
+ _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance;
+ _activitySource = innerClient.GetService();
+ _functionInvocationServices = functionInvocationServices;
+ }
+
+ ///
+ /// Gets or sets the for the current function invocation.
+ ///
+ ///
+ /// This value flows across async calls.
+ ///
+ public static FunctionInvocationContext? CurrentContext
+ {
+ get => _currentContext.Value;
+ protected set => _currentContext.Value = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether detailed exception information should be included
+ /// in the chat history when calling the underlying .
+ ///
+ ///
+ /// if the full exception message is added to the chat history
+ /// when calling the underlying .
+ /// if a generic error message is included in the chat history.
+ /// The default value is .
+ ///
+ ///
+ ///
+ /// Setting the value to prevents the underlying language model from disclosing
+ /// raw exception details to the end user, since it doesn't receive that information. Even in this
+ /// case, the raw object is available to application code by inspecting
+ /// the property.
+ ///
+ ///
+ /// Setting the value to can help the underlying bypass problems on
+ /// its own, for example by retrying the function call with different arguments. However it might
+ /// result in disclosing the raw exception information to external users, which can be a security
+ /// concern depending on the application scenario.
+ ///
+ ///
+ /// Changing the value of this property while the client is in use might result in inconsistencies
+ /// as to whether detailed errors are provided during an in-flight request.
+ ///
+ ///
+ public bool IncludeDetailedErrors { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to allow concurrent invocation of functions.
+ ///
+ ///
+ /// if multiple function calls can execute in parallel.
+ /// if function calls are processed serially.
+ /// The default value is .
+ ///
+ ///
+ /// An individual response from the inner client might contain multiple function call requests.
+ /// By default, such function calls are processed serially. Set to
+ /// to enable concurrent invocation such that multiple function calls can execute in parallel.
+ ///
+ public bool AllowConcurrentInvocation { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of iterations per request.
+ ///
+ ///
+ /// The maximum number of iterations per request.
+ /// The default value is 10.
+ ///
+ ///
+ ///
+ /// Each request to this might end up making
+ /// multiple requests to the inner client. Each time the inner client responds with
+ /// a function call request, this client might perform that invocation and send the results
+ /// back to the inner client in a new request. This property limits the number of times
+ /// such a roundtrip is performed. The value must be at least one, as it includes the initial request.
+ ///
+ ///
+ /// Changing the value of this property while the client is in use might result in inconsistencies
+ /// as to how many iterations are allowed for an in-flight request.
+ ///
+ ///
+ public int MaximumIterationsPerRequest
+ {
+ get => _maximumIterationsPerRequest;
+ set
+ {
+ if (value < 1)
+ {
+ Throw.ArgumentOutOfRangeException(nameof(value));
+ }
+
+ _maximumIterationsPerRequest = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum number of consecutive iterations that are allowed to fail with an error.
+ ///
+ ///
+ /// The maximum number of consecutive iterations that are allowed to fail with an error.
+ /// The default value is 3.
+ ///
+ ///
+ ///
+ /// When function invocations fail with an exception, the
+ /// continues to make requests to the inner client, optionally supplying exception information (as
+ /// controlled by ). This allows the to
+ /// recover from errors by trying other function parameters that may succeed.
+ ///
+ ///
+ /// However, in case function invocations continue to produce exceptions, this property can be used to
+ /// limit the number of consecutive failing attempts. When the limit is reached, the exception will be
+ /// rethrown to the caller.
+ ///
+ ///
+ /// If the value is set to zero, all function calling exceptions immediately terminate the function
+ /// invocation loop and the exception will be rethrown to the caller.
+ ///
+ ///
+ /// Changing the value of this property while the client is in use might result in inconsistencies
+ /// as to how many iterations are allowed for an in-flight request.
+ ///
+ ///
+ public int MaximumConsecutiveErrorsPerRequest
+ {
+ get => _maximumConsecutiveErrorsPerRequest;
+ set => _maximumConsecutiveErrorsPerRequest = Throw.IfLessThan(value, 0);
+ }
+
+ ///
+ public override async Task GetResponseAsync(
+ IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNull(messages);
+
+ // A single request into this GetResponseAsync may result in multiple requests to the inner client.
+ // Create an activity to group them together for better observability.
+ using Activity? activity = _activitySource?.StartActivity(nameof(FunctionInvokingChatClient));
+
+ // Copy the original messages in order to avoid enumerating the original messages multiple times.
+ // The IEnumerable can represent an arbitrary amount of work.
+ List originalMessages = [.. messages];
+ messages = originalMessages;
+
+ List? augmentedHistory = null; // the actual history of messages sent on turns other than the first
+ ChatResponse? response = null; // the response from the inner client, which is possibly modified and then eventually returned
+ List? responseMessages = null; // tracked list of messages, across multiple turns, to be used for the final response
+ UsageDetails? totalUsage = null; // tracked usage across all turns, to be used for the final response
+ List? functionCallContents = null; // function call contents that need responding to in the current turn
+ bool lastIterationHadThreadId = false; // whether the last iteration's response had a ChatThreadId set
+ int consecutiveErrorCount = 0;
+
+ for (int iteration = 0; ; iteration++)
+ {
+ functionCallContents?.Clear();
+
+ // Make the call to the inner client.
+ response = await base.GetResponseAsync(messages, options, cancellationToken);
+ if (response is null)
+ {
+ Throw.InvalidOperationException($"The inner {nameof(IChatClient)} returned a null {nameof(ChatResponse)}.");
+ }
+
+ // Any function call work to do? If yes, ensure we're tracking that work in functionCallContents.
+ bool requiresFunctionInvocation =
+ options?.Tools is { Count: > 0 } &&
+ iteration < MaximumIterationsPerRequest &&
+ CopyFunctionCalls(response.Messages, ref functionCallContents);
+
+ // In a common case where we make a request and there's no function calling work required,
+ // fast path out by just returning the original response.
+ if (iteration == 0 && !requiresFunctionInvocation)
+ {
+ return response;
+ }
+
+ // Track aggregatable details from the response, including all of the response messages and usage details.
+ (responseMessages ??= []).AddRange(response.Messages);
+ if (response.Usage is not null)
+ {
+ if (totalUsage is not null)
+ {
+ totalUsage.Add(response.Usage);
+ }
+ else
+ {
+ totalUsage = response.Usage;
+ }
+ }
+
+ // If there are no tools to call, or for any other reason we should stop, we're done.
+ // Break out of the loop and allow the handling at the end to configure the response
+ // with aggregated data from previous requests.
+ if (!requiresFunctionInvocation)
+ {
+ break;
+ }
+
+ // Prepare the history for the next iteration.
+ FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadThreadId);
+
+ // Add the responses from the function calls into the augmented history and also into the tracked
+ // list of response messages.
+ var modeAndMessages = await ProcessFunctionCallsAsync(response, augmentedHistory, options!, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: false, _functionInvocationServices, cancellationToken);
+ responseMessages.AddRange(modeAndMessages.MessagesAdded);
+ consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount;
+
+ if (modeAndMessages.ShouldTerminate)
+ {
+ break;
+ }
+
+ UpdateOptionsForNextIteration(ref options!, response.ChatThreadId);
+ }
+
+ Debug.Assert(responseMessages is not null, "Expected to only be here if we have response messages.");
+ response.Messages = responseMessages!;
+ response.Usage = totalUsage;
+
+ return response;
+ }
+
+ ///
+ public override async IAsyncEnumerable GetStreamingResponseAsync(
+ IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNull(messages);
+
+ // A single request into this GetStreamingResponseAsync may result in multiple requests to the inner client.
+ // Create an activity to group them together for better observability.
+ using Activity? activity = _activitySource?.StartActivity(nameof(FunctionInvokingChatClient));
+
+ // Copy the original messages in order to avoid enumerating the original messages multiple times.
+ // The IEnumerable can represent an arbitrary amount of work.
+ List originalMessages = [.. messages];
+ messages = originalMessages;
+
+ List? augmentedHistory = null; // the actual history of messages sent on turns other than the first
+ List? functionCallContents = null; // function call contents that need responding to in the current turn
+ List? responseMessages = null; // tracked list of messages, across multiple turns, to be used in fallback cases to reconstitute history
+ bool lastIterationHadThreadId = false; // whether the last iteration's response had a ChatThreadId set
+ List updates = []; // updates from the current response
+ int consecutiveErrorCount = 0;
+
+ for (int iteration = 0; ; iteration++)
+ {
+ updates.Clear();
+ functionCallContents?.Clear();
+
+ await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken))
+ {
+ if (update is null)
+ {
+ Throw.InvalidOperationException($"The inner {nameof(IChatClient)} streamed a null {nameof(ChatResponseUpdate)}.");
+ }
+
+ updates.Add(update);
+
+ _ = CopyFunctionCalls(update.Contents, ref functionCallContents);
+
+ yield return update;
+ Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
+ }
+
+ // If there are no tools to call, or for any other reason we should stop, return the response.
+ if (functionCallContents is not { Count: > 0 } ||
+ options?.Tools is not { Count: > 0 } ||
+ iteration >= _maximumIterationsPerRequest)
+ {
+ break;
+ }
+
+ // Reconsistitue a response from the response updates.
+ var response = updates.ToChatResponse();
+ (responseMessages ??= []).AddRange(response.Messages);
+
+ // Prepare the history for the next iteration.
+ FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadThreadId);
+
+ // Process all of the functions, adding their results into the history.
+ var modeAndMessages = await ProcessFunctionCallsAsync(response, augmentedHistory, options, functionCallContents, iteration, consecutiveErrorCount, isStreaming: true, _functionInvocationServices, cancellationToken);
+ responseMessages.AddRange(modeAndMessages.MessagesAdded);
+ consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount;
+
+ // This is a synthetic ID since we're generating the tool messages instead of getting them from
+ // the underlying provider. When emitting the streamed chunks, it's perfectly valid for us to
+ // use the same message ID for all of them within a given iteration, as this is a single logical
+ // message with multiple content items. We could also use different message IDs per tool content,
+ // but there's no benefit to doing so.
+ string toolResponseId = Guid.NewGuid().ToString("N");
+
+ // Stream any generated function results. This mirrors what's done for GetResponseAsync, where the returned messages
+ // includes all activitys, including generated function results.
+ foreach (var message in modeAndMessages.MessagesAdded)
+ {
+ var toolResultUpdate = new ChatResponseUpdate
+ {
+ AdditionalProperties = message.AdditionalProperties,
+ AuthorName = message.AuthorName,
+ ChatThreadId = response.ChatThreadId,
+ CreatedAt = DateTimeOffset.UtcNow,
+ Contents = message.Contents,
+ RawRepresentation = message.RawRepresentation,
+ ResponseId = toolResponseId,
+ MessageId = toolResponseId, // See above for why this can be the same as ResponseId
+ Role = message.Role,
+ };
+
+ yield return toolResultUpdate;
+ Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
+ }
+
+ if (modeAndMessages.ShouldTerminate)
+ {
+ yield break;
+ }
+
+ UpdateOptionsForNextIteration(ref options, response.ChatThreadId);
+ }
+ }
+
+ /// Prepares the various chat message lists after a response from the inner client and before invoking functions.
+ /// The original messages provided by the caller.
+ /// The messages reference passed to the inner client.
+ /// The augmented history containing all the messages to be sent.
+ /// The most recent response being handled.
+ /// A list of all response messages received up until this point.
+ /// Whether the previous iteration's response had a thread id.
+ private static void FixupHistories(
+ IEnumerable originalMessages,
+ ref IEnumerable messages,
+ [NotNull] ref List? augmentedHistory,
+ ChatResponse response,
+ List allTurnsResponseMessages,
+ ref bool lastIterationHadThreadId)
+ {
+ // We're now going to need to augment the history with function result contents.
+ // That means we need a separate list to store the augmented history.
+ if (response.ChatThreadId is not null)
+ {
+ // The response indicates the inner client is tracking the history, so we don't want to send
+ // anything we've already sent or received.
+ if (augmentedHistory is not null)
+ {
+ augmentedHistory.Clear();
+ }
+ else
+ {
+ augmentedHistory = [];
+ }
+
+ lastIterationHadThreadId = true;
+ }
+ else if (lastIterationHadThreadId)
+ {
+ // In the very rare case where the inner client returned a response with a thread ID but then
+ // returned a subsequent response without one, we want to reconstitue the full history. To do that,
+ // we can populate the history with the original chat messages and then all of the response
+ // messages up until this point, which includes the most recent ones.
+ augmentedHistory ??= [];
+ augmentedHistory.Clear();
+ augmentedHistory.AddRange(originalMessages);
+ augmentedHistory.AddRange(allTurnsResponseMessages);
+
+ lastIterationHadThreadId = false;
+ }
+ else
+ {
+ // If augmentedHistory is already non-null, then we've already populated it with everything up
+ // until this point (except for the most recent response). If it's null, we need to seed it with
+ // the chat history provided by the caller.
+ augmentedHistory ??= originalMessages.ToList();
+
+ // Now add the most recent response messages.
+ augmentedHistory.AddMessages(response);
+
+ lastIterationHadThreadId = false;
+ }
+
+ // Use the augmented history as the new set of messages to send.
+ messages = augmentedHistory;
+ }
+
+ /// Copies any from to .
+ private static bool CopyFunctionCalls(
+ IList messages, [NotNullWhen(true)] ref List? functionCalls)
+ {
+ bool any = false;
+ int count = messages.Count;
+ for (int i = 0; i < count; i++)
+ {
+ any |= CopyFunctionCalls(messages[i].Contents, ref functionCalls);
+ }
+
+ return any;
+ }
+
+ /// Copies any from to .
+ private static bool CopyFunctionCalls(
+ IList content, [NotNullWhen(true)] ref List? functionCalls)
+ {
+ bool any = false;
+ int count = content.Count;
+ for (int i = 0; i < count; i++)
+ {
+ if (content[i] is FunctionCallContent functionCall)
+ {
+ (functionCalls ??= []).Add(functionCall);
+ any = true;
+ }
+ }
+
+ return any;
+ }
+
+ private static void UpdateOptionsForNextIteration(ref ChatOptions options, string? chatThreadId)
+ {
+ if (options.ToolMode is RequiredChatToolMode)
+ {
+ // We have to reset the tool mode to be non-required after the first iteration,
+ // as otherwise we'll be in an infinite loop.
+ options = options.Clone();
+ options.ToolMode = null;
+ options.ChatThreadId = chatThreadId;
+ }
+ else if (options.ChatThreadId != chatThreadId)
+ {
+ // As with the other modes, ensure we've propagated the chat thread ID to the options.
+ // We only need to clone the options if we're actually mutating it.
+ options = options.Clone();
+ options.ChatThreadId = chatThreadId;
+ }
+ }
+
+ ///
+ /// Processes the function calls in the list.
+ ///
+ /// The response being processed.
+ /// The current chat contents, inclusive of the function call contents being processed.
+ /// The options used for the response being processed.
+ /// The function call contents representing the functions to be invoked.
+ /// The iteration number of how many roundtrips have been made to the inner client.
+ /// The number of consecutive iterations, prior to this one, that were recorded as having function invocation errors.
+ /// Whether the function calls are being processed in a streaming context.
+ /// The services to use for function invocation.
+ /// The to monitor for cancellation requests.
+ /// A value indicating how the caller should proceed.
+ protected virtual async Task<(bool ShouldTerminate, int NewConsecutiveErrorCount, IList MessagesAdded)> ProcessFunctionCallsAsync(
+ ChatResponse response, List messages, ChatOptions options, List functionCallContents, int iteration, int consecutiveErrorCount, bool isStreaming, IServiceProvider functionInvocationServices, CancellationToken cancellationToken)
+ {
+ // We must add a response for every tool call, regardless of whether we successfully executed it or not.
+ // If we successfully execute it, we'll add the result. If we don't, we'll add an error.
+
+ Debug.Assert(functionCallContents.Count > 0, "Expected at least one function call.");
+
+ var captureCurrentIterationExceptions = consecutiveErrorCount < _maximumConsecutiveErrorsPerRequest;
+
+ // Process all functions. If there's more than one and concurrent invocation is enabled, do so in parallel.
+ if (functionCallContents.Count == 1)
+ {
+ FunctionInvocationResult result = await ProcessFunctionCallAsync(
+ messages, options, functionCallContents, iteration, 0, captureCurrentIterationExceptions, isStreaming, functionInvocationServices, cancellationToken);
+
+ IList added = CreateResponseMessages([result]);
+ ThrowIfNoFunctionResultsAdded(added);
+ UpdateConsecutiveErrorCountOrThrow(added, ref consecutiveErrorCount);
+
+ messages.AddRange(added);
+ return (result.ShouldTerminate, consecutiveErrorCount, added);
+ }
+ else
+ {
+ FunctionInvocationResult[] results;
+
+ if (AllowConcurrentInvocation)
+ {
+ // Rather than await'ing each function before invoking the next, invoke all of them
+ // and then await all of them. We avoid forcibly introducing parallelism via Task.Run,
+ // but if a function invocation completes asynchronously, its processing can overlap
+ // with the processing of other the other invocation invocations.
+ results = await Task.WhenAll(
+ from i in Enumerable.Range(0, functionCallContents.Count)
+ select ProcessFunctionCallAsync(
+ messages, options, functionCallContents,
+ iteration, i, captureExceptions: true, isStreaming, functionInvocationServices, cancellationToken));
+ }
+ else
+ {
+ // Invoke each function serially.
+ results = new FunctionInvocationResult[functionCallContents.Count];
+ for (int i = 0; i < results.Length; i++)
+ {
+ results[i] = await ProcessFunctionCallAsync(
+ messages, options, functionCallContents,
+ iteration, i, captureCurrentIterationExceptions, isStreaming, functionInvocationServices, cancellationToken);
+ }
+ }
+
+ var shouldTerminate = false;
+
+ IList added = CreateResponseMessages(results);
+ ThrowIfNoFunctionResultsAdded(added);
+ UpdateConsecutiveErrorCountOrThrow(added, ref consecutiveErrorCount);
+
+ messages.AddRange(added);
+ foreach (FunctionInvocationResult fir in results)
+ {
+ shouldTerminate = shouldTerminate || fir.ShouldTerminate;
+ }
+
+ return (shouldTerminate, consecutiveErrorCount, added);
+ }
+ }
+
+ protected void UpdateConsecutiveErrorCountOrThrow(IList added, ref int consecutiveErrorCount)
+ {
+ var allExceptions = added.SelectMany(m => m.Contents.OfType())
+ .Select(frc => frc.Exception!)
+ .Where(e => e is not null);
+
+ if (allExceptions.Any())
+ {
+ consecutiveErrorCount++;
+ if (consecutiveErrorCount > _maximumConsecutiveErrorsPerRequest)
+ {
+ var allExceptionsArray = allExceptions.ToArray();
+ if (allExceptionsArray.Length == 1)
+ {
+ ExceptionDispatchInfo.Capture(allExceptionsArray[0]).Throw();
+ }
+ else
+ {
+ throw new AggregateException(allExceptionsArray);
+ }
+ }
+ }
+ else
+ {
+ consecutiveErrorCount = 0;
+ }
+ }
+
+ ///
+ /// Throws an exception if doesn't create any messages.
+ ///
+ protected void ThrowIfNoFunctionResultsAdded(IList? messages)
+ {
+ if (messages is null || messages.Count == 0)
+ {
+ Throw.InvalidOperationException($"{GetType().Name}.{nameof(CreateResponseMessages)} returned null or an empty collection of messages.");
+ }
+ }
+
+ /// Processes the function call described in [ ].
+ /// The current chat contents, inclusive of the function call contents being processed.
+ /// The options used for the response being processed.
+ /// The function call contents representing all the functions being invoked.
+ /// The iteration number of how many roundtrips have been made to the inner client.
+ /// The 0-based index of the function being called out of .
+ /// If true, handles function-invocation exceptions by returning a value with . Otherwise, rethrows.
+ /// Whether the function calls are being processed in a streaming context.
+ /// The services to use for function invocation.
+ /// The to monitor for cancellation requests.
+ /// A value indicating how the caller should proceed.
+ protected virtual async Task ProcessFunctionCallAsync(
+ List messages, ChatOptions options, List callContents,
+ int iteration, int functionCallIndex, bool captureExceptions, bool isStreaming, IServiceProvider functionInvocationServices, CancellationToken cancellationToken)
+ {
+ var callContent = callContents[functionCallIndex];
+
+ // Look up the AIFunction for the function call. If the requested function isn't available, send back an error.
+ AIFunction? function = options.Tools!.OfType().FirstOrDefault(t => t.Name == callContent.Name);
+ if (function is null)
+ {
+ return new(shouldTerminate: false, FunctionInvocationStatus.NotFound, callContent, result: null, exception: null);
+ }
+
+ FunctionInvocationContext context = new()
+ {
+ Function = function,
+ Arguments = new(callContent.Arguments) { Services = functionInvocationServices },
+
+ Messages = messages,
+ Options = options,
+
+ CallContent = callContent,
+ Iteration = iteration,
+ FunctionCallIndex = functionCallIndex,
+ FunctionCount = callContents.Count,
+ };
+
+ object? result;
+ try
+ {
+ result = await InvokeFunctionAsync(context, cancellationToken);
+ }
+ catch (Exception e) when (!cancellationToken.IsCancellationRequested)
+ {
+ if (!captureExceptions)
+ {
+ throw;
+ }
+
+ return new(
+ shouldTerminate: false,
+ FunctionInvocationStatus.Exception,
+ callContent,
+ result: null,
+ exception: e);
+ }
+
+ return new(
+ shouldTerminate: context.Terminate,
+ FunctionInvocationStatus.RanToCompletion,
+ callContent,
+ result,
+ exception: null);
+ }
+
+ /// Creates one or more response messages for function invocation results.
+ /// Information about the function call invocations and results.
+ /// A list of all chat messages created from .
+ protected virtual IList CreateResponseMessages(
+ ReadOnlySpan results)
+ {
+ var contents = new List(results.Length);
+ for (int i = 0; i < results.Length; i++)
+ {
+ contents.Add(CreateFunctionResultContent(results[i]));
+ }
+
+ return [new(ChatRole.Tool, contents)];
+
+ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult result)
+ {
+ _ = Throw.IfNull(result);
+
+ object? functionResult;
+ if (result.Status == FunctionInvocationStatus.RanToCompletion)
+ {
+ functionResult = result.Result ?? "Success: Function completed.";
+ }
+ else
+ {
+ string message = result.Status switch
+ {
+ FunctionInvocationStatus.NotFound => $"Error: Requested function \"{result.CallContent.Name}\" not found.",
+ FunctionInvocationStatus.Exception => "Error: Function failed.",
+ _ => "Error: Unknown error.",
+ };
+
+ if (IncludeDetailedErrors && result.Exception is not null)
+ {
+ message = $"{message} Exception: {result.Exception.Message}";
+ }
+
+ functionResult = message;
+ }
+
+ return new FunctionResultContent(result.CallContent.CallId, functionResult) { Exception = result.Exception };
+ }
+ }
+
+ /// Invokes the function asynchronously.
+ ///
+ /// The function invocation context detailing the function to be invoked and its arguments along with additional request information.
+ ///
+ /// The to monitor for cancellation requests. The default is .
+ /// The result of the function invocation, or if the function invocation returned .
+ /// is .
+ protected virtual async Task InvokeFunctionAsync(FunctionInvocationContext context, CancellationToken cancellationToken)
+ {
+ _ = Throw.IfNull(context);
+
+ using Activity? activity = _activitySource?.StartActivity(context.Function.Name);
+
+ long startingTimestamp = 0;
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ startingTimestamp = Stopwatch.GetTimestamp();
+ if (_logger.IsEnabled(LogLevel.Trace))
+ {
+ LogInvokingSensitive(context.Function.Name, LoggingHelpers.AsJson(context.Arguments, context.Function.JsonSerializerOptions));
+ }
+ else
+ {
+ LogInvoking(context.Function.Name);
+ }
+ }
+
+ object? result = null;
+ try
+ {
+ CurrentContext = context; // doesn't need to be explicitly reset after, as that's handled automatically at async method exit
+ result = await TryInvokeFunctionAsync(context, cancellationToken);
+ }
+ catch (Exception e)
+ {
+ if (activity is not null)
+ {
+ _ = activity.SetTag("error.type", e.GetType().FullName)
+ .SetStatus(ActivityStatusCode.Error, e.Message);
+ }
+
+ if (e is OperationCanceledException)
+ {
+ LogInvocationCanceled(context.Function.Name);
+ }
+ else
+ {
+ LogInvocationFailed(context.Function.Name, e);
+ }
+
+ throw;
+ }
+ finally
+ {
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ TimeSpan elapsed = GetElapsedTime(startingTimestamp);
+
+ if (result is not null && _logger.IsEnabled(LogLevel.Trace))
+ {
+ LogInvocationCompletedSensitive(context.Function.Name, elapsed, LoggingHelpers.AsJson(result, context.Function.JsonSerializerOptions));
+ }
+ else
+ {
+ LogInvocationCompleted(context.Function.Name, elapsed);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ protected virtual async Task<(FunctionInvocationContext Context, object? Result)> TryInvokeFunctionAsync(FunctionInvocationContext context, CancellationToken cancellationToken)
+ {
+ var result = await context.Function.InvokeAsync(context.Arguments, cancellationToken);
+
+ return (context, result);
+ }
+
+ private static TimeSpan GetElapsedTime(long startingTimestamp) =>
+#if NET
+ Stopwatch.GetElapsedTime(startingTimestamp);
+#else
+ new((long)((Stopwatch.GetTimestamp() - startingTimestamp) * ((double)TimeSpan.TicksPerSecond / Stopwatch.Frequency)));
+#endif
+
+ [LoggerMessage(LogLevel.Debug, "Invoking {MethodName}.", SkipEnabledCheck = true)]
+ private partial void LogInvoking(string methodName);
+
+ [LoggerMessage(LogLevel.Trace, "Invoking {MethodName}({Arguments}).", SkipEnabledCheck = true)]
+ private partial void LogInvokingSensitive(string methodName, string arguments);
+
+ [LoggerMessage(LogLevel.Debug, "{MethodName} invocation completed. Duration: {Duration}", SkipEnabledCheck = true)]
+ private partial void LogInvocationCompleted(string methodName, TimeSpan duration);
+
+ [LoggerMessage(LogLevel.Trace, "{MethodName} invocation completed. Duration: {Duration}. Result: {Result}", SkipEnabledCheck = true)]
+ private partial void LogInvocationCompletedSensitive(string methodName, TimeSpan duration, string result);
+
+ [LoggerMessage(LogLevel.Debug, "{MethodName} invocation canceled.")]
+ private partial void LogInvocationCanceled(string methodName);
+
+ [LoggerMessage(LogLevel.Error, "{MethodName} invocation failed.")]
+ private partial void LogInvocationFailed(string methodName, Exception error);
+
+ /// Provides information about the invocation of a function call.
+ public sealed class FunctionInvocationResult
+ {
+ internal FunctionInvocationResult(bool shouldTerminate, FunctionInvocationStatus status, FunctionCallContent callContent, object? result, Exception? exception)
+ {
+ ShouldTerminate = shouldTerminate;
+ Status = status;
+ CallContent = callContent;
+ Result = result;
+ Exception = exception;
+ }
+
+ /// Gets status about how the function invocation completed.
+ public FunctionInvocationStatus Status { get; }
+
+ /// Gets the function call content information associated with this invocation.
+ public FunctionCallContent CallContent { get; }
+
+ /// Gets the result of the function call.
+ public object? Result { get; }
+
+ /// Gets any exception the function call threw.
+ public Exception? Exception { get; }
+
+ /// Gets a value indicating whether the caller should terminate the processing loop.
+ internal bool ShouldTerminate { get; }
+ }
+
+ /// Provides error codes for when errors occur as part of the function calling loop.
+ public enum FunctionInvocationStatus
+ {
+ /// The operation completed successfully.
+ RanToCompletion,
+
+ /// The requested function could not be found.
+ NotFound,
+
+ /// The function call failed with an exception.
+ Exception,
+ }
+}
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
index ea2dce48fc62..fc4968a390ed 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
@@ -4,15 +4,10 @@
#pragma warning restore IDE0073 // The file header does not match the required text
using System.Collections.Generic;
using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Runtime.ExceptionServices;
-using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
@@ -51,469 +46,12 @@ namespace Microsoft.Extensions.AI;
/// invocation requests to that same function.
///
///
-public partial class KernelFunctionInvokingChatClient : DelegatingChatClient
+public partial class KernelFunctionInvokingChatClient : FunctionInvokingChatClientV2
{
- /// The for the current function invocation.
- private static readonly AsyncLocal _currentContext = new();
-
- /// Optional services used for function invocation.
- private readonly IServiceProvider? _functionInvocationServices;
-
- /// The logger to use for logging information about function invocation.
- private readonly ILogger _logger;
-
- /// The to use for telemetry.
- /// This component does not own the instance and should not dispose it.
- private readonly ActivitySource? _activitySource;
-
- /// Maximum number of roundtrips allowed to the inner client.
- private int _maximumIterationsPerRequest = 10;
-
- /// Maximum number of consecutive iterations that are allowed contain at least one exception result. If the limit is exceeded, we rethrow the exception instead of continuing.
- private int _maximumConsecutiveErrorsPerRequest = 3;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The underlying , or the next instance in a chain of clients.
- /// An to use for logging information about function invocation.
- /// An optional to use for resolving services required by the instances being invoked.
- public KernelFunctionInvokingChatClient(IChatClient innerClient, ILoggerFactory? loggerFactory = null, IServiceProvider? functionInvocationServices = null)
- : base(innerClient)
- {
- _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance;
- _activitySource = innerClient.GetService();
- _functionInvocationServices = functionInvocationServices;
- }
-
- ///
- /// Gets or sets the for the current function invocation.
- ///
- ///
- /// This value flows across async calls.
- ///
- public static AutoFunctionInvocationContext? CurrentContext
- {
- get => _currentContext.Value;
- protected set => _currentContext.Value = value;
- }
-
- ///
- /// Gets or sets a value indicating whether detailed exception information should be included
- /// in the chat history when calling the underlying .
- ///
- ///
- /// if the full exception message is added to the chat history
- /// when calling the underlying .
- /// if a generic error message is included in the chat history.
- /// The default value is .
- ///
- ///
- ///
- /// Setting the value to prevents the underlying language model from disclosing
- /// raw exception details to the end user, since it doesn't receive that information. Even in this
- /// case, the raw object is available to application code by inspecting
- /// the property.
- ///
- ///
- /// Setting the value to can help the underlying bypass problems on
- /// its own, for example by retrying the function call with different arguments. However, it might
- /// result in disclosing the raw exception information to external users, which can be a security
- /// concern depending on the application scenario.
- ///
- ///
- /// Changing the value of this property while the client is in use might result in inconsistencies
- /// whether detailed errors are provided during an in-flight request.
- ///
- ///
- public bool IncludeDetailedErrors { get; set; }
-
- ///
- /// Gets or sets a value indicating whether to allow concurrent invocation of functions.
- ///
- ///
- /// if multiple function calls can execute in parallel.
- /// if function calls are processed serially.
- /// The default value is .
- ///
- ///
- /// An individual response from the inner client might contain multiple function call requests.
- /// By default, such function calls are processed serially. Set to
- /// to enable concurrent invocation such that multiple function calls can execute in parallel.
- ///
- public bool AllowConcurrentInvocation { get; set; }
-
- ///
- /// Gets or sets the maximum number of iterations per request.
- ///
- ///
- /// The maximum number of iterations per request.
- /// The default value is 10.
- ///
- ///
- ///
- /// Each request to this might end up making
- /// multiple requests to the inner client. Each time the inner client responds with
- /// a function call request, this client might perform that invocation and send the results
- /// back to the inner client in a new request. This property limits the number of times
- /// such a roundtrip is performed. The value must be at least one, as it includes the initial request.
- ///
- ///
- /// Changing the value of this property while the client is in use might result in inconsistencies
- /// as to how many iterations are allowed for an in-flight request.
- ///
- ///
- public int MaximumIterationsPerRequest
- {
- get => _maximumIterationsPerRequest;
- set
- {
- if (value < 1)
- {
- throw new ArgumentOutOfRangeException(nameof(value));
- }
-
- _maximumIterationsPerRequest = value;
- }
- }
-
- ///
- /// Gets or sets the maximum number of consecutive iterations that are allowed to fail with an error.
- ///
- ///
- /// The maximum number of consecutive iterations that are allowed to fail with an error.
- /// The default value is 3.
- ///
- ///
- ///
- /// When function invocations fail with an exception, the
- /// continues to make requests to the inner client, optionally supplying exception information (as
- /// controlled by ). This allows the to
- /// recover from errors by trying other function parameters that may succeed.
- ///
- ///
- /// However, in case function invocations continue to produce exceptions, this property can be used to
- /// limit the number of consecutive failing attempts. When the limit is reached, the exception will be
- /// rethrown to the caller.
- ///
- ///
- /// If the value is set to zero, all function calling exceptions immediately terminate the function
- /// invocation loop and the exception will be rethrown to the caller.
- ///
- ///
- /// Changing the value of this property while the client is in use might result in inconsistencies
- /// as to how many iterations are allowed for an in-flight request.
- ///
- ///
- public int MaximumConsecutiveErrorsPerRequest
- {
- get => _maximumConsecutiveErrorsPerRequest;
- set
- {
- if (value < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(value), "Argument less than minimum value 0");
- }
- _maximumConsecutiveErrorsPerRequest = value;
- }
- }
-
///
- public override async Task GetResponseAsync(
- IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
- {
- Verify.NotNull(messages);
-
- // A single request into this GetResponseAsync may result in multiple requests to the inner client.
- // Create an activity to group them together for better observability.
- using Activity? activity = _activitySource?.StartActivity(nameof(KernelFunctionInvokingChatClient));
-
- // Copy the original messages in order to avoid enumerating the original messages multiple times.
- // The IEnumerable can represent an arbitrary amount of work.
- List originalMessages = [.. messages];
- messages = originalMessages;
-
- List? augmentedHistory = null; // the actual history of messages sent on turns other than the first
- ChatResponse? response = null; // the response from the inner client, which is possibly modified and then eventually returned
- List? responseMessages = null; // tracked list of messages, across multiple turns, to be used for the final response
- UsageDetails? totalUsage = null; // tracked usage across all turns, to be used for the final response
- List? functionCallContents = null; // function call contents that need responding to in the current turn
- bool lastIterationHadThreadId = false; // whether the last iteration's response had a ChatThreadId set
- int consecutiveErrorCount = 0;
-
- for (int iteration = 0; ; iteration++)
- {
- functionCallContents?.Clear();
-
- // Make the call to the inner client.
- response = await base.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
- if (response is null)
- {
- throw new InvalidOperationException($"The inner {nameof(IChatClient)} returned a null {nameof(ChatResponse)}.");
- }
-
- // Any function call work to do? If yes, ensure we're tracking that work in functionCallContents.
- bool requiresFunctionInvocation =
- options?.Tools is { Count: > 0 } &&
- iteration < MaximumIterationsPerRequest &&
- CopyFunctionCalls(response.Messages, ref functionCallContents);
-
- // In a common case where we make a request and there's no function calling work required,
- // fast path out by just returning the original response.
- if (iteration == 0 && !requiresFunctionInvocation)
- {
- return response;
- }
-
- // Track aggregate details from the response, including all the response messages and usage details.
- (responseMessages ??= []).AddRange(response.Messages);
- if (response.Usage is not null)
- {
- if (totalUsage is not null)
- {
- totalUsage.Add(response.Usage);
- }
- else
- {
- totalUsage = response.Usage;
- }
- }
-
- // If there are no tools to call, or for any other reason we should stop, we're done.
- // Break out of the loop and allow the handling at the end to configure the response
- // with aggregated data from previous requests.
- if (!requiresFunctionInvocation)
- {
- break;
- }
-
- // Prepare the history for the next iteration.
- FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadThreadId);
-
- // Prepare the options for the next auto function invocation iteration.
- UpdateOptionsForAutoFunctionInvocation(ref options!, response.Messages.Last().ToChatMessageContent(), isStreaming: false);
-
- // Add the responses from the function calls into the augmented history and also into the tracked
- // list of response messages.
- var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options!, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: false, cancellationToken).ConfigureAwait(false);
- responseMessages.AddRange(modeAndMessages.MessagesAdded);
- consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount;
-
- if (modeAndMessages.ShouldTerminate)
- {
- break;
- }
-
- // Clear the auto function invocation options.
- ClearOptionsForAutoFunctionInvocation(ref options);
-
- UpdateOptionsForNextIteration(ref options!, response.ChatThreadId);
- }
-
- Debug.Assert(responseMessages is not null, "Expected to only be here if we have response messages.");
- response.Messages = responseMessages!;
- response.Usage = totalUsage;
-
- return response;
- }
-
- ///
- public override async IAsyncEnumerable GetStreamingResponseAsync(
- IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
- {
- Verify.NotNull(messages);
-
- // A single request into this GetStreamingResponseAsync may result in multiple requests to the inner client.
- // Create an activity to group them together for better observability.
- using Activity? activity = _activitySource?.StartActivity(nameof(FunctionInvokingChatClient));
-
- // Copy the original messages in order to avoid enumerating the original messages multiple times.
- // The IEnumerable can represent an arbitrary amount of work.
- List originalMessages = [.. messages];
- messages = originalMessages;
-
- List? augmentedHistory = null; // the actual history of messages sent on turns other than the first
- List? functionCallContents = null; // function call contents that need responding to in the current turn
- List? responseMessages = null; // tracked list of messages, across multiple turns, to be used in fallback cases to reconstitute history
- bool lastIterationHadThreadId = false; // whether the last iteration's response had a ChatThreadId set
- List updates = []; // updates from the current response
- int consecutiveErrorCount = 0;
-
- for (int iteration = 0; ; iteration++)
- {
- updates.Clear();
- functionCallContents?.Clear();
-
- await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false))
- {
- if (update is null)
- {
- throw new InvalidOperationException($"The inner {nameof(IChatClient)} streamed a null {nameof(ChatResponseUpdate)}.");
- }
-
- updates.Add(update);
-
- _ = CopyFunctionCalls(update.Contents, ref functionCallContents);
-
- yield return update;
- Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
- }
-
- // If there are no tools to call, or for any other reason we should stop, return the response.
- if (functionCallContents is not { Count: > 0 } ||
- options?.Tools is not { Count: > 0 } ||
- iteration >= _maximumIterationsPerRequest)
- {
- break;
- }
-
- // Reconstitute a response from the response updates.
- var response = updates.ToChatResponse();
- (responseMessages ??= []).AddRange(response.Messages);
-
- // Prepare the history for the next iteration.
- FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadThreadId);
-
- // Prepare the options for the next auto function invocation iteration.
- UpdateOptionsForAutoFunctionInvocation(ref options, response.Messages.Last().ToChatMessageContent(), isStreaming: true);
-
- // Process all the functions, adding their results into the history.
- var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, functionCallContents, iteration, consecutiveErrorCount, isStreaming: true, cancellationToken).ConfigureAwait(false);
- responseMessages.AddRange(modeAndMessages.MessagesAdded);
- consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount;
-
- // Clear the auto function invocation options.
- ClearOptionsForAutoFunctionInvocation(ref options);
-
- // This is a synthetic ID since we're generating the tool messages instead of getting them from
- // the underlying provider. When emitting the streamed chunks, it's perfectly valid for us to
- // use the same message ID for all of them within a given iteration, as this is a single logical
- // message with multiple content items. We could also use different message IDs per tool content,
- // but there's no benefit to doing so.
- string toolResponseId = Guid.NewGuid().ToString("N");
-
- // Stream any generated function results. This mirrors what's done for GetResponseAsync, where the returned messages
- // include all activity, including generated function results.
- foreach (var message in modeAndMessages.MessagesAdded)
- {
- var toolResultUpdate = new ChatResponseUpdate
- {
- AdditionalProperties = message.AdditionalProperties,
- AuthorName = message.AuthorName,
- ChatThreadId = response.ChatThreadId,
- CreatedAt = DateTimeOffset.UtcNow,
- Contents = message.Contents,
- RawRepresentation = message.RawRepresentation,
- ResponseId = toolResponseId,
- MessageId = toolResponseId, // See above for why this can be the same as ResponseId
- Role = message.Role,
- };
-
- yield return toolResultUpdate;
- Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
- }
-
- if (modeAndMessages.ShouldTerminate)
- {
- yield break;
- }
-
- UpdateOptionsForNextIteration(ref options, response.ChatThreadId);
- }
- }
-
- /// Prepares the various chat message lists after a response from the inner client and before invoking functions.
- /// The original messages provided by the caller.
- /// The messages reference passed to the inner client.
- /// The augmented history containing all the messages to be sent.
- /// The most recent response being handled.
- /// A list of all response messages received up until this point.
- /// Whether the previous iteration's response had a thread id.
- private static void FixupHistories(
- IEnumerable originalMessages,
- ref IEnumerable messages,
- [NotNull] ref List? augmentedHistory,
- ChatResponse response,
- List allTurnsResponseMessages,
- ref bool lastIterationHadThreadId)
- {
- // We're now going to need to augment the history with function result contents.
- // That means we need a separate list to store the augmented history.
- if (response.ChatThreadId is not null)
- {
- // The response indicates the inner client is tracking the history, so we don't want to send
- // anything we've already sent or received.
- if (augmentedHistory is not null)
- {
- augmentedHistory.Clear();
- }
- else
- {
- augmentedHistory = [];
- }
-
- lastIterationHadThreadId = true;
- }
- else if (lastIterationHadThreadId)
- {
- // In the very rare case where the inner client returned a response with a thread ID but then
- // returned a subsequent response without one, we want to reconstitute the full history. To do that,
- // we can populate the history with the original chat messages and then all the response
- // messages up until this point, which includes the most recent ones.
- augmentedHistory ??= [];
- augmentedHistory.Clear();
- augmentedHistory.AddRange(originalMessages);
- augmentedHistory.AddRange(allTurnsResponseMessages);
-
- lastIterationHadThreadId = false;
- }
- else
- {
- // If augmentedHistory is already non-null, then we've already populated it with everything up
- // until this point (except for the most recent response). If it's null, we need to seed it with
- // the chat history provided by the caller.
- augmentedHistory ??= originalMessages.ToList();
-
- // Now add the most recent response messages.
- augmentedHistory.AddMessages(response);
-
- lastIterationHadThreadId = false;
- }
-
- // Use the augmented history as the new set of messages to send.
- messages = augmentedHistory;
- }
-
- /// Copies any from to .
- private static bool CopyFunctionCalls(
- IList messages, [NotNullWhen(true)] ref List? functionCalls)
- {
- bool any = false;
- int count = messages.Count;
- for (int i = 0; i < count; i++)
- {
- any |= CopyFunctionCalls(messages[i].Contents, ref functionCalls);
- }
-
- return any;
- }
-
- /// Copies any from to .
- private static bool CopyFunctionCalls(
- IList content, [NotNullWhen(true)] ref List? functionCalls)
+ public KernelFunctionInvokingChatClient(IChatClient innerClient, ILoggerFactory? loggerFactory = null, IServiceProvider? functionInvocationServices = null)
+ : base(innerClient, loggerFactory, functionInvocationServices)
{
- bool any = false;
- int count = content.Count;
- for (int i = 0; i < count; i++)
- {
- if (content[i] is FunctionCallContent functionCall)
- {
- (functionCalls ??= []).Add(functionCall);
- any = true;
- }
- }
-
- return any;
}
private static void UpdateOptionsForAutoFunctionInvocation(ref ChatOptions options, ChatMessageContent content, bool isStreaming)
@@ -547,67 +85,40 @@ private static void ClearOptionsForAutoFunctionInvocation(ref ChatOptions option
}
}
- private static void UpdateOptionsForNextIteration(ref ChatOptions options, string? chatThreadId)
+ ///
+ protected override async Task<(bool ShouldTerminate, int NewConsecutiveErrorCount, IList MessagesAdded)> ProcessFunctionCallsAsync(
+ ChatResponse response, List messages, ChatOptions options, List functionCallContents, int iteration, int consecutiveErrorCount, bool isStreaming, IServiceProvider functionInvocationServices, CancellationToken cancellationToken)
{
- if (options.ToolMode is RequiredChatToolMode)
- {
- // We have to reset the tool mode to be non-required after the first iteration,
- // as otherwise we'll be in an infinite loop.
- options = options.Clone();
- options.ToolMode = null;
- options.ChatThreadId = chatThreadId;
- }
- else if (options.ChatThreadId != chatThreadId)
- {
- // As with the other modes, ensure we've propagated the chat thread ID to the options.
- // We only need to clone the options if we're actually mutating it.
- options = options.Clone();
- options.ChatThreadId = chatThreadId;
- }
- }
+ // Prepare the options for the next auto function invocation iteration.
+ UpdateOptionsForAutoFunctionInvocation(ref options!, response.Messages.Last().ToChatMessageContent(), isStreaming: false);
- ///
- /// Processes the function calls in the list.
- ///
- /// The current chat contents, inclusive of the function call contents being processed.
- /// The options used for the response being processed.
- /// The function call contents representing the functions to be invoked.
- /// The iteration number of how many roundtrips have been made to the inner client.
- /// The number of consecutive iterations, prior to this one, that were recorded as having function invocation errors.
- /// Whether the function calls are being processed in a streaming context.
- /// The to monitor for cancellation requests.
- /// A value indicating how the caller should proceed.
- private async Task<(bool ShouldTerminate, int NewConsecutiveErrorCount, IList MessagesAdded)> ProcessFunctionCallsAsync(
- List messages, ChatOptions options, List functionCallContents,
- int iteration, int consecutiveErrorCount, bool isStreaming, CancellationToken cancellationToken)
- {
// We must add a response for every tool call, regardless of whether we successfully executed it or not.
// If we successfully execute it, we'll add the result. If we don't, we'll add an error.
-
+ (bool ShouldTerminate, int NewConsecutiveErrorCount, IList MessagesAdded) result;
Debug.Assert(functionCallContents.Count > 0, "Expected at least one function call.");
var shouldTerminate = false;
- var captureCurrentIterationExceptions = consecutiveErrorCount < _maximumConsecutiveErrorsPerRequest;
+ var captureCurrentIterationExceptions = consecutiveErrorCount < this.MaximumConsecutiveErrorsPerRequest;
// Process all functions. If there's more than one and concurrent invocation is enabled, do so in parallel.
if (functionCallContents.Count == 1)
{
- FunctionInvocationResult result = await ProcessFunctionCallAsync(
- messages, options, functionCallContents, iteration, 0, captureCurrentIterationExceptions, isStreaming, cancellationToken).ConfigureAwait(false);
+ FunctionInvocationResult functionResult = await this.ProcessFunctionCallAsync(
+ messages, options, functionCallContents, iteration, 0, captureCurrentIterationExceptions, isStreaming, functionInvocationServices, cancellationToken).ConfigureAwait(false);
- IList added = CreateResponseMessages([result]);
- ThrowIfNoFunctionResultsAdded(added);
- UpdateConsecutiveErrorCountOrThrow(added, ref consecutiveErrorCount);
+ IList added = this.CreateResponseMessages([functionResult]);
+ this.ThrowIfNoFunctionResultsAdded(added);
+ this.UpdateConsecutiveErrorCountOrThrow(added, ref consecutiveErrorCount);
messages.AddRange(added);
- return (result.ShouldTerminate, consecutiveErrorCount, added);
+ result = (functionResult.ShouldTerminate, consecutiveErrorCount, added);
}
else
{
List results = [];
var terminationRequested = false;
- if (AllowConcurrentInvocation)
+ if (this.AllowConcurrentInvocation)
{
// Rather than awaiting each function before invoking the next, invoke all of them
// and then await all of them. We avoid forcibly introducing parallelism via Task.Run,
@@ -615,9 +126,9 @@ private static void UpdateOptionsForNextIteration(ref ChatOptions options, strin
// with the processing of other the other invocation invocations.
results.AddRange(await Task.WhenAll(
from i in Enumerable.Range(0, functionCallContents.Count)
- select ProcessFunctionCallAsync(
+ select this.ProcessFunctionCallAsync(
messages, options, functionCallContents,
- iteration, i, captureExceptions: true, isStreaming, cancellationToken)).ConfigureAwait(false));
+ iteration, i, captureExceptions: true, isStreaming, functionInvocationServices, cancellationToken)).ConfigureAwait(false));
terminationRequested = results.Any(r => r.ShouldTerminate);
}
@@ -626,13 +137,15 @@ select ProcessFunctionCallAsync(
// Invoke each function serially.
for (int i = 0; i < functionCallContents.Count; i++)
{
- var result = await ProcessFunctionCallAsync(
+ var functionResult = await this.ProcessFunctionCallAsync(
messages, options, functionCallContents,
- iteration, i, captureCurrentIterationExceptions, isStreaming, cancellationToken).ConfigureAwait(false);
+ iteration, i, captureCurrentIterationExceptions, isStreaming, functionInvocationServices, cancellationToken).ConfigureAwait(false);
- results.Add(result);
+ results.Add(functionResult);
- if (result.ShouldTerminate)
+ // Differently from the original FunctionInvokingChatClient, as soon the termination is requested,
+ // we stop and don't continue
+ if (functionResult.ShouldTerminate)
{
shouldTerminate = true;
terminationRequested = true;
@@ -641,9 +154,9 @@ select ProcessFunctionCallAsync(
}
}
- IList added = CreateResponseMessages(results);
- ThrowIfNoFunctionResultsAdded(added);
- UpdateConsecutiveErrorCountOrThrow(added, ref consecutiveErrorCount);
+ IList added = this.CreateResponseMessages(results);
+ this.ThrowIfNoFunctionResultsAdded(added);
+ this.UpdateConsecutiveErrorCountOrThrow(added, ref consecutiveErrorCount);
messages.AddRange(added);
@@ -657,64 +170,19 @@ select ProcessFunctionCallAsync(
}
}
- return (shouldTerminate, consecutiveErrorCount, added);
+ result = (shouldTerminate, consecutiveErrorCount, added);
}
- }
- private void UpdateConsecutiveErrorCountOrThrow(IList added, ref int consecutiveErrorCount)
- {
- var allExceptions = added.SelectMany(m => m.Contents.OfType())
- .Select(frc => frc.Exception!)
- .Where(e => e is not null);
+ // Clear the auto function invocation options.
+ ClearOptionsForAutoFunctionInvocation(ref options);
-#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection
- if (allExceptions.Any())
- {
- consecutiveErrorCount++;
- if (consecutiveErrorCount > _maximumConsecutiveErrorsPerRequest)
- {
- var allExceptionsArray = allExceptions.ToArray();
- if (allExceptionsArray.Length == 1)
- {
- ExceptionDispatchInfo.Capture(allExceptionsArray[0]).Throw();
- }
- else
- {
- throw new AggregateException(allExceptionsArray);
- }
- }
- }
- else
- {
- consecutiveErrorCount = 0;
- }
-#pragma warning restore CA1851 // Possible multiple enumerations of 'IEnumerable' collection
- }
-
- ///
- /// Throws an exception if doesn't create any messages.
- ///
- private void ThrowIfNoFunctionResultsAdded(IList? messages)
- {
- if (messages is null || messages.Count == 0)
- {
- throw new InvalidOperationException($"{this.GetType().Name}.{nameof(this.CreateResponseMessages)} returned null or an empty collection of messages.");
- }
+ return result;
}
- /// Processes the function call described in [ ].
- /// The current chat contents, inclusive of the function call contents being processed.
- /// The options used for the response being processed.
- /// The function call contents representing all the functions being invoked.
- /// The iteration number of how many roundtrips have been made to the inner client.
- /// The 0-based index of the function being called out of .
- /// If true, handles function-invocation exceptions by returning a value with . Otherwise, rethrows.
- /// Whether the function calls are being processed in a streaming context.
- /// The to monitor for cancellation requests.
- /// A value indicating how the caller should proceed.
- private async Task ProcessFunctionCallAsync(
+ ///
+ protected override async Task ProcessFunctionCallAsync(
List messages, ChatOptions options, List callContents,
- int iteration, int functionCallIndex, bool captureExceptions, bool isStreaming, CancellationToken cancellationToken)
+ int iteration, int functionCallIndex, bool captureExceptions, bool isStreaming, IServiceProvider functionInvocationServices, CancellationToken cancellationToken)
{
var callContent = callContents[functionCallIndex];
@@ -722,7 +190,7 @@ private async Task ProcessFunctionCallAsync(
AIFunction? function = options.Tools!.OfType().FirstOrDefault(t => t.Name == callContent.Name);
if (function is null)
{
- return new(shouldTerminate: false, FunctionInvokingChatClient.FunctionInvocationStatus.NotFound, callContent, result: null, exception: null);
+ return new(shouldTerminate: false, FunctionInvocationStatus.NotFound, callContent, result: null, exception: null);
}
if (callContent.Arguments is not null)
@@ -733,7 +201,7 @@ private async Task ProcessFunctionCallAsync(
var context = new AutoFunctionInvocationContext(new()
{
Function = function,
- Arguments = new(callContent.Arguments) { Services = _functionInvocationServices },
+ Arguments = new(callContent.Arguments) { Services = functionInvocationServices },
Messages = messages,
Options = options,
@@ -858,39 +326,13 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(
}
}
- /// Invokes the function asynchronously.
- ///
- /// The function invocation context detailing the function to be invoked and its arguments along with additional request information.
- ///
- /// The to monitor for cancellation requests. The default is .
- /// The result of the function invocation, or if the function invocation returned .
- /// is .
- private async Task InvokeFunctionAsync(AutoFunctionInvocationContext context, CancellationToken cancellationToken)
+ protected override async Task<(FunctionInvocationContext context, object? result)> TryInvokeFunctionAsync(FunctionInvocationContext context, CancellationToken cancellationToken)
{
- Verify.NotNull(context);
-
- using Activity? activity = _activitySource?.StartActivity(context.Function.Name);
-
- long startingTimestamp = 0;
- if (_logger.IsEnabled(LogLevel.Debug))
- {
- startingTimestamp = Stopwatch.GetTimestamp();
- if (_logger.IsEnabled(LogLevel.Trace))
- {
- LogInvokingSensitive(context.Function.Name, LoggingAsJson(context.CallContent.Arguments, context.AIFunction.JsonSerializerOptions));
- }
- else
- {
- LogInvoking(context.Function.Name);
- }
- }
-
object? result = null;
- try
+ if (context is AutoFunctionInvocationContext autoContext)
{
- CurrentContext = context; // doesn't need to be explicitly reset after, as that's handled automatically at async method exit
context = await this.OnAutoFunctionInvocationAsync(
- context,
+ autoContext,
async (ctx) =>
{
// Check if filter requested termination
@@ -902,122 +344,16 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(
// Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any
// further calls made as part of this function invocation. In particular, we must not use function calling settings naively here,
// as the called function could in turn telling the model about itself as a possible candidate for invocation.
- result = await context.AIFunction.InvokeAsync(new(context.Arguments), cancellationToken).ConfigureAwait(false);
+ result = await autoContext.AIFunction.InvokeAsync(new(context.Arguments), cancellationToken).ConfigureAwait(false);
ctx.Result = new FunctionResult(ctx.Function, result);
}).ConfigureAwait(false);
- result = context.Result.GetValue();
- }
- catch (Exception e)
- {
- if (activity is not null)
- {
- _ = activity.SetTag("error.type", e.GetType().FullName)
- .SetStatus(ActivityStatusCode.Error, e.Message);
- }
-
- if (e is OperationCanceledException)
- {
- LogInvocationCanceled(context.Function.Name);
- }
- else
- {
- LogInvocationFailed(context.Function.Name, e);
- }
-
- throw;
- }
- finally
- {
- if (_logger.IsEnabled(LogLevel.Debug))
- {
- TimeSpan elapsed = GetElapsedTime(startingTimestamp);
-
- if (result is not null && _logger.IsEnabled(LogLevel.Trace))
- {
- LogInvocationCompletedSensitive(context.Function.Name, elapsed, LoggingAsJson(result, context.AIFunction.JsonSerializerOptions));
- }
- else
- {
- LogInvocationCompleted(context.Function.Name, elapsed);
- }
- }
+ result = autoContext.Result.GetValue();
}
-
- return result;
- }
-
- /// Serializes as JSON for logging purposes.
- private static string LoggingAsJson(T value, JsonSerializerOptions? options)
- {
- if (options?.TryGetTypeInfo(typeof(T), out var typeInfo) is true ||
- AIJsonUtilities.DefaultOptions.TryGetTypeInfo(typeof(T), out typeInfo))
- {
-#pragma warning disable CA1031 // Do not catch general exception types
- try
- {
- return JsonSerializer.Serialize(value, typeInfo);
- }
- catch
- {
- }
-#pragma warning restore CA1031 // Do not catch general exception types
- }
-
- // If we're unable to get a type info for the value, or if we fail to serialize,
- // return an empty JSON object. We do not want lack of type info to disrupt application behavior with exceptions.
- return "{}";
- }
-
- private static TimeSpan GetElapsedTime(long startingTimestamp) =>
-#if NET
- Stopwatch.GetElapsedTime(startingTimestamp);
-#else
- new((long)((Stopwatch.GetTimestamp() - startingTimestamp) * ((double)TimeSpan.TicksPerSecond / Stopwatch.Frequency)));
-#endif
-
- [LoggerMessage(LogLevel.Debug, "Invoking {MethodName}.", SkipEnabledCheck = true)]
- private partial void LogInvoking(string methodName);
-
- [LoggerMessage(LogLevel.Trace, "Invoking {MethodName}({Arguments}).", SkipEnabledCheck = true)]
- private partial void LogInvokingSensitive(string methodName, string arguments);
-
- [LoggerMessage(LogLevel.Debug, "{MethodName} invocation completed. Duration: {Duration}", SkipEnabledCheck = true)]
- private partial void LogInvocationCompleted(string methodName, TimeSpan duration);
-
- [LoggerMessage(LogLevel.Trace, "{MethodName} invocation completed. Duration: {Duration}. Result: {Result}", SkipEnabledCheck = true)]
- private partial void LogInvocationCompletedSensitive(string methodName, TimeSpan duration, string result);
-
- [LoggerMessage(LogLevel.Debug, "{MethodName} invocation canceled.")]
- private partial void LogInvocationCanceled(string methodName);
-
- [LoggerMessage(LogLevel.Error, "{MethodName} invocation failed.")]
- private partial void LogInvocationFailed(string methodName, Exception error);
-
- /// Provides information about the invocation of a function call.
- public sealed class FunctionInvocationResult
- {
- internal FunctionInvocationResult(bool shouldTerminate, FunctionInvokingChatClient.FunctionInvocationStatus status, FunctionCallContent callContent, object? result, Exception? exception)
+ else
{
- ShouldTerminate = shouldTerminate;
- Status = status;
- CallContent = callContent;
- Result = result;
- Exception = exception;
+ result = await context.Function.InvokeAsync(context.Arguments, cancellationToken).ConfigureAwait(false);
}
- /// Gets status about how the function invocation completed.
- public FunctionInvokingChatClient.FunctionInvocationStatus Status { get; }
-
- /// Gets the function call content information associated with this invocation.
- public FunctionCallContent CallContent { get; }
-
- /// Gets the result of the function call.
- public object? Result { get; }
-
- /// Gets any exception the function call threw.
- public Exception? Exception { get; }
-
- /// Gets a value indicating whether the caller should terminate the processing loop.
- internal bool ShouldTerminate { get; }
+ return (context, result);
}
}
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClientOld.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClientOld.cs
new file mode 100644
index 000000000000..1afb2689983b
--- /dev/null
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClientOld.cs
@@ -0,0 +1,1023 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+#pragma warning restore IDE0073 // The file header does not match the required text
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.ExceptionServices;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.ChatCompletion;
+
+#pragma warning disable IDE1006 // Naming Styles
+#pragma warning disable IDE0009 // This
+#pragma warning disable CA2213 // Disposable fields should be disposed
+#pragma warning disable EA0002 // Use 'System.TimeProvider' to make the code easier to test
+#pragma warning disable SA1202 // 'protected' members should come before 'private' members
+
+// Modified source from 2025-04-07
+// https://raw.githubusercontent.com/dotnet/extensions/84d09b794d994435568adcbb85a981143d4f15cb/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// A delegating chat client that invokes functions defined on .
+/// Include this in a chat pipeline to resolve function calls automatically.
+///
+///
+///
+/// When this client receives a in a chat response, it responds
+/// by calling the corresponding defined in ,
+/// producing a that it sends back to the inner client. This loop
+/// is repeated until there are no more function calls to make, or until another stop condition is met,
+/// such as hitting .
+///
+///
+/// The provided implementation of is thread-safe for concurrent use so long as the
+/// instances employed as part of the supplied are also safe.
+/// The property can be used to control whether multiple function invocation
+/// requests as part of the same request are invocable concurrently, but even with that set to
+/// (the default), multiple concurrent requests to this same instance and using the same tools could result in those
+/// tools being used concurrently (one per request). For example, a function that accesses the HttpContext of a specific
+/// ASP.NET web request should only be used as part of a single at a time, and only with
+/// set to , in case the inner client decided to issue multiple
+/// invocation requests to that same function.
+///
+///
+public partial class KernelFunctionInvokingChatClientOld : DelegatingChatClient
+{
+ /// The for the current function invocation.
+ private static readonly AsyncLocal _currentContext = new();
+
+ /// Optional services used for function invocation.
+ private readonly IServiceProvider? _functionInvocationServices;
+
+ /// The logger to use for logging information about function invocation.
+ private readonly ILogger _logger;
+
+ /// The to use for telemetry.
+ /// This component does not own the instance and should not dispose it.
+ private readonly ActivitySource? _activitySource;
+
+ /// Maximum number of roundtrips allowed to the inner client.
+ private int _maximumIterationsPerRequest = 10;
+
+ /// Maximum number of consecutive iterations that are allowed contain at least one exception result. If the limit is exceeded, we rethrow the exception instead of continuing.
+ private int _maximumConsecutiveErrorsPerRequest = 3;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The underlying , or the next instance in a chain of clients.
+ /// An to use for logging information about function invocation.
+ /// An optional to use for resolving services required by the instances being invoked.
+ public KernelFunctionInvokingChatClientOld(IChatClient innerClient, ILoggerFactory? loggerFactory = null, IServiceProvider? functionInvocationServices = null)
+ : base(innerClient)
+ {
+ _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance;
+ _activitySource = innerClient.GetService();
+ _functionInvocationServices = functionInvocationServices;
+ }
+
+ ///
+ /// Gets or sets the for the current function invocation.
+ ///
+ ///
+ /// This value flows across async calls.
+ ///
+ public static AutoFunctionInvocationContext? CurrentContext
+ {
+ get => _currentContext.Value;
+ protected set => _currentContext.Value = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether detailed exception information should be included
+ /// in the chat history when calling the underlying .
+ ///
+ ///
+ /// if the full exception message is added to the chat history
+ /// when calling the underlying .
+ /// if a generic error message is included in the chat history.
+ /// The default value is .
+ ///
+ ///
+ ///
+ /// Setting the value to prevents the underlying language model from disclosing
+ /// raw exception details to the end user, since it doesn't receive that information. Even in this
+ /// case, the raw object is available to application code by inspecting
+ /// the property.
+ ///
+ ///
+ /// Setting the value to can help the underlying bypass problems on
+ /// its own, for example by retrying the function call with different arguments. However, it might
+ /// result in disclosing the raw exception information to external users, which can be a security
+ /// concern depending on the application scenario.
+ ///
+ ///
+ /// Changing the value of this property while the client is in use might result in inconsistencies
+ /// whether detailed errors are provided during an in-flight request.
+ ///
+ ///
+ public bool IncludeDetailedErrors { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to allow concurrent invocation of functions.
+ ///
+ ///
+ /// if multiple function calls can execute in parallel.
+ /// if function calls are processed serially.
+ /// The default value is .
+ ///
+ ///
+ /// An individual response from the inner client might contain multiple function call requests.
+ /// By default, such function calls are processed serially. Set to
+ /// to enable concurrent invocation such that multiple function calls can execute in parallel.
+ ///
+ public bool AllowConcurrentInvocation { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of iterations per request.
+ ///
+ ///
+ /// The maximum number of iterations per request.
+ /// The default value is 10.
+ ///
+ ///
+ ///
+ /// Each request to this might end up making
+ /// multiple requests to the inner client. Each time the inner client responds with
+ /// a function call request, this client might perform that invocation and send the results
+ /// back to the inner client in a new request. This property limits the number of times
+ /// such a roundtrip is performed. The value must be at least one, as it includes the initial request.
+ ///
+ ///
+ /// Changing the value of this property while the client is in use might result in inconsistencies
+ /// as to how many iterations are allowed for an in-flight request.
+ ///
+ ///
+ public int MaximumIterationsPerRequest
+ {
+ get => _maximumIterationsPerRequest;
+ set
+ {
+ if (value < 1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+
+ _maximumIterationsPerRequest = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum number of consecutive iterations that are allowed to fail with an error.
+ ///
+ ///
+ /// The maximum number of consecutive iterations that are allowed to fail with an error.
+ /// The default value is 3.
+ ///
+ ///
+ ///
+ /// When function invocations fail with an exception, the
+ /// continues to make requests to the inner client, optionally supplying exception information (as
+ /// controlled by ). This allows the to
+ /// recover from errors by trying other function parameters that may succeed.
+ ///
+ ///
+ /// However, in case function invocations continue to produce exceptions, this property can be used to
+ /// limit the number of consecutive failing attempts. When the limit is reached, the exception will be
+ /// rethrown to the caller.
+ ///
+ ///
+ /// If the value is set to zero, all function calling exceptions immediately terminate the function
+ /// invocation loop and the exception will be rethrown to the caller.
+ ///
+ ///
+ /// Changing the value of this property while the client is in use might result in inconsistencies
+ /// as to how many iterations are allowed for an in-flight request.
+ ///
+ ///
+ public int MaximumConsecutiveErrorsPerRequest
+ {
+ get => _maximumConsecutiveErrorsPerRequest;
+ set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "Argument less than minimum value 0");
+ }
+ _maximumConsecutiveErrorsPerRequest = value;
+ }
+ }
+
+ ///
+ public override async Task GetResponseAsync(
+ IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(messages);
+
+ // A single request into this GetResponseAsync may result in multiple requests to the inner client.
+ // Create an activity to group them together for better observability.
+ using Activity? activity = _activitySource?.StartActivity(nameof(KernelFunctionInvokingChatClient));
+
+ // Copy the original messages in order to avoid enumerating the original messages multiple times.
+ // The IEnumerable can represent an arbitrary amount of work.
+ List originalMessages = [.. messages];
+ messages = originalMessages;
+
+ List? augmentedHistory = null; // the actual history of messages sent on turns other than the first
+ ChatResponse? response = null; // the response from the inner client, which is possibly modified and then eventually returned
+ List? responseMessages = null; // tracked list of messages, across multiple turns, to be used for the final response
+ UsageDetails? totalUsage = null; // tracked usage across all turns, to be used for the final response
+ List? functionCallContents = null; // function call contents that need responding to in the current turn
+ bool lastIterationHadThreadId = false; // whether the last iteration's response had a ChatThreadId set
+ int consecutiveErrorCount = 0;
+
+ for (int iteration = 0; ; iteration++)
+ {
+ functionCallContents?.Clear();
+
+ // Make the call to the inner client.
+ response = await base.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
+ if (response is null)
+ {
+ throw new InvalidOperationException($"The inner {nameof(IChatClient)} returned a null {nameof(ChatResponse)}.");
+ }
+
+ // Any function call work to do? If yes, ensure we're tracking that work in functionCallContents.
+ bool requiresFunctionInvocation =
+ options?.Tools is { Count: > 0 } &&
+ iteration < MaximumIterationsPerRequest &&
+ CopyFunctionCalls(response.Messages, ref functionCallContents);
+
+ // In a common case where we make a request and there's no function calling work required,
+ // fast path out by just returning the original response.
+ if (iteration == 0 && !requiresFunctionInvocation)
+ {
+ return response;
+ }
+
+ // Track aggregate details from the response, including all the response messages and usage details.
+ (responseMessages ??= []).AddRange(response.Messages);
+ if (response.Usage is not null)
+ {
+ if (totalUsage is not null)
+ {
+ totalUsage.Add(response.Usage);
+ }
+ else
+ {
+ totalUsage = response.Usage;
+ }
+ }
+
+ // If there are no tools to call, or for any other reason we should stop, we're done.
+ // Break out of the loop and allow the handling at the end to configure the response
+ // with aggregated data from previous requests.
+ if (!requiresFunctionInvocation)
+ {
+ break;
+ }
+
+ // Prepare the history for the next iteration.
+ FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadThreadId);
+
+ // Prepare the options for the next auto function invocation iteration.
+ UpdateOptionsForAutoFunctionInvocation(ref options!, response.Messages.Last().ToChatMessageContent(), isStreaming: false);
+
+ // Add the responses from the function calls into the augmented history and also into the tracked
+ // list of response messages.
+ var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options!, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: false, cancellationToken).ConfigureAwait(false);
+ responseMessages.AddRange(modeAndMessages.MessagesAdded);
+ consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount;
+
+ if (modeAndMessages.ShouldTerminate)
+ {
+ break;
+ }
+
+ // Clear the auto function invocation options.
+ ClearOptionsForAutoFunctionInvocation(ref options);
+
+ UpdateOptionsForNextIteration(ref options!, response.ChatThreadId);
+ }
+
+ Debug.Assert(responseMessages is not null, "Expected to only be here if we have response messages.");
+ response.Messages = responseMessages!;
+ response.Usage = totalUsage;
+
+ return response;
+ }
+
+ ///
+ public override async IAsyncEnumerable GetStreamingResponseAsync(
+ IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(messages);
+
+ // A single request into this GetStreamingResponseAsync may result in multiple requests to the inner client.
+ // Create an activity to group them together for better observability.
+ using Activity? activity = _activitySource?.StartActivity(nameof(FunctionInvokingChatClient));
+
+ // Copy the original messages in order to avoid enumerating the original messages multiple times.
+ // The IEnumerable can represent an arbitrary amount of work.
+ List originalMessages = [.. messages];
+ messages = originalMessages;
+
+ List? augmentedHistory = null; // the actual history of messages sent on turns other than the first
+ List? functionCallContents = null; // function call contents that need responding to in the current turn
+ List? responseMessages = null; // tracked list of messages, across multiple turns, to be used in fallback cases to reconstitute history
+ bool lastIterationHadThreadId = false; // whether the last iteration's response had a ChatThreadId set
+ List updates = []; // updates from the current response
+ int consecutiveErrorCount = 0;
+
+ for (int iteration = 0; ; iteration++)
+ {
+ updates.Clear();
+ functionCallContents?.Clear();
+
+ await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false))
+ {
+ if (update is null)
+ {
+ throw new InvalidOperationException($"The inner {nameof(IChatClient)} streamed a null {nameof(ChatResponseUpdate)}.");
+ }
+
+ updates.Add(update);
+
+ _ = CopyFunctionCalls(update.Contents, ref functionCallContents);
+
+ yield return update;
+ Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
+ }
+
+ // If there are no tools to call, or for any other reason we should stop, return the response.
+ if (functionCallContents is not { Count: > 0 } ||
+ options?.Tools is not { Count: > 0 } ||
+ iteration >= _maximumIterationsPerRequest)
+ {
+ break;
+ }
+
+ // Reconstitute a response from the response updates.
+ var response = updates.ToChatResponse();
+ (responseMessages ??= []).AddRange(response.Messages);
+
+ // Prepare the history for the next iteration.
+ FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadThreadId);
+
+ // Prepare the options for the next auto function invocation iteration.
+ UpdateOptionsForAutoFunctionInvocation(ref options, response.Messages.Last().ToChatMessageContent(), isStreaming: true);
+
+ // Process all the functions, adding their results into the history.
+ var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, functionCallContents, iteration, consecutiveErrorCount, isStreaming: true, cancellationToken).ConfigureAwait(false);
+ responseMessages.AddRange(modeAndMessages.MessagesAdded);
+ consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount;
+
+ // Clear the auto function invocation options.
+ ClearOptionsForAutoFunctionInvocation(ref options);
+
+ // This is a synthetic ID since we're generating the tool messages instead of getting them from
+ // the underlying provider. When emitting the streamed chunks, it's perfectly valid for us to
+ // use the same message ID for all of them within a given iteration, as this is a single logical
+ // message with multiple content items. We could also use different message IDs per tool content,
+ // but there's no benefit to doing so.
+ string toolResponseId = Guid.NewGuid().ToString("N");
+
+ // Stream any generated function results. This mirrors what's done for GetResponseAsync, where the returned messages
+ // include all activity, including generated function results.
+ foreach (var message in modeAndMessages.MessagesAdded)
+ {
+ var toolResultUpdate = new ChatResponseUpdate
+ {
+ AdditionalProperties = message.AdditionalProperties,
+ AuthorName = message.AuthorName,
+ ChatThreadId = response.ChatThreadId,
+ CreatedAt = DateTimeOffset.UtcNow,
+ Contents = message.Contents,
+ RawRepresentation = message.RawRepresentation,
+ ResponseId = toolResponseId,
+ MessageId = toolResponseId, // See above for why this can be the same as ResponseId
+ Role = message.Role,
+ };
+
+ yield return toolResultUpdate;
+ Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
+ }
+
+ if (modeAndMessages.ShouldTerminate)
+ {
+ yield break;
+ }
+
+ UpdateOptionsForNextIteration(ref options, response.ChatThreadId);
+ }
+ }
+
+ /// Prepares the various chat message lists after a response from the inner client and before invoking functions.
+ /// The original messages provided by the caller.
+ /// The messages reference passed to the inner client.
+ /// The augmented history containing all the messages to be sent.
+ /// The most recent response being handled.
+ /// A list of all response messages received up until this point.
+ /// Whether the previous iteration's response had a thread id.
+ private static void FixupHistories(
+ IEnumerable originalMessages,
+ ref IEnumerable messages,
+ [NotNull] ref List? augmentedHistory,
+ ChatResponse response,
+ List allTurnsResponseMessages,
+ ref bool lastIterationHadThreadId)
+ {
+ // We're now going to need to augment the history with function result contents.
+ // That means we need a separate list to store the augmented history.
+ if (response.ChatThreadId is not null)
+ {
+ // The response indicates the inner client is tracking the history, so we don't want to send
+ // anything we've already sent or received.
+ if (augmentedHistory is not null)
+ {
+ augmentedHistory.Clear();
+ }
+ else
+ {
+ augmentedHistory = [];
+ }
+
+ lastIterationHadThreadId = true;
+ }
+ else if (lastIterationHadThreadId)
+ {
+ // In the very rare case where the inner client returned a response with a thread ID but then
+ // returned a subsequent response without one, we want to reconstitute the full history. To do that,
+ // we can populate the history with the original chat messages and then all the response
+ // messages up until this point, which includes the most recent ones.
+ augmentedHistory ??= [];
+ augmentedHistory.Clear();
+ augmentedHistory.AddRange(originalMessages);
+ augmentedHistory.AddRange(allTurnsResponseMessages);
+
+ lastIterationHadThreadId = false;
+ }
+ else
+ {
+ // If augmentedHistory is already non-null, then we've already populated it with everything up
+ // until this point (except for the most recent response). If it's null, we need to seed it with
+ // the chat history provided by the caller.
+ augmentedHistory ??= originalMessages.ToList();
+
+ // Now add the most recent response messages.
+ augmentedHistory.AddMessages(response);
+
+ lastIterationHadThreadId = false;
+ }
+
+ // Use the augmented history as the new set of messages to send.
+ messages = augmentedHistory;
+ }
+
+ /// Copies any from to .
+ private static bool CopyFunctionCalls(
+ IList messages, [NotNullWhen(true)] ref List? functionCalls)
+ {
+ bool any = false;
+ int count = messages.Count;
+ for (int i = 0; i < count; i++)
+ {
+ any |= CopyFunctionCalls(messages[i].Contents, ref functionCalls);
+ }
+
+ return any;
+ }
+
+ /// Copies any from to .
+ private static bool CopyFunctionCalls(
+ IList content, [NotNullWhen(true)] ref List? functionCalls)
+ {
+ bool any = false;
+ int count = content.Count;
+ for (int i = 0; i < count; i++)
+ {
+ if (content[i] is FunctionCallContent functionCall)
+ {
+ (functionCalls ??= []).Add(functionCall);
+ any = true;
+ }
+ }
+
+ return any;
+ }
+
+ private static void UpdateOptionsForAutoFunctionInvocation(ref ChatOptions options, ChatMessageContent content, bool isStreaming)
+ {
+ if (options.AdditionalProperties?.ContainsKey(ChatOptionsExtensions.IsStreamingKey) ?? false)
+ {
+ throw new KernelException($"The reserved key name '{ChatOptionsExtensions.IsStreamingKey}' is already specified in the options. Avoid using this key name.");
+ }
+
+ if (options.AdditionalProperties?.ContainsKey(ChatOptionsExtensions.ChatMessageContentKey) ?? false)
+ {
+ throw new KernelException($"The reserved key name '{ChatOptionsExtensions.ChatMessageContentKey}' is already specified in the options. Avoid using this key name.");
+ }
+
+ options.AdditionalProperties ??= [];
+
+ options.AdditionalProperties[ChatOptionsExtensions.IsStreamingKey] = isStreaming;
+ options.AdditionalProperties[ChatOptionsExtensions.ChatMessageContentKey] = content;
+ }
+
+ private static void ClearOptionsForAutoFunctionInvocation(ref ChatOptions options)
+ {
+ if (options.AdditionalProperties?.ContainsKey(ChatOptionsExtensions.IsStreamingKey) ?? false)
+ {
+ options.AdditionalProperties.Remove(ChatOptionsExtensions.IsStreamingKey);
+ }
+
+ if (options.AdditionalProperties?.ContainsKey(ChatOptionsExtensions.ChatMessageContentKey) ?? false)
+ {
+ options.AdditionalProperties.Remove(ChatOptionsExtensions.ChatMessageContentKey);
+ }
+ }
+
+ private static void UpdateOptionsForNextIteration(ref ChatOptions options, string? chatThreadId)
+ {
+ if (options.ToolMode is RequiredChatToolMode)
+ {
+ // We have to reset the tool mode to be non-required after the first iteration,
+ // as otherwise we'll be in an infinite loop.
+ options = options.Clone();
+ options.ToolMode = null;
+ options.ChatThreadId = chatThreadId;
+ }
+ else if (options.ChatThreadId != chatThreadId)
+ {
+ // As with the other modes, ensure we've propagated the chat thread ID to the options.
+ // We only need to clone the options if we're actually mutating it.
+ options = options.Clone();
+ options.ChatThreadId = chatThreadId;
+ }
+ }
+
+ ///
+ /// Processes the function calls in the list.
+ ///
+ /// The current chat contents, inclusive of the function call contents being processed.
+ /// The options used for the response being processed.
+ /// The function call contents representing the functions to be invoked.
+ /// The iteration number of how many roundtrips have been made to the inner client.
+ /// The number of consecutive iterations, prior to this one, that were recorded as having function invocation errors.
+ /// Whether the function calls are being processed in a streaming context.
+ /// The to monitor for cancellation requests.
+ /// A value indicating how the caller should proceed.
+ private async Task<(bool ShouldTerminate, int NewConsecutiveErrorCount, IList MessagesAdded)> ProcessFunctionCallsAsync(
+ List messages, ChatOptions options, List functionCallContents,
+ int iteration, int consecutiveErrorCount, bool isStreaming, CancellationToken cancellationToken)
+ {
+ // We must add a response for every tool call, regardless of whether we successfully executed it or not.
+ // If we successfully execute it, we'll add the result. If we don't, we'll add an error.
+
+ Debug.Assert(functionCallContents.Count > 0, "Expected at least one function call.");
+ var shouldTerminate = false;
+
+ var captureCurrentIterationExceptions = consecutiveErrorCount < _maximumConsecutiveErrorsPerRequest;
+
+ // Process all functions. If there's more than one and concurrent invocation is enabled, do so in parallel.
+ if (functionCallContents.Count == 1)
+ {
+ FunctionInvocationResult result = await ProcessFunctionCallAsync(
+ messages, options, functionCallContents, iteration, 0, captureCurrentIterationExceptions, isStreaming, cancellationToken).ConfigureAwait(false);
+
+ IList added = CreateResponseMessages([result]);
+ ThrowIfNoFunctionResultsAdded(added);
+ UpdateConsecutiveErrorCountOrThrow(added, ref consecutiveErrorCount);
+
+ messages.AddRange(added);
+ return (result.ShouldTerminate, consecutiveErrorCount, added);
+ }
+ else
+ {
+ List results = [];
+
+ var terminationRequested = false;
+ if (AllowConcurrentInvocation)
+ {
+ // Rather than awaiting each function before invoking the next, invoke all of them
+ // and then await all of them. We avoid forcibly introducing parallelism via Task.Run,
+ // but if a function invocation completes asynchronously, its processing can overlap
+ // with the processing of other the other invocation invocations.
+ results.AddRange(await Task.WhenAll(
+ from i in Enumerable.Range(0, functionCallContents.Count)
+ select ProcessFunctionCallAsync(
+ messages, options, functionCallContents,
+ iteration, i, captureExceptions: true, isStreaming, cancellationToken)).ConfigureAwait(false));
+
+ terminationRequested = results.Any(r => r.ShouldTerminate);
+ }
+ else
+ {
+ // Invoke each function serially.
+ for (int i = 0; i < functionCallContents.Count; i++)
+ {
+ var result = await ProcessFunctionCallAsync(
+ messages, options, functionCallContents,
+ iteration, i, captureCurrentIterationExceptions, isStreaming, cancellationToken).ConfigureAwait(false);
+
+ results.Add(result);
+
+ if (result.ShouldTerminate)
+ {
+ shouldTerminate = true;
+ terminationRequested = true;
+ break;
+ }
+ }
+ }
+
+ IList added = CreateResponseMessages(results);
+ ThrowIfNoFunctionResultsAdded(added);
+ UpdateConsecutiveErrorCountOrThrow(added, ref consecutiveErrorCount);
+
+ messages.AddRange(added);
+
+ if (!terminationRequested)
+ {
+ // If any function requested termination, we'll terminate.
+ shouldTerminate = false;
+ foreach (FunctionInvocationResult fir in results)
+ {
+ shouldTerminate = shouldTerminate || fir.ShouldTerminate;
+ }
+ }
+
+ return (shouldTerminate, consecutiveErrorCount, added);
+ }
+ }
+
+ private void UpdateConsecutiveErrorCountOrThrow(IList added, ref int consecutiveErrorCount)
+ {
+ var allExceptions = added.SelectMany(m => m.Contents.OfType())
+ .Select(frc => frc.Exception!)
+ .Where(e => e is not null);
+
+#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection
+ if (allExceptions.Any())
+ {
+ consecutiveErrorCount++;
+ if (consecutiveErrorCount > _maximumConsecutiveErrorsPerRequest)
+ {
+ var allExceptionsArray = allExceptions.ToArray();
+ if (allExceptionsArray.Length == 1)
+ {
+ ExceptionDispatchInfo.Capture(allExceptionsArray[0]).Throw();
+ }
+ else
+ {
+ throw new AggregateException(allExceptionsArray);
+ }
+ }
+ }
+ else
+ {
+ consecutiveErrorCount = 0;
+ }
+#pragma warning restore CA1851 // Possible multiple enumerations of 'IEnumerable' collection
+ }
+
+ ///
+ /// Throws an exception if doesn't create any messages.
+ ///
+ private void ThrowIfNoFunctionResultsAdded(IList? messages)
+ {
+ if (messages is null || messages.Count == 0)
+ {
+ throw new InvalidOperationException($"{this.GetType().Name}.{nameof(this.CreateResponseMessages)} returned null or an empty collection of messages.");
+ }
+ }
+
+ /// Processes the function call described in [ ].
+ /// The current chat contents, inclusive of the function call contents being processed.
+ /// The options used for the response being processed.
+ /// The function call contents representing all the functions being invoked.
+ /// The iteration number of how many roundtrips have been made to the inner client.
+ /// The 0-based index of the function being called out of .
+ /// If true, handles function-invocation exceptions by returning a value with . Otherwise, rethrows.
+ /// Whether the function calls are being processed in a streaming context.
+ /// The to monitor for cancellation requests.
+ /// A value indicating how the caller should proceed.
+ private async Task ProcessFunctionCallAsync(
+ List messages, ChatOptions options, List callContents,
+ int iteration, int functionCallIndex, bool captureExceptions, bool isStreaming, CancellationToken cancellationToken)
+ {
+ var callContent = callContents[functionCallIndex];
+
+ // Look up the AIFunction for the function call. If the requested function isn't available, send back an error.
+ AIFunction? function = options.Tools!.OfType().FirstOrDefault(t => t.Name == callContent.Name);
+ if (function is null)
+ {
+ return new(shouldTerminate: false, FunctionInvokingChatClient.FunctionInvocationStatus.NotFound, callContent, result: null, exception: null);
+ }
+
+ if (callContent.Arguments is not null)
+ {
+ callContent.Arguments = new KernelArguments(callContent.Arguments);
+ }
+
+ var context = new AutoFunctionInvocationContext(new()
+ {
+ Function = function,
+ Arguments = new(callContent.Arguments) { Services = _functionInvocationServices },
+
+ Messages = messages,
+ Options = options,
+
+ CallContent = callContent,
+ Iteration = iteration,
+ FunctionCallIndex = functionCallIndex,
+ FunctionCount = callContents.Count,
+ })
+ { IsStreaming = isStreaming };
+
+ object? result;
+ try
+ {
+ result = await InvokeFunctionAsync(context, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception e) when (!cancellationToken.IsCancellationRequested)
+ {
+ if (!captureExceptions)
+ {
+ throw;
+ }
+
+ return new(
+ shouldTerminate: false,
+ FunctionInvokingChatClient.FunctionInvocationStatus.Exception,
+ callContent,
+ result: null,
+ exception: e);
+ }
+
+ return new(
+ shouldTerminate: context.Terminate,
+ FunctionInvokingChatClient.FunctionInvocationStatus.RanToCompletion,
+ callContent,
+ result,
+ exception: null);
+ }
+
+ /// Creates one or more response messages for function invocation results.
+ /// Information about the function call invocations and results.
+ /// A list of all chat messages created from .
+ private IList CreateResponseMessages(List results)
+ {
+ var contents = new List(results.Count);
+ for (int i = 0; i < results.Count; i++)
+ {
+ contents.Add(CreateFunctionResultContent(results[i]));
+ }
+
+ return [new(ChatRole.Tool, contents)];
+
+ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult result)
+ {
+ Verify.NotNull(result);
+
+ object? functionResult;
+ if (result.Status == FunctionInvokingChatClient.FunctionInvocationStatus.RanToCompletion)
+ {
+ functionResult = result.Result ?? "Success: Function completed.";
+ }
+ else
+ {
+ string message = result.Status switch
+ {
+ FunctionInvokingChatClient.FunctionInvocationStatus.NotFound => $"Error: Requested function \"{result.CallContent.Name}\" not found.",
+ FunctionInvokingChatClient.FunctionInvocationStatus.Exception => "Error: Function failed.",
+ _ => "Error: Unknown error.",
+ };
+
+ if (IncludeDetailedErrors && result.Exception is not null)
+ {
+ message = $"{message} Exception: {result.Exception.Message}";
+ }
+
+ functionResult = message;
+ }
+
+ return new FunctionResultContent(result.CallContent.CallId, functionResult) { Exception = result.Exception };
+ }
+ }
+
+ ///
+ /// Invokes the auto function invocation filters.
+ ///
+ /// The auto function invocation context.
+ /// The function to call after the filters.
+ /// The auto function invocation context.
+ private async Task OnAutoFunctionInvocationAsync(
+ AutoFunctionInvocationContext context,
+ Func functionCallCallback)
+ {
+ await this.InvokeFilterOrFunctionAsync(functionCallCallback, context).ConfigureAwait(false);
+
+ return context;
+ }
+
+ ///
+ /// This method will execute auto function invocation filters and function recursively.
+ /// If there are no registered filters, just function will be executed.
+ /// If there are registered filters, filter on position will be executed.
+ /// Second parameter of filter is callback. It can be either filter on + 1 position or function if there are no remaining filters to execute.
+ /// Function will always be executed as last step after all filters.
+ ///
+ private async Task InvokeFilterOrFunctionAsync(
+ Func functionCallCallback,
+ AutoFunctionInvocationContext context,
+ int index = 0)
+ {
+ IList autoFunctionInvocationFilters = context.Kernel.AutoFunctionInvocationFilters;
+
+ if (autoFunctionInvocationFilters is { Count: > 0 } && index < autoFunctionInvocationFilters.Count)
+ {
+ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(
+ context,
+ (ctx) => this.InvokeFilterOrFunctionAsync(functionCallCallback, ctx, index + 1)
+ ).ConfigureAwait(false);
+ }
+ else
+ {
+ await functionCallCallback(context).ConfigureAwait(false);
+ }
+ }
+
+ /// Invokes the function asynchronously.
+ ///
+ /// The function invocation context detailing the function to be invoked and its arguments along with additional request information.
+ ///
+ /// The to monitor for cancellation requests. The default is .
+ /// The result of the function invocation, or if the function invocation returned .
+ /// is .
+ private async Task InvokeFunctionAsync(AutoFunctionInvocationContext context, CancellationToken cancellationToken)
+ {
+ Verify.NotNull(context);
+
+ using Activity? activity = _activitySource?.StartActivity(context.Function.Name);
+
+ long startingTimestamp = 0;
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ startingTimestamp = Stopwatch.GetTimestamp();
+ if (_logger.IsEnabled(LogLevel.Trace))
+ {
+ LogInvokingSensitive(context.Function.Name, LoggingAsJson(context.CallContent.Arguments, context.AIFunction.JsonSerializerOptions));
+ }
+ else
+ {
+ LogInvoking(context.Function.Name);
+ }
+ }
+
+ object? result = null;
+ try
+ {
+ CurrentContext = context; // doesn't need to be explicitly reset after, as that's handled automatically at async method exit
+ context = await this.OnAutoFunctionInvocationAsync(
+ context,
+ async (ctx) =>
+ {
+ // Check if filter requested termination
+ if (ctx.Terminate)
+ {
+ return;
+ }
+
+ // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any
+ // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here,
+ // as the called function could in turn telling the model about itself as a possible candidate for invocation.
+ result = await context.AIFunction.InvokeAsync(new(context.Arguments), cancellationToken).ConfigureAwait(false);
+ ctx.Result = new FunctionResult(ctx.Function, result);
+ }).ConfigureAwait(false);
+ result = context.Result.GetValue();
+ }
+ catch (Exception e)
+ {
+ if (activity is not null)
+ {
+ _ = activity.SetTag("error.type", e.GetType().FullName)
+ .SetStatus(ActivityStatusCode.Error, e.Message);
+ }
+
+ if (e is OperationCanceledException)
+ {
+ LogInvocationCanceled(context.Function.Name);
+ }
+ else
+ {
+ LogInvocationFailed(context.Function.Name, e);
+ }
+
+ throw;
+ }
+ finally
+ {
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ TimeSpan elapsed = GetElapsedTime(startingTimestamp);
+
+ if (result is not null && _logger.IsEnabled(LogLevel.Trace))
+ {
+ LogInvocationCompletedSensitive(context.Function.Name, elapsed, LoggingAsJson(result, context.AIFunction.JsonSerializerOptions));
+ }
+ else
+ {
+ LogInvocationCompleted(context.Function.Name, elapsed);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /// Serializes as JSON for logging purposes.
+ private static string LoggingAsJson(T value, JsonSerializerOptions? options)
+ {
+ if (options?.TryGetTypeInfo(typeof(T), out var typeInfo) is true ||
+ AIJsonUtilities.DefaultOptions.TryGetTypeInfo(typeof(T), out typeInfo))
+ {
+#pragma warning disable CA1031 // Do not catch general exception types
+ try
+ {
+ return JsonSerializer.Serialize(value, typeInfo);
+ }
+ catch
+ {
+ }
+#pragma warning restore CA1031 // Do not catch general exception types
+ }
+
+ // If we're unable to get a type info for the value, or if we fail to serialize,
+ // return an empty JSON object. We do not want lack of type info to disrupt application behavior with exceptions.
+ return "{}";
+ }
+
+ private static TimeSpan GetElapsedTime(long startingTimestamp) =>
+#if NET
+ Stopwatch.GetElapsedTime(startingTimestamp);
+#else
+ new((long)((Stopwatch.GetTimestamp() - startingTimestamp) * ((double)TimeSpan.TicksPerSecond / Stopwatch.Frequency)));
+#endif
+
+ [LoggerMessage(LogLevel.Debug, "Invoking {MethodName}.", SkipEnabledCheck = true)]
+ private partial void LogInvoking(string methodName);
+
+ [LoggerMessage(LogLevel.Trace, "Invoking {MethodName}({Arguments}).", SkipEnabledCheck = true)]
+ private partial void LogInvokingSensitive(string methodName, string arguments);
+
+ [LoggerMessage(LogLevel.Debug, "{MethodName} invocation completed. Duration: {Duration}", SkipEnabledCheck = true)]
+ private partial void LogInvocationCompleted(string methodName, TimeSpan duration);
+
+ [LoggerMessage(LogLevel.Trace, "{MethodName} invocation completed. Duration: {Duration}. Result: {Result}", SkipEnabledCheck = true)]
+ private partial void LogInvocationCompletedSensitive(string methodName, TimeSpan duration, string result);
+
+ [LoggerMessage(LogLevel.Debug, "{MethodName} invocation canceled.")]
+ private partial void LogInvocationCanceled(string methodName);
+
+ [LoggerMessage(LogLevel.Error, "{MethodName} invocation failed.")]
+ private partial void LogInvocationFailed(string methodName, Exception error);
+
+ /// Provides information about the invocation of a function call.
+ public sealed class FunctionInvocationResult
+ {
+ internal FunctionInvocationResult(bool shouldTerminate, FunctionInvokingChatClient.FunctionInvocationStatus status, FunctionCallContent callContent, object? result, Exception? exception)
+ {
+ ShouldTerminate = shouldTerminate;
+ Status = status;
+ CallContent = callContent;
+ Result = result;
+ Exception = exception;
+ }
+
+ /// Gets status about how the function invocation completed.
+ public FunctionInvokingChatClient.FunctionInvocationStatus Status { get; }
+
+ /// Gets the function call content information associated with this invocation.
+ public FunctionCallContent CallContent { get; }
+
+ /// Gets the result of the function call.
+ public object? Result { get; }
+
+ /// Gets any exception the function call threw.
+ public Exception? Exception { get; }
+
+ /// Gets a value indicating whether the caller should terminate the processing loop.
+ internal bool ShouldTerminate { get; }
+ }
+}
From 3df56aaa7ab739987d3f7a7e2154762636026f37 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Sat, 19 Apr 2025 18:18:11 +0100
Subject: [PATCH 02/24] Adding FunctionInvocationContextV2 and making
AutoFunctionInvocation a specialization of it
---
.../FunctionInvocationContextV2.Extra.cs | 125 ++++++++++++++++++
.../ChatClient/FunctionInvocationContextV2.cs | 101 ++++++++++++++
.../FunctionInvokingChatClientV2.cs | 57 +++++---
.../KernelFunctionInvokingChatClient.cs | 62 +++------
.../KernelFunctionInvokingChatClientOld.cs | 2 +-
.../AutoFunctionInvocationContext.cs | 95 ++++---------
6 files changed, 311 insertions(+), 131 deletions(-)
create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.Extra.cs
create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.cs
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.Extra.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.Extra.cs
new file mode 100644
index 000000000000..d1a20e00c348
--- /dev/null
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.Extra.cs
@@ -0,0 +1,125 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using Microsoft.Shared.Collections;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.AI;
+
+/// Provides context for an in-flight function invocation.
+public partial class FunctionInvocationContextV2
+{
+ private static class Throw
+ {
+ ///
+ /// Throws an if the specified argument is .
+ ///
+ /// Argument type to be checked for .
+ /// Object to be checked for .
+ /// The name of the parameter being checked.
+ /// The original value of .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [return: NotNull]
+ public static T IfNull([NotNull] T argument, [CallerArgumentExpression(nameof(argument))] string paramName = "")
+ {
+ if (argument is null)
+ {
+ throw new ArgumentNullException(paramName);
+ }
+
+ return argument;
+ }
+
+ ///
+ /// Throws an .
+ ///
+ /// A message that describes the error.
+#if !NET6_0_OR_GREATER
+ [MethodImpl(MethodImplOptions.NoInlining)]
+#endif
+ [DoesNotReturn]
+ public static void InvalidOperationException(string message)
+ => throw new InvalidOperationException(message);
+
+ ///
+ /// Throws an .
+ ///
+ /// The name of the parameter that caused the exception.
+#if !NET6_0_OR_GREATER
+ [MethodImpl(MethodImplOptions.NoInlining)]
+#endif
+ [DoesNotReturn]
+ public static void ArgumentOutOfRangeException(string paramName)
+ => throw new ArgumentOutOfRangeException(paramName);
+
+ ///
+ /// Throws an .
+ ///
+ /// The name of the parameter that caused the exception.
+ /// A message that describes the error.
+#if !NET6_0_OR_GREATER
+ [MethodImpl(MethodImplOptions.NoInlining)]
+#endif
+ [DoesNotReturn]
+ public static void ArgumentOutOfRangeException(string paramName, string? message)
+ => throw new ArgumentOutOfRangeException(paramName, message);
+
+ ///
+ /// Throws an .
+ ///
+ /// The name of the parameter that caused the exception.
+ /// The value of the argument that caused this exception.
+ /// A message that describes the error.
+#if !NET6_0_OR_GREATER
+ [MethodImpl(MethodImplOptions.NoInlining)]
+#endif
+ [DoesNotReturn]
+ public static void ArgumentOutOfRangeException(string paramName, object? actualValue, string? message)
+ => throw new ArgumentOutOfRangeException(paramName, actualValue, message);
+
+ ///
+ /// Throws an if the specified number is less than min.
+ ///
+ /// Number to be expected being less than min.
+ /// The number that must be less than the argument.
+ /// The name of the parameter being checked.
+ /// The original value of .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int IfLessThan(int argument, int min, [CallerArgumentExpression(nameof(argument))] string paramName = "")
+ {
+ if (argument < min)
+ {
+ ArgumentOutOfRangeException(paramName, argument, $"Argument less than minimum value {min}");
+ }
+
+ return argument;
+ }
+ }
+
+ private static class LoggingHelpers
+ {
+ /// Serializes as JSON for logging purposes.
+ public static string AsJson(T value, System.Text.Json.JsonSerializerOptions? options)
+ {
+ if (options?.TryGetTypeInfo(typeof(T), out var typeInfo) is true ||
+ AIJsonUtilities.DefaultOptions.TryGetTypeInfo(typeof(T), out typeInfo))
+ {
+ try
+ {
+ return System.Text.Json.JsonSerializer.Serialize(value, typeInfo);
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch
+#pragma warning restore CA1031 // Do not catch general exception types
+ {
+ }
+ }
+
+ return "{}";
+ }
+ }
+}
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.cs
new file mode 100644
index 000000000000..d096656fba56
--- /dev/null
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.cs
@@ -0,0 +1,101 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.AI;
+
+/// Provides context for an in-flight function invocation.
+public partial class FunctionInvocationContextV2
+{
+ ///
+ /// A nop function used to allow to be non-nullable. Default instances of
+ /// start with this as the target function.
+ ///
+ private static readonly AIFunction _nopFunction = AIFunctionFactory.Create(() => { }, nameof(FunctionInvocationContext));
+
+ /// The chat contents associated with the operation that initiated this function call request.
+ private IList _messages = Array.Empty();
+
+ /// The AI function to be invoked.
+ private AIFunction _function = _nopFunction;
+
+ /// The function call content information associated with this invocation.
+ private FunctionCallContent? _callContent;
+
+ /// The arguments used with the function.
+ private AIFunctionArguments? _arguments;
+
+ /// Initializes a new instance of the class.
+ public FunctionInvocationContextV2()
+ {
+ }
+
+ /// Gets or sets the AI function to be invoked.
+ public AIFunction Function
+ {
+ get => _function;
+ set => _function = Throw.IfNull(value);
+ }
+
+ /// Gets or sets the arguments associated with this invocation.
+ public AIFunctionArguments Arguments
+ {
+ get => _arguments ??= [];
+ set => _arguments = Throw.IfNull(value);
+ }
+
+ /// Gets or sets the function call content information associated with this invocation.
+ public FunctionCallContent CallContent
+ {
+ get => _callContent ??= new(string.Empty, _nopFunction.Name, EmptyReadOnlyDictionary.Instance);
+ set => _callContent = Throw.IfNull(value);
+ }
+
+ /// Gets or sets the chat contents associated with the operation that initiated this function call request.
+ public IList Messages
+ {
+ get => _messages;
+ set => _messages = Throw.IfNull(value);
+ }
+
+ /// Gets or sets the chat options associated with the operation that initiated this function call request.
+ public ChatOptions? Options { get; set; }
+
+ /// Gets or sets the number of this iteration with the underlying client.
+ ///
+ /// The initial request to the client that passes along the chat contents provided to the
+ /// is iteration 1. If the client responds with a function call request, the next request to the client is iteration 2, and so on.
+ ///
+ public int Iteration { get; set; }
+
+ /// Gets or sets the index of the function call within the iteration.
+ ///
+ /// The response from the underlying client may include multiple function call requests.
+ /// This index indicates the position of the function call within the iteration.
+ ///
+ public int FunctionCallIndex { get; set; }
+
+ /// Gets or sets the total number of function call requests within the iteration.
+ ///
+ /// The response from the underlying client might include multiple function call requests.
+ /// This count indicates how many there were.
+ ///
+ public int FunctionCount { get; set; }
+
+ /// Gets or sets a value indicating whether to terminate the request.
+ ///
+ /// In response to a function call request, the function might be invoked, its result added to the chat contents,
+ /// and a new request issued to the wrapped client. If this property is set to , that subsequent request
+ /// will not be issued and instead the loop immediately terminated rather than continuing until there are no
+ /// more function call requests in responses.
+ ///
+ public bool Terminate { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the context is happening in a streaming scenario.
+ ///
+ public bool IsStreaming { get; set; }
+}
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
index f2f3d9f12a62..a5ceb8254089 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
@@ -47,18 +47,18 @@ namespace Microsoft.Extensions.AI;
///
public partial class FunctionInvokingChatClientV2 : DelegatingChatClient
{
- /// The for the current function invocation.
- private static readonly AsyncLocal _currentContext = new();
+ /// The for the current function invocation.
+ private static readonly AsyncLocal _currentContext = new();
/// Optional services used for function invocation.
private readonly IServiceProvider? _functionInvocationServices;
/// The logger to use for logging information about function invocation.
- protected readonly ILogger _logger;
+ private readonly ILogger _logger;
/// The to use for telemetry.
/// This component does not own the instance and should not dispose it.
- protected readonly ActivitySource? _activitySource;
+ private readonly ActivitySource? _activitySource;
/// Maximum number of roundtrips allowed to the inner client.
private int _maximumIterationsPerRequest = 10;
@@ -81,12 +81,12 @@ public FunctionInvokingChatClientV2(IChatClient innerClient, ILoggerFactory? log
}
///
- /// Gets or sets the for the current function invocation.
+ /// Gets or sets the for the current function invocation.
///
///
/// This value flows across async calls.
///
- public static FunctionInvocationContext? CurrentContext
+ public static FunctionInvocationContextV2? CurrentContext
{
get => _currentContext.Value;
protected set => _currentContext.Value = value;
@@ -205,6 +205,12 @@ public int MaximumConsecutiveErrorsPerRequest
set => _maximumConsecutiveErrorsPerRequest = Throw.IfLessThan(value, 0);
}
+ /// Optional services used for function invocation.
+ protected IServiceProvider? FunctionInvocationServices
+ {
+ get => _functionInvocationServices;
+ }
+
///
public override async Task GetResponseAsync(
IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
@@ -279,7 +285,7 @@ public override async Task GetResponseAsync(
// Add the responses from the function calls into the augmented history and also into the tracked
// list of response messages.
- var modeAndMessages = await ProcessFunctionCallsAsync(response, augmentedHistory, options!, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: false, _functionInvocationServices, cancellationToken);
+ var modeAndMessages = await ProcessFunctionCallsAsync(response, augmentedHistory, options!, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: false, cancellationToken);
responseMessages.AddRange(modeAndMessages.MessagesAdded);
consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount;
@@ -356,7 +362,7 @@ public override async IAsyncEnumerable GetStreamingResponseA
FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadThreadId);
// Process all of the functions, adding their results into the history.
- var modeAndMessages = await ProcessFunctionCallsAsync(response, augmentedHistory, options, functionCallContents, iteration, consecutiveErrorCount, isStreaming: true, _functionInvocationServices, cancellationToken);
+ var modeAndMessages = await ProcessFunctionCallsAsync(response, augmentedHistory, options, functionCallContents, iteration, consecutiveErrorCount, isStreaming: true, cancellationToken);
responseMessages.AddRange(modeAndMessages.MessagesAdded);
consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount;
@@ -520,11 +526,10 @@ private static void UpdateOptionsForNextIteration(ref ChatOptions options, strin
/// The iteration number of how many roundtrips have been made to the inner client.
/// The number of consecutive iterations, prior to this one, that were recorded as having function invocation errors.
/// Whether the function calls are being processed in a streaming context.
- /// The services to use for function invocation.
/// The to monitor for cancellation requests.
/// A value indicating how the caller should proceed.
protected virtual async Task<(bool ShouldTerminate, int NewConsecutiveErrorCount, IList MessagesAdded)> ProcessFunctionCallsAsync(
- ChatResponse response, List messages, ChatOptions options, List functionCallContents, int iteration, int consecutiveErrorCount, bool isStreaming, IServiceProvider functionInvocationServices, CancellationToken cancellationToken)
+ ChatResponse response, List messages, ChatOptions options, List functionCallContents, int iteration, int consecutiveErrorCount, bool isStreaming, CancellationToken cancellationToken)
{
// We must add a response for every tool call, regardless of whether we successfully executed it or not.
// If we successfully execute it, we'll add the result. If we don't, we'll add an error.
@@ -537,7 +542,7 @@ private static void UpdateOptionsForNextIteration(ref ChatOptions options, strin
if (functionCallContents.Count == 1)
{
FunctionInvocationResult result = await ProcessFunctionCallAsync(
- messages, options, functionCallContents, iteration, 0, captureCurrentIterationExceptions, isStreaming, functionInvocationServices, cancellationToken);
+ messages, options, functionCallContents, iteration, 0, captureCurrentIterationExceptions, isStreaming, cancellationToken);
IList added = CreateResponseMessages([result]);
ThrowIfNoFunctionResultsAdded(added);
@@ -560,7 +565,7 @@ private static void UpdateOptionsForNextIteration(ref ChatOptions options, strin
from i in Enumerable.Range(0, functionCallContents.Count)
select ProcessFunctionCallAsync(
messages, options, functionCallContents,
- iteration, i, captureExceptions: true, isStreaming, functionInvocationServices, cancellationToken));
+ iteration, i, captureExceptions: true, isStreaming, cancellationToken));
}
else
{
@@ -570,7 +575,7 @@ select ProcessFunctionCallAsync(
{
results[i] = await ProcessFunctionCallAsync(
messages, options, functionCallContents,
- iteration, i, captureCurrentIterationExceptions, isStreaming, functionInvocationServices, cancellationToken);
+ iteration, i, captureCurrentIterationExceptions, isStreaming, cancellationToken);
}
}
@@ -590,6 +595,13 @@ select ProcessFunctionCallAsync(
}
}
+#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection
+ ///
+ /// TODO: Documentation
+ ///
+ ///
+ ///
+ ///
protected void UpdateConsecutiveErrorCountOrThrow(IList added, ref int consecutiveErrorCount)
{
var allExceptions = added.SelectMany(m => m.Contents.OfType())
@@ -617,6 +629,7 @@ protected void UpdateConsecutiveErrorCountOrThrow(IList added, ref
consecutiveErrorCount = 0;
}
}
+#pragma warning restore CA1851
///
/// Throws an exception if doesn't create any messages.
@@ -637,12 +650,11 @@ protected void ThrowIfNoFunctionResultsAdded(IList? messages)
/// The 0-based index of the function being called out of .
/// If true, handles function-invocation exceptions by returning a value with . Otherwise, rethrows.
/// Whether the function calls are being processed in a streaming context.
- /// The services to use for function invocation.
/// The to monitor for cancellation requests.
/// A value indicating how the caller should proceed.
protected virtual async Task ProcessFunctionCallAsync(
List messages, ChatOptions options, List callContents,
- int iteration, int functionCallIndex, bool captureExceptions, bool isStreaming, IServiceProvider functionInvocationServices, CancellationToken cancellationToken)
+ int iteration, int functionCallIndex, bool captureExceptions, bool isStreaming, CancellationToken cancellationToken)
{
var callContent = callContents[functionCallIndex];
@@ -653,10 +665,10 @@ protected virtual async Task ProcessFunctionCallAsync(
return new(shouldTerminate: false, FunctionInvocationStatus.NotFound, callContent, result: null, exception: null);
}
- FunctionInvocationContext context = new()
+ FunctionInvocationContextV2 context = new()
{
Function = function,
- Arguments = new(callContent.Arguments) { Services = functionInvocationServices },
+ Arguments = new(callContent.Arguments) { Services = _functionInvocationServices },
Messages = messages,
Options = options,
@@ -665,6 +677,7 @@ protected virtual async Task ProcessFunctionCallAsync(
Iteration = iteration,
FunctionCallIndex = functionCallIndex,
FunctionCount = callContents.Count,
+ IsStreaming = isStreaming
};
object? result;
@@ -746,7 +759,7 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul
/// The to monitor for cancellation requests. The default is .
/// The result of the function invocation, or if the function invocation returned .
/// is .
- protected virtual async Task InvokeFunctionAsync(FunctionInvocationContext context, CancellationToken cancellationToken)
+ protected virtual async Task InvokeFunctionAsync(FunctionInvocationContextV2 context, CancellationToken cancellationToken)
{
_ = Throw.IfNull(context);
@@ -811,7 +824,13 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul
return result;
}
- protected virtual async Task<(FunctionInvocationContext Context, object? Result)> TryInvokeFunctionAsync(FunctionInvocationContext context, CancellationToken cancellationToken)
+ ///
+ /// This method will execute auto function invocation filters and function recursively within the try block
+ ///
+ /// The function invocation context.
+ /// Cancellation token.
+ /// The function invocation context and result.
+ protected virtual async Task<(FunctionInvocationContextV2 context, object? result)> TryInvokeFunctionAsync(FunctionInvocationContextV2 context, CancellationToken cancellationToken)
{
var result = await context.Function.InvokeAsync(context.Arguments, cancellationToken);
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
index fc4968a390ed..7098315fcd03 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
@@ -11,42 +11,15 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
-#pragma warning disable IDE1006 // Naming Styles
-#pragma warning disable IDE0009 // This
-#pragma warning disable CA2213 // Disposable fields should be disposed
-#pragma warning disable EA0002 // Use 'System.TimeProvider' to make the code easier to test
-#pragma warning disable SA1202 // 'protected' members should come before 'private' members
-
// Modified source from 2025-04-07
// https://raw.githubusercontent.com/dotnet/extensions/84d09b794d994435568adcbb85a981143d4f15cb/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs
namespace Microsoft.Extensions.AI;
///
-/// A delegating chat client that invokes functions defined on .
-/// Include this in a chat pipeline to resolve function calls automatically.
+/// Specialization of that uses and supports .
///
-///
-///
-/// When this client receives a in a chat response, it responds
-/// by calling the corresponding defined in ,
-/// producing a that it sends back to the inner client. This loop
-/// is repeated until there are no more function calls to make, or until another stop condition is met,
-/// such as hitting .
-///
-///
-/// The provided implementation of is thread-safe for concurrent use so long as the
-/// instances employed as part of the supplied are also safe.
-/// The property can be used to control whether multiple function invocation
-/// requests as part of the same request are invocable concurrently, but even with that set to
-/// (the default), multiple concurrent requests to this same instance and using the same tools could result in those
-/// tools being used concurrently (one per request). For example, a function that accesses the HttpContext of a specific
-/// ASP.NET web request should only be used as part of a single at a time, and only with
-/// set to , in case the inner client decided to issue multiple
-/// invocation requests to that same function.
-///
-///
-public partial class KernelFunctionInvokingChatClient : FunctionInvokingChatClientV2
+internal sealed class KernelFunctionInvokingChatClient : FunctionInvokingChatClientV2
{
///
public KernelFunctionInvokingChatClient(IChatClient innerClient, ILoggerFactory? loggerFactory = null, IServiceProvider? functionInvocationServices = null)
@@ -87,7 +60,7 @@ private static void ClearOptionsForAutoFunctionInvocation(ref ChatOptions option
///
protected override async Task<(bool ShouldTerminate, int NewConsecutiveErrorCount, IList MessagesAdded)> ProcessFunctionCallsAsync(
- ChatResponse response, List messages, ChatOptions options, List functionCallContents, int iteration, int consecutiveErrorCount, bool isStreaming, IServiceProvider functionInvocationServices, CancellationToken cancellationToken)
+ ChatResponse response, List messages, ChatOptions options, List functionCallContents, int iteration, int consecutiveErrorCount, bool isStreaming, CancellationToken cancellationToken)
{
// Prepare the options for the next auto function invocation iteration.
UpdateOptionsForAutoFunctionInvocation(ref options!, response.Messages.Last().ToChatMessageContent(), isStreaming: false);
@@ -104,7 +77,7 @@ private static void ClearOptionsForAutoFunctionInvocation(ref ChatOptions option
if (functionCallContents.Count == 1)
{
FunctionInvocationResult functionResult = await this.ProcessFunctionCallAsync(
- messages, options, functionCallContents, iteration, 0, captureCurrentIterationExceptions, isStreaming, functionInvocationServices, cancellationToken).ConfigureAwait(false);
+ messages, options, functionCallContents, iteration, 0, captureCurrentIterationExceptions, isStreaming, cancellationToken).ConfigureAwait(false);
IList added = this.CreateResponseMessages([functionResult]);
this.ThrowIfNoFunctionResultsAdded(added);
@@ -128,7 +101,7 @@ private static void ClearOptionsForAutoFunctionInvocation(ref ChatOptions option
from i in Enumerable.Range(0, functionCallContents.Count)
select this.ProcessFunctionCallAsync(
messages, options, functionCallContents,
- iteration, i, captureExceptions: true, isStreaming, functionInvocationServices, cancellationToken)).ConfigureAwait(false));
+ iteration, i, captureExceptions: true, isStreaming, cancellationToken)).ConfigureAwait(false));
terminationRequested = results.Any(r => r.ShouldTerminate);
}
@@ -139,12 +112,12 @@ select this.ProcessFunctionCallAsync(
{
var functionResult = await this.ProcessFunctionCallAsync(
messages, options, functionCallContents,
- iteration, i, captureCurrentIterationExceptions, isStreaming, functionInvocationServices, cancellationToken).ConfigureAwait(false);
+ iteration, i, captureCurrentIterationExceptions, isStreaming, cancellationToken).ConfigureAwait(false);
results.Add(functionResult);
// Differently from the original FunctionInvokingChatClient, as soon the termination is requested,
- // we stop and don't continue
+ // we stop and don't continue, if that can be parametrized, this override won't be needed
if (functionResult.ShouldTerminate)
{
shouldTerminate = true;
@@ -182,7 +155,7 @@ select this.ProcessFunctionCallAsync(
///
protected override async Task ProcessFunctionCallAsync(
List messages, ChatOptions options, List callContents,
- int iteration, int functionCallIndex, bool captureExceptions, bool isStreaming, IServiceProvider functionInvocationServices, CancellationToken cancellationToken)
+ int iteration, int functionCallIndex, bool captureExceptions, bool isStreaming, CancellationToken cancellationToken)
{
var callContent = callContents[functionCallIndex];
@@ -201,7 +174,7 @@ protected override async Task ProcessFunctionCallAsync
var context = new AutoFunctionInvocationContext(new()
{
Function = function,
- Arguments = new(callContent.Arguments) { Services = functionInvocationServices },
+ Arguments = new(callContent.Arguments) { Services = this.FunctionInvocationServices },
Messages = messages,
Options = options,
@@ -216,7 +189,7 @@ protected override async Task ProcessFunctionCallAsync
object? result;
try
{
- result = await InvokeFunctionAsync(context, cancellationToken).ConfigureAwait(false);
+ result = await this.InvokeFunctionAsync(context, cancellationToken).ConfigureAwait(false);
}
catch (Exception e) when (!cancellationToken.IsCancellationRequested)
{
@@ -227,7 +200,7 @@ protected override async Task ProcessFunctionCallAsync
return new(
shouldTerminate: false,
- FunctionInvokingChatClient.FunctionInvocationStatus.Exception,
+ FunctionInvocationStatus.Exception,
callContent,
result: null,
exception: e);
@@ -235,7 +208,7 @@ protected override async Task ProcessFunctionCallAsync
return new(
shouldTerminate: context.Terminate,
- FunctionInvokingChatClient.FunctionInvocationStatus.RanToCompletion,
+ FunctionInvocationStatus.RanToCompletion,
callContent,
result,
exception: null);
@@ -259,7 +232,7 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul
Verify.NotNull(result);
object? functionResult;
- if (result.Status == FunctionInvokingChatClient.FunctionInvocationStatus.RanToCompletion)
+ if (result.Status == FunctionInvocationStatus.RanToCompletion)
{
functionResult = result.Result ?? "Success: Function completed.";
}
@@ -267,12 +240,12 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul
{
string message = result.Status switch
{
- FunctionInvokingChatClient.FunctionInvocationStatus.NotFound => $"Error: Requested function \"{result.CallContent.Name}\" not found.",
- FunctionInvokingChatClient.FunctionInvocationStatus.Exception => "Error: Function failed.",
+ FunctionInvocationStatus.NotFound => $"Error: Requested function \"{result.CallContent.Name}\" not found.",
+ FunctionInvocationStatus.Exception => "Error: Function failed.",
_ => "Error: Unknown error.",
};
- if (IncludeDetailedErrors && result.Exception is not null)
+ if (this.IncludeDetailedErrors && result.Exception is not null)
{
message = $"{message} Exception: {result.Exception.Message}";
}
@@ -326,7 +299,8 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(
}
}
- protected override async Task<(FunctionInvocationContext context, object? result)> TryInvokeFunctionAsync(FunctionInvocationContext context, CancellationToken cancellationToken)
+ ///
+ protected override async Task<(FunctionInvocationContextV2 context, object? result)> TryInvokeFunctionAsync(FunctionInvocationContextV2 context, CancellationToken cancellationToken)
{
object? result = null;
if (context is AutoFunctionInvocationContext autoContext)
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClientOld.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClientOld.cs
index 1afb2689983b..20c2b6e39aeb 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClientOld.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClientOld.cs
@@ -51,7 +51,7 @@ namespace Microsoft.Extensions.AI;
/// invocation requests to that same function.
///
///
-public partial class KernelFunctionInvokingChatClientOld : DelegatingChatClient
+internal partial class KernelFunctionInvokingChatClientOld : DelegatingChatClient
{
/// The for the current function invocation.
private static readonly AsyncLocal _currentContext = new();
diff --git a/dotnet/src/SemanticKernel.Abstractions/Filters/AutoFunctionInvocation/AutoFunctionInvocationContext.cs b/dotnet/src/SemanticKernel.Abstractions/Filters/AutoFunctionInvocation/AutoFunctionInvocationContext.cs
index bc8dd0c3490c..d4e66f48cfc3 100644
--- a/dotnet/src/SemanticKernel.Abstractions/Filters/AutoFunctionInvocation/AutoFunctionInvocationContext.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/Filters/AutoFunctionInvocation/AutoFunctionInvocationContext.cs
@@ -13,16 +13,15 @@ namespace Microsoft.SemanticKernel;
///
/// Class with data related to automatic function invocation.
///
-public class AutoFunctionInvocationContext
+public class AutoFunctionInvocationContext : FunctionInvocationContextV2
{
private ChatHistory? _chatHistory;
private KernelFunction? _kernelFunction;
- private readonly Microsoft.Extensions.AI.FunctionInvocationContext _invocationContext = new();
///
/// Initializes a new instance of the class from an existing .
///
- internal AutoFunctionInvocationContext(Microsoft.Extensions.AI.FunctionInvocationContext invocationContext)
+ internal AutoFunctionInvocationContext(Microsoft.Extensions.AI.FunctionInvocationContextV2 invocationContext)
{
Verify.NotNull(invocationContext);
Verify.NotNull(invocationContext.Options);
@@ -38,7 +37,6 @@ internal AutoFunctionInvocationContext(Microsoft.Extensions.AI.FunctionInvocatio
invocationContext.Options.AdditionalProperties.TryGetValue(ChatOptionsExtensions.PromptExecutionSettingsKey, out var executionSettings);
this.ExecutionSettings = executionSettings;
- this._invocationContext = invocationContext;
this.Result = new FunctionResult(this.Function) { Culture = kernel.Culture };
}
@@ -64,7 +62,7 @@ public AutoFunctionInvocationContext(
Verify.NotNull(chatHistory);
Verify.NotNull(chatMessageContent);
- this._invocationContext.Options = new()
+ this.Options = new()
{
AdditionalProperties = new()
{
@@ -75,9 +73,9 @@ public AutoFunctionInvocationContext(
this._kernelFunction = function;
this._chatHistory = chatHistory;
- this._invocationContext.Messages = chatHistory.ToChatMessageList();
- chatHistory.SetChatMessageHandlers(this._invocationContext.Messages);
- this._invocationContext.Function = function.AsAIFunction();
+ this.Messages = chatHistory.ToChatMessageList();
+ chatHistory.SetChatMessageHandlers(this.Messages);
+ base.Function = function.AsAIFunction();
this.Result = result;
}
@@ -87,18 +85,13 @@ public AutoFunctionInvocationContext(
///
public CancellationToken CancellationToken { get; init; }
- ///
- /// Boolean flag which indicates whether a filter is invoked within streaming or non-streaming mode.
- ///
- public bool IsStreaming { get; init; }
-
///
/// Gets the arguments associated with the operation.
///
- public KernelArguments? Arguments
+ public new KernelArguments? Arguments
{
- get => this._invocationContext.CallContent.Arguments is KernelArguments kernelArguments ? kernelArguments : null;
- init => this._invocationContext.CallContent.Arguments = value;
+ get => this.CallContent.Arguments is KernelArguments kernelArguments ? kernelArguments : null;
+ init => this.CallContent.Arguments = value;
}
///
@@ -106,8 +99,8 @@ public KernelArguments? Arguments
///
public int RequestSequenceIndex
{
- get => this._invocationContext.Iteration;
- init => this._invocationContext.Iteration = value;
+ get => this.Iteration;
+ init => this.Iteration = value;
}
///
@@ -115,19 +108,8 @@ public int RequestSequenceIndex
///
public int FunctionSequenceIndex
{
- get => this._invocationContext.FunctionCallIndex;
- init => this._invocationContext.FunctionCallIndex = value;
- }
-
- /// Gets or sets the total number of function call requests within the iteration.
- ///
- /// The response from the underlying client might include multiple function call requests.
- /// This count indicates how many there were.
- ///
- public int FunctionCount
- {
- get => this._invocationContext.FunctionCount;
- init => this._invocationContext.FunctionCount = value;
+ get => this.FunctionCallIndex;
+ init => this.FunctionCallIndex = value;
}
///
@@ -135,13 +117,13 @@ public int FunctionCount
///
public string? ToolCallId
{
- get => this._invocationContext.CallContent.CallId;
+ get => this.CallContent.CallId;
init
{
- this._invocationContext.CallContent = new Microsoft.Extensions.AI.FunctionCallContent(
+ this.CallContent = new Microsoft.Extensions.AI.FunctionCallContent(
callId: value ?? string.Empty,
- name: this._invocationContext.CallContent.Name,
- arguments: this._invocationContext.CallContent.Arguments);
+ name: this.CallContent.Name,
+ arguments: this.CallContent.Arguments);
}
}
@@ -149,40 +131,40 @@ public string? ToolCallId
/// The chat message content associated with automatic function invocation.
///
public ChatMessageContent ChatMessageContent
- => (this._invocationContext.Options?.AdditionalProperties?[ChatOptionsExtensions.ChatMessageContentKey] as ChatMessageContent)!;
+ => (this.Options?.AdditionalProperties?[ChatOptionsExtensions.ChatMessageContentKey] as ChatMessageContent)!;
///
/// The execution settings associated with the operation.
///
public PromptExecutionSettings? ExecutionSettings
{
- get => this._invocationContext.Options?.AdditionalProperties?[ChatOptionsExtensions.PromptExecutionSettingsKey] as PromptExecutionSettings;
+ get => this.Options?.AdditionalProperties?[ChatOptionsExtensions.PromptExecutionSettingsKey] as PromptExecutionSettings;
init
{
- this._invocationContext.Options ??= new();
- this._invocationContext.Options.AdditionalProperties ??= [];
- this._invocationContext.Options.AdditionalProperties[ChatOptionsExtensions.PromptExecutionSettingsKey] = value;
+ this.Options ??= new();
+ this.Options.AdditionalProperties ??= [];
+ this.Options.AdditionalProperties[ChatOptionsExtensions.PromptExecutionSettingsKey] = value;
}
}
///
/// Gets the associated with automatic function invocation.
///
- public ChatHistory ChatHistory => this._chatHistory ??= new ChatMessageHistory(this._invocationContext.Messages);
+ public ChatHistory ChatHistory => this._chatHistory ??= new ChatMessageHistory(this.Messages);
///
/// Gets the with which this filter is associated.
///
- public KernelFunction Function
+ public new KernelFunction Function
{
get
{
if (this._kernelFunction is null
// If the schemas are different,
// AIFunction reference potentially was modified and the kernel function should be regenerated.
- || !IsSameSchema(this._kernelFunction, this._invocationContext.Function))
+ || !IsSameSchema(this._kernelFunction, base.Function))
{
- this._kernelFunction = this._invocationContext.Function.AsKernelFunction();
+ this._kernelFunction = base.Function.AsKernelFunction();
}
return this._kernelFunction;
@@ -197,7 +179,7 @@ public Kernel Kernel
get
{
Kernel? kernel = null;
- this._invocationContext.Options?.AdditionalProperties?.TryGetValue(ChatOptionsExtensions.KernelKey, out kernel);
+ this.Options?.AdditionalProperties?.TryGetValue(ChatOptionsExtensions.KernelKey, out kernel);
// To avoid exception from properties, when attempting to retrieve a kernel from a non-ready context, it will give a null.
return kernel!;
@@ -209,30 +191,9 @@ public Kernel Kernel
///
public FunctionResult Result { get; set; }
- /// Gets or sets a value indicating whether to terminate the request.
- ///
- /// In response to a function call request, the function might be invoked, its result added to the chat contents,
- /// and a new request issued to the wrapped client. If this property is set to , that subsequent request
- /// will not be issued and instead the loop immediately terminated rather than continuing until there are no
- /// more function call requests in responses.
- ///
- public bool Terminate
- {
- get => this._invocationContext.Terminate;
- set => this._invocationContext.Terminate = value;
- }
-
- /// Gets or sets the function call content information associated with this invocation.
- internal Microsoft.Extensions.AI.FunctionCallContent CallContent
- {
- get => this._invocationContext.CallContent;
- set => this._invocationContext.CallContent = value;
- }
-
internal AIFunction AIFunction
{
- get => this._invocationContext.Function;
- set => this._invocationContext.Function = value;
+ get => base.Function;
}
private static bool IsSameSchema(KernelFunction kernelFunction, AIFunction aiFunction)
From 48b50ee3a2b50643a5b1fdc560a6d4e54bd386ac Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Sat, 19 Apr 2025 19:35:41 +0100
Subject: [PATCH 03/24] Most Tests passing, missing identify reference issue
---
.../FunctionInvokingChatClientV2.cs | 2 +-
.../KernelFunctionInvokingChatClient.cs | 21 +-
.../KernelFunctionInvokingChatClientOld.cs | 1023 -----------------
.../AutoFunctionInvocationContext.cs | 36 +-
4 files changed, 36 insertions(+), 1046 deletions(-)
delete mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClientOld.cs
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
index a5ceb8254089..e155ad7a68d3 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
@@ -783,7 +783,7 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul
try
{
CurrentContext = context; // doesn't need to be explicitly reset after, as that's handled automatically at async method exit
- result = await TryInvokeFunctionAsync(context, cancellationToken);
+ (context, result) = await TryInvokeFunctionAsync(context, cancellationToken);
}
catch (Exception e)
{
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
index 7098315fcd03..0415a6dc648b 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
@@ -166,25 +166,22 @@ protected override async Task ProcessFunctionCallAsync
return new(shouldTerminate: false, FunctionInvocationStatus.NotFound, callContent, result: null, exception: null);
}
- if (callContent.Arguments is not null)
- {
- callContent.Arguments = new KernelArguments(callContent.Arguments);
- }
+ //if (callContent.Arguments is not null)
+ //{
+ // callContent.Arguments = new KernelArguments(callContent.Arguments);
+ //}
- var context = new AutoFunctionInvocationContext(new()
+ var context = new AutoFunctionInvocationContext(options)
{
- Function = function,
- Arguments = new(callContent.Arguments) { Services = this.FunctionInvocationServices },
-
+ AIFunction = function,
+ AIArguments = new AIFunctionArguments(callContent.Arguments) { Services = this.FunctionInvocationServices },
Messages = messages,
- Options = options,
-
CallContent = callContent,
Iteration = iteration,
FunctionCallIndex = functionCallIndex,
FunctionCount = callContents.Count,
- })
- { IsStreaming = isStreaming };
+ IsStreaming = isStreaming
+ };
object? result;
try
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClientOld.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClientOld.cs
deleted file mode 100644
index 20c2b6e39aeb..000000000000
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClientOld.cs
+++ /dev/null
@@ -1,1023 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-#pragma warning restore IDE0073 // The file header does not match the required text
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Runtime.ExceptionServices;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.ChatCompletion;
-
-#pragma warning disable IDE1006 // Naming Styles
-#pragma warning disable IDE0009 // This
-#pragma warning disable CA2213 // Disposable fields should be disposed
-#pragma warning disable EA0002 // Use 'System.TimeProvider' to make the code easier to test
-#pragma warning disable SA1202 // 'protected' members should come before 'private' members
-
-// Modified source from 2025-04-07
-// https://raw.githubusercontent.com/dotnet/extensions/84d09b794d994435568adcbb85a981143d4f15cb/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// A delegating chat client that invokes functions defined on .
-/// Include this in a chat pipeline to resolve function calls automatically.
-///
-///
-///
-/// When this client receives a in a chat response, it responds
-/// by calling the corresponding defined in ,
-/// producing a that it sends back to the inner client. This loop
-/// is repeated until there are no more function calls to make, or until another stop condition is met,
-/// such as hitting .
-///
-///
-/// The provided implementation of is thread-safe for concurrent use so long as the
-/// instances employed as part of the supplied are also safe.
-/// The property can be used to control whether multiple function invocation
-/// requests as part of the same request are invocable concurrently, but even with that set to
-/// (the default), multiple concurrent requests to this same instance and using the same tools could result in those
-/// tools being used concurrently (one per request). For example, a function that accesses the HttpContext of a specific
-/// ASP.NET web request should only be used as part of a single at a time, and only with
-/// set to , in case the inner client decided to issue multiple
-/// invocation requests to that same function.
-///
-///
-internal partial class KernelFunctionInvokingChatClientOld : DelegatingChatClient
-{
- /// The for the current function invocation.
- private static readonly AsyncLocal _currentContext = new();
-
- /// Optional services used for function invocation.
- private readonly IServiceProvider? _functionInvocationServices;
-
- /// The logger to use for logging information about function invocation.
- private readonly ILogger _logger;
-
- /// The to use for telemetry.
- /// This component does not own the instance and should not dispose it.
- private readonly ActivitySource? _activitySource;
-
- /// Maximum number of roundtrips allowed to the inner client.
- private int _maximumIterationsPerRequest = 10;
-
- /// Maximum number of consecutive iterations that are allowed contain at least one exception result. If the limit is exceeded, we rethrow the exception instead of continuing.
- private int _maximumConsecutiveErrorsPerRequest = 3;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The underlying , or the next instance in a chain of clients.
- /// An to use for logging information about function invocation.
- /// An optional to use for resolving services required by the instances being invoked.
- public KernelFunctionInvokingChatClientOld(IChatClient innerClient, ILoggerFactory? loggerFactory = null, IServiceProvider? functionInvocationServices = null)
- : base(innerClient)
- {
- _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance;
- _activitySource = innerClient.GetService();
- _functionInvocationServices = functionInvocationServices;
- }
-
- ///
- /// Gets or sets the for the current function invocation.
- ///
- ///
- /// This value flows across async calls.
- ///
- public static AutoFunctionInvocationContext? CurrentContext
- {
- get => _currentContext.Value;
- protected set => _currentContext.Value = value;
- }
-
- ///
- /// Gets or sets a value indicating whether detailed exception information should be included
- /// in the chat history when calling the underlying .
- ///
- ///
- /// if the full exception message is added to the chat history
- /// when calling the underlying .
- /// if a generic error message is included in the chat history.
- /// The default value is .
- ///
- ///
- ///
- /// Setting the value to prevents the underlying language model from disclosing
- /// raw exception details to the end user, since it doesn't receive that information. Even in this
- /// case, the raw object is available to application code by inspecting
- /// the property.
- ///
- ///
- /// Setting the value to can help the underlying bypass problems on
- /// its own, for example by retrying the function call with different arguments. However, it might
- /// result in disclosing the raw exception information to external users, which can be a security
- /// concern depending on the application scenario.
- ///
- ///
- /// Changing the value of this property while the client is in use might result in inconsistencies
- /// whether detailed errors are provided during an in-flight request.
- ///
- ///
- public bool IncludeDetailedErrors { get; set; }
-
- ///
- /// Gets or sets a value indicating whether to allow concurrent invocation of functions.
- ///
- ///
- /// if multiple function calls can execute in parallel.
- /// if function calls are processed serially.
- /// The default value is .
- ///
- ///
- /// An individual response from the inner client might contain multiple function call requests.
- /// By default, such function calls are processed serially. Set to
- /// to enable concurrent invocation such that multiple function calls can execute in parallel.
- ///
- public bool AllowConcurrentInvocation { get; set; }
-
- ///
- /// Gets or sets the maximum number of iterations per request.
- ///
- ///
- /// The maximum number of iterations per request.
- /// The default value is 10.
- ///
- ///
- ///
- /// Each request to this might end up making
- /// multiple requests to the inner client. Each time the inner client responds with
- /// a function call request, this client might perform that invocation and send the results
- /// back to the inner client in a new request. This property limits the number of times
- /// such a roundtrip is performed. The value must be at least one, as it includes the initial request.
- ///
- ///
- /// Changing the value of this property while the client is in use might result in inconsistencies
- /// as to how many iterations are allowed for an in-flight request.
- ///
- ///
- public int MaximumIterationsPerRequest
- {
- get => _maximumIterationsPerRequest;
- set
- {
- if (value < 1)
- {
- throw new ArgumentOutOfRangeException(nameof(value));
- }
-
- _maximumIterationsPerRequest = value;
- }
- }
-
- ///
- /// Gets or sets the maximum number of consecutive iterations that are allowed to fail with an error.
- ///
- ///
- /// The maximum number of consecutive iterations that are allowed to fail with an error.
- /// The default value is 3.
- ///
- ///
- ///
- /// When function invocations fail with an exception, the
- /// continues to make requests to the inner client, optionally supplying exception information (as
- /// controlled by ). This allows the to
- /// recover from errors by trying other function parameters that may succeed.
- ///
- ///
- /// However, in case function invocations continue to produce exceptions, this property can be used to
- /// limit the number of consecutive failing attempts. When the limit is reached, the exception will be
- /// rethrown to the caller.
- ///
- ///
- /// If the value is set to zero, all function calling exceptions immediately terminate the function
- /// invocation loop and the exception will be rethrown to the caller.
- ///
- ///
- /// Changing the value of this property while the client is in use might result in inconsistencies
- /// as to how many iterations are allowed for an in-flight request.
- ///
- ///
- public int MaximumConsecutiveErrorsPerRequest
- {
- get => _maximumConsecutiveErrorsPerRequest;
- set
- {
- if (value < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(value), "Argument less than minimum value 0");
- }
- _maximumConsecutiveErrorsPerRequest = value;
- }
- }
-
- ///
- public override async Task GetResponseAsync(
- IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
- {
- Verify.NotNull(messages);
-
- // A single request into this GetResponseAsync may result in multiple requests to the inner client.
- // Create an activity to group them together for better observability.
- using Activity? activity = _activitySource?.StartActivity(nameof(KernelFunctionInvokingChatClient));
-
- // Copy the original messages in order to avoid enumerating the original messages multiple times.
- // The IEnumerable can represent an arbitrary amount of work.
- List originalMessages = [.. messages];
- messages = originalMessages;
-
- List? augmentedHistory = null; // the actual history of messages sent on turns other than the first
- ChatResponse? response = null; // the response from the inner client, which is possibly modified and then eventually returned
- List? responseMessages = null; // tracked list of messages, across multiple turns, to be used for the final response
- UsageDetails? totalUsage = null; // tracked usage across all turns, to be used for the final response
- List? functionCallContents = null; // function call contents that need responding to in the current turn
- bool lastIterationHadThreadId = false; // whether the last iteration's response had a ChatThreadId set
- int consecutiveErrorCount = 0;
-
- for (int iteration = 0; ; iteration++)
- {
- functionCallContents?.Clear();
-
- // Make the call to the inner client.
- response = await base.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
- if (response is null)
- {
- throw new InvalidOperationException($"The inner {nameof(IChatClient)} returned a null {nameof(ChatResponse)}.");
- }
-
- // Any function call work to do? If yes, ensure we're tracking that work in functionCallContents.
- bool requiresFunctionInvocation =
- options?.Tools is { Count: > 0 } &&
- iteration < MaximumIterationsPerRequest &&
- CopyFunctionCalls(response.Messages, ref functionCallContents);
-
- // In a common case where we make a request and there's no function calling work required,
- // fast path out by just returning the original response.
- if (iteration == 0 && !requiresFunctionInvocation)
- {
- return response;
- }
-
- // Track aggregate details from the response, including all the response messages and usage details.
- (responseMessages ??= []).AddRange(response.Messages);
- if (response.Usage is not null)
- {
- if (totalUsage is not null)
- {
- totalUsage.Add(response.Usage);
- }
- else
- {
- totalUsage = response.Usage;
- }
- }
-
- // If there are no tools to call, or for any other reason we should stop, we're done.
- // Break out of the loop and allow the handling at the end to configure the response
- // with aggregated data from previous requests.
- if (!requiresFunctionInvocation)
- {
- break;
- }
-
- // Prepare the history for the next iteration.
- FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadThreadId);
-
- // Prepare the options for the next auto function invocation iteration.
- UpdateOptionsForAutoFunctionInvocation(ref options!, response.Messages.Last().ToChatMessageContent(), isStreaming: false);
-
- // Add the responses from the function calls into the augmented history and also into the tracked
- // list of response messages.
- var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options!, functionCallContents!, iteration, consecutiveErrorCount, isStreaming: false, cancellationToken).ConfigureAwait(false);
- responseMessages.AddRange(modeAndMessages.MessagesAdded);
- consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount;
-
- if (modeAndMessages.ShouldTerminate)
- {
- break;
- }
-
- // Clear the auto function invocation options.
- ClearOptionsForAutoFunctionInvocation(ref options);
-
- UpdateOptionsForNextIteration(ref options!, response.ChatThreadId);
- }
-
- Debug.Assert(responseMessages is not null, "Expected to only be here if we have response messages.");
- response.Messages = responseMessages!;
- response.Usage = totalUsage;
-
- return response;
- }
-
- ///
- public override async IAsyncEnumerable GetStreamingResponseAsync(
- IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
- {
- Verify.NotNull(messages);
-
- // A single request into this GetStreamingResponseAsync may result in multiple requests to the inner client.
- // Create an activity to group them together for better observability.
- using Activity? activity = _activitySource?.StartActivity(nameof(FunctionInvokingChatClient));
-
- // Copy the original messages in order to avoid enumerating the original messages multiple times.
- // The IEnumerable can represent an arbitrary amount of work.
- List originalMessages = [.. messages];
- messages = originalMessages;
-
- List? augmentedHistory = null; // the actual history of messages sent on turns other than the first
- List? functionCallContents = null; // function call contents that need responding to in the current turn
- List? responseMessages = null; // tracked list of messages, across multiple turns, to be used in fallback cases to reconstitute history
- bool lastIterationHadThreadId = false; // whether the last iteration's response had a ChatThreadId set
- List updates = []; // updates from the current response
- int consecutiveErrorCount = 0;
-
- for (int iteration = 0; ; iteration++)
- {
- updates.Clear();
- functionCallContents?.Clear();
-
- await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false))
- {
- if (update is null)
- {
- throw new InvalidOperationException($"The inner {nameof(IChatClient)} streamed a null {nameof(ChatResponseUpdate)}.");
- }
-
- updates.Add(update);
-
- _ = CopyFunctionCalls(update.Contents, ref functionCallContents);
-
- yield return update;
- Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
- }
-
- // If there are no tools to call, or for any other reason we should stop, return the response.
- if (functionCallContents is not { Count: > 0 } ||
- options?.Tools is not { Count: > 0 } ||
- iteration >= _maximumIterationsPerRequest)
- {
- break;
- }
-
- // Reconstitute a response from the response updates.
- var response = updates.ToChatResponse();
- (responseMessages ??= []).AddRange(response.Messages);
-
- // Prepare the history for the next iteration.
- FixupHistories(originalMessages, ref messages, ref augmentedHistory, response, responseMessages, ref lastIterationHadThreadId);
-
- // Prepare the options for the next auto function invocation iteration.
- UpdateOptionsForAutoFunctionInvocation(ref options, response.Messages.Last().ToChatMessageContent(), isStreaming: true);
-
- // Process all the functions, adding their results into the history.
- var modeAndMessages = await ProcessFunctionCallsAsync(augmentedHistory, options, functionCallContents, iteration, consecutiveErrorCount, isStreaming: true, cancellationToken).ConfigureAwait(false);
- responseMessages.AddRange(modeAndMessages.MessagesAdded);
- consecutiveErrorCount = modeAndMessages.NewConsecutiveErrorCount;
-
- // Clear the auto function invocation options.
- ClearOptionsForAutoFunctionInvocation(ref options);
-
- // This is a synthetic ID since we're generating the tool messages instead of getting them from
- // the underlying provider. When emitting the streamed chunks, it's perfectly valid for us to
- // use the same message ID for all of them within a given iteration, as this is a single logical
- // message with multiple content items. We could also use different message IDs per tool content,
- // but there's no benefit to doing so.
- string toolResponseId = Guid.NewGuid().ToString("N");
-
- // Stream any generated function results. This mirrors what's done for GetResponseAsync, where the returned messages
- // include all activity, including generated function results.
- foreach (var message in modeAndMessages.MessagesAdded)
- {
- var toolResultUpdate = new ChatResponseUpdate
- {
- AdditionalProperties = message.AdditionalProperties,
- AuthorName = message.AuthorName,
- ChatThreadId = response.ChatThreadId,
- CreatedAt = DateTimeOffset.UtcNow,
- Contents = message.Contents,
- RawRepresentation = message.RawRepresentation,
- ResponseId = toolResponseId,
- MessageId = toolResponseId, // See above for why this can be the same as ResponseId
- Role = message.Role,
- };
-
- yield return toolResultUpdate;
- Activity.Current = activity; // workaround for https://github.com/dotnet/runtime/issues/47802
- }
-
- if (modeAndMessages.ShouldTerminate)
- {
- yield break;
- }
-
- UpdateOptionsForNextIteration(ref options, response.ChatThreadId);
- }
- }
-
- /// Prepares the various chat message lists after a response from the inner client and before invoking functions.
- /// The original messages provided by the caller.
- /// The messages reference passed to the inner client.
- /// The augmented history containing all the messages to be sent.
- /// The most recent response being handled.
- /// A list of all response messages received up until this point.
- /// Whether the previous iteration's response had a thread id.
- private static void FixupHistories(
- IEnumerable originalMessages,
- ref IEnumerable messages,
- [NotNull] ref List? augmentedHistory,
- ChatResponse response,
- List allTurnsResponseMessages,
- ref bool lastIterationHadThreadId)
- {
- // We're now going to need to augment the history with function result contents.
- // That means we need a separate list to store the augmented history.
- if (response.ChatThreadId is not null)
- {
- // The response indicates the inner client is tracking the history, so we don't want to send
- // anything we've already sent or received.
- if (augmentedHistory is not null)
- {
- augmentedHistory.Clear();
- }
- else
- {
- augmentedHistory = [];
- }
-
- lastIterationHadThreadId = true;
- }
- else if (lastIterationHadThreadId)
- {
- // In the very rare case where the inner client returned a response with a thread ID but then
- // returned a subsequent response without one, we want to reconstitute the full history. To do that,
- // we can populate the history with the original chat messages and then all the response
- // messages up until this point, which includes the most recent ones.
- augmentedHistory ??= [];
- augmentedHistory.Clear();
- augmentedHistory.AddRange(originalMessages);
- augmentedHistory.AddRange(allTurnsResponseMessages);
-
- lastIterationHadThreadId = false;
- }
- else
- {
- // If augmentedHistory is already non-null, then we've already populated it with everything up
- // until this point (except for the most recent response). If it's null, we need to seed it with
- // the chat history provided by the caller.
- augmentedHistory ??= originalMessages.ToList();
-
- // Now add the most recent response messages.
- augmentedHistory.AddMessages(response);
-
- lastIterationHadThreadId = false;
- }
-
- // Use the augmented history as the new set of messages to send.
- messages = augmentedHistory;
- }
-
- /// Copies any from to .
- private static bool CopyFunctionCalls(
- IList messages, [NotNullWhen(true)] ref List? functionCalls)
- {
- bool any = false;
- int count = messages.Count;
- for (int i = 0; i < count; i++)
- {
- any |= CopyFunctionCalls(messages[i].Contents, ref functionCalls);
- }
-
- return any;
- }
-
- /// Copies any from to .
- private static bool CopyFunctionCalls(
- IList content, [NotNullWhen(true)] ref List? functionCalls)
- {
- bool any = false;
- int count = content.Count;
- for (int i = 0; i < count; i++)
- {
- if (content[i] is FunctionCallContent functionCall)
- {
- (functionCalls ??= []).Add(functionCall);
- any = true;
- }
- }
-
- return any;
- }
-
- private static void UpdateOptionsForAutoFunctionInvocation(ref ChatOptions options, ChatMessageContent content, bool isStreaming)
- {
- if (options.AdditionalProperties?.ContainsKey(ChatOptionsExtensions.IsStreamingKey) ?? false)
- {
- throw new KernelException($"The reserved key name '{ChatOptionsExtensions.IsStreamingKey}' is already specified in the options. Avoid using this key name.");
- }
-
- if (options.AdditionalProperties?.ContainsKey(ChatOptionsExtensions.ChatMessageContentKey) ?? false)
- {
- throw new KernelException($"The reserved key name '{ChatOptionsExtensions.ChatMessageContentKey}' is already specified in the options. Avoid using this key name.");
- }
-
- options.AdditionalProperties ??= [];
-
- options.AdditionalProperties[ChatOptionsExtensions.IsStreamingKey] = isStreaming;
- options.AdditionalProperties[ChatOptionsExtensions.ChatMessageContentKey] = content;
- }
-
- private static void ClearOptionsForAutoFunctionInvocation(ref ChatOptions options)
- {
- if (options.AdditionalProperties?.ContainsKey(ChatOptionsExtensions.IsStreamingKey) ?? false)
- {
- options.AdditionalProperties.Remove(ChatOptionsExtensions.IsStreamingKey);
- }
-
- if (options.AdditionalProperties?.ContainsKey(ChatOptionsExtensions.ChatMessageContentKey) ?? false)
- {
- options.AdditionalProperties.Remove(ChatOptionsExtensions.ChatMessageContentKey);
- }
- }
-
- private static void UpdateOptionsForNextIteration(ref ChatOptions options, string? chatThreadId)
- {
- if (options.ToolMode is RequiredChatToolMode)
- {
- // We have to reset the tool mode to be non-required after the first iteration,
- // as otherwise we'll be in an infinite loop.
- options = options.Clone();
- options.ToolMode = null;
- options.ChatThreadId = chatThreadId;
- }
- else if (options.ChatThreadId != chatThreadId)
- {
- // As with the other modes, ensure we've propagated the chat thread ID to the options.
- // We only need to clone the options if we're actually mutating it.
- options = options.Clone();
- options.ChatThreadId = chatThreadId;
- }
- }
-
- ///
- /// Processes the function calls in the list.
- ///
- /// The current chat contents, inclusive of the function call contents being processed.
- /// The options used for the response being processed.
- /// The function call contents representing the functions to be invoked.
- /// The iteration number of how many roundtrips have been made to the inner client.
- /// The number of consecutive iterations, prior to this one, that were recorded as having function invocation errors.
- /// Whether the function calls are being processed in a streaming context.
- /// The to monitor for cancellation requests.
- /// A value indicating how the caller should proceed.
- private async Task<(bool ShouldTerminate, int NewConsecutiveErrorCount, IList MessagesAdded)> ProcessFunctionCallsAsync(
- List messages, ChatOptions options, List functionCallContents,
- int iteration, int consecutiveErrorCount, bool isStreaming, CancellationToken cancellationToken)
- {
- // We must add a response for every tool call, regardless of whether we successfully executed it or not.
- // If we successfully execute it, we'll add the result. If we don't, we'll add an error.
-
- Debug.Assert(functionCallContents.Count > 0, "Expected at least one function call.");
- var shouldTerminate = false;
-
- var captureCurrentIterationExceptions = consecutiveErrorCount < _maximumConsecutiveErrorsPerRequest;
-
- // Process all functions. If there's more than one and concurrent invocation is enabled, do so in parallel.
- if (functionCallContents.Count == 1)
- {
- FunctionInvocationResult result = await ProcessFunctionCallAsync(
- messages, options, functionCallContents, iteration, 0, captureCurrentIterationExceptions, isStreaming, cancellationToken).ConfigureAwait(false);
-
- IList added = CreateResponseMessages([result]);
- ThrowIfNoFunctionResultsAdded(added);
- UpdateConsecutiveErrorCountOrThrow(added, ref consecutiveErrorCount);
-
- messages.AddRange(added);
- return (result.ShouldTerminate, consecutiveErrorCount, added);
- }
- else
- {
- List results = [];
-
- var terminationRequested = false;
- if (AllowConcurrentInvocation)
- {
- // Rather than awaiting each function before invoking the next, invoke all of them
- // and then await all of them. We avoid forcibly introducing parallelism via Task.Run,
- // but if a function invocation completes asynchronously, its processing can overlap
- // with the processing of other the other invocation invocations.
- results.AddRange(await Task.WhenAll(
- from i in Enumerable.Range(0, functionCallContents.Count)
- select ProcessFunctionCallAsync(
- messages, options, functionCallContents,
- iteration, i, captureExceptions: true, isStreaming, cancellationToken)).ConfigureAwait(false));
-
- terminationRequested = results.Any(r => r.ShouldTerminate);
- }
- else
- {
- // Invoke each function serially.
- for (int i = 0; i < functionCallContents.Count; i++)
- {
- var result = await ProcessFunctionCallAsync(
- messages, options, functionCallContents,
- iteration, i, captureCurrentIterationExceptions, isStreaming, cancellationToken).ConfigureAwait(false);
-
- results.Add(result);
-
- if (result.ShouldTerminate)
- {
- shouldTerminate = true;
- terminationRequested = true;
- break;
- }
- }
- }
-
- IList added = CreateResponseMessages(results);
- ThrowIfNoFunctionResultsAdded(added);
- UpdateConsecutiveErrorCountOrThrow(added, ref consecutiveErrorCount);
-
- messages.AddRange(added);
-
- if (!terminationRequested)
- {
- // If any function requested termination, we'll terminate.
- shouldTerminate = false;
- foreach (FunctionInvocationResult fir in results)
- {
- shouldTerminate = shouldTerminate || fir.ShouldTerminate;
- }
- }
-
- return (shouldTerminate, consecutiveErrorCount, added);
- }
- }
-
- private void UpdateConsecutiveErrorCountOrThrow(IList added, ref int consecutiveErrorCount)
- {
- var allExceptions = added.SelectMany(m => m.Contents.OfType())
- .Select(frc => frc.Exception!)
- .Where(e => e is not null);
-
-#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection
- if (allExceptions.Any())
- {
- consecutiveErrorCount++;
- if (consecutiveErrorCount > _maximumConsecutiveErrorsPerRequest)
- {
- var allExceptionsArray = allExceptions.ToArray();
- if (allExceptionsArray.Length == 1)
- {
- ExceptionDispatchInfo.Capture(allExceptionsArray[0]).Throw();
- }
- else
- {
- throw new AggregateException(allExceptionsArray);
- }
- }
- }
- else
- {
- consecutiveErrorCount = 0;
- }
-#pragma warning restore CA1851 // Possible multiple enumerations of 'IEnumerable' collection
- }
-
- ///
- /// Throws an exception if doesn't create any messages.
- ///
- private void ThrowIfNoFunctionResultsAdded(IList? messages)
- {
- if (messages is null || messages.Count == 0)
- {
- throw new InvalidOperationException($"{this.GetType().Name}.{nameof(this.CreateResponseMessages)} returned null or an empty collection of messages.");
- }
- }
-
- /// Processes the function call described in [ ].
- /// The current chat contents, inclusive of the function call contents being processed.
- /// The options used for the response being processed.
- /// The function call contents representing all the functions being invoked.
- /// The iteration number of how many roundtrips have been made to the inner client.
- /// The 0-based index of the function being called out of .
- /// If true, handles function-invocation exceptions by returning a value with . Otherwise, rethrows.
- /// Whether the function calls are being processed in a streaming context.
- /// The to monitor for cancellation requests.
- /// A value indicating how the caller should proceed.
- private async Task ProcessFunctionCallAsync(
- List messages, ChatOptions options, List callContents,
- int iteration, int functionCallIndex, bool captureExceptions, bool isStreaming, CancellationToken cancellationToken)
- {
- var callContent = callContents[functionCallIndex];
-
- // Look up the AIFunction for the function call. If the requested function isn't available, send back an error.
- AIFunction? function = options.Tools!.OfType().FirstOrDefault(t => t.Name == callContent.Name);
- if (function is null)
- {
- return new(shouldTerminate: false, FunctionInvokingChatClient.FunctionInvocationStatus.NotFound, callContent, result: null, exception: null);
- }
-
- if (callContent.Arguments is not null)
- {
- callContent.Arguments = new KernelArguments(callContent.Arguments);
- }
-
- var context = new AutoFunctionInvocationContext(new()
- {
- Function = function,
- Arguments = new(callContent.Arguments) { Services = _functionInvocationServices },
-
- Messages = messages,
- Options = options,
-
- CallContent = callContent,
- Iteration = iteration,
- FunctionCallIndex = functionCallIndex,
- FunctionCount = callContents.Count,
- })
- { IsStreaming = isStreaming };
-
- object? result;
- try
- {
- result = await InvokeFunctionAsync(context, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception e) when (!cancellationToken.IsCancellationRequested)
- {
- if (!captureExceptions)
- {
- throw;
- }
-
- return new(
- shouldTerminate: false,
- FunctionInvokingChatClient.FunctionInvocationStatus.Exception,
- callContent,
- result: null,
- exception: e);
- }
-
- return new(
- shouldTerminate: context.Terminate,
- FunctionInvokingChatClient.FunctionInvocationStatus.RanToCompletion,
- callContent,
- result,
- exception: null);
- }
-
- /// Creates one or more response messages for function invocation results.
- /// Information about the function call invocations and results.
- /// A list of all chat messages created from .
- private IList CreateResponseMessages(List results)
- {
- var contents = new List(results.Count);
- for (int i = 0; i < results.Count; i++)
- {
- contents.Add(CreateFunctionResultContent(results[i]));
- }
-
- return [new(ChatRole.Tool, contents)];
-
- FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult result)
- {
- Verify.NotNull(result);
-
- object? functionResult;
- if (result.Status == FunctionInvokingChatClient.FunctionInvocationStatus.RanToCompletion)
- {
- functionResult = result.Result ?? "Success: Function completed.";
- }
- else
- {
- string message = result.Status switch
- {
- FunctionInvokingChatClient.FunctionInvocationStatus.NotFound => $"Error: Requested function \"{result.CallContent.Name}\" not found.",
- FunctionInvokingChatClient.FunctionInvocationStatus.Exception => "Error: Function failed.",
- _ => "Error: Unknown error.",
- };
-
- if (IncludeDetailedErrors && result.Exception is not null)
- {
- message = $"{message} Exception: {result.Exception.Message}";
- }
-
- functionResult = message;
- }
-
- return new FunctionResultContent(result.CallContent.CallId, functionResult) { Exception = result.Exception };
- }
- }
-
- ///
- /// Invokes the auto function invocation filters.
- ///
- /// The auto function invocation context.
- /// The function to call after the filters.
- /// The auto function invocation context.
- private async Task OnAutoFunctionInvocationAsync(
- AutoFunctionInvocationContext context,
- Func functionCallCallback)
- {
- await this.InvokeFilterOrFunctionAsync(functionCallCallback, context).ConfigureAwait(false);
-
- return context;
- }
-
- ///
- /// This method will execute auto function invocation filters and function recursively.
- /// If there are no registered filters, just function will be executed.
- /// If there are registered filters, filter on position will be executed.
- /// Second parameter of filter is callback. It can be either filter on + 1 position or function if there are no remaining filters to execute.
- /// Function will always be executed as last step after all filters.
- ///
- private async Task InvokeFilterOrFunctionAsync(
- Func functionCallCallback,
- AutoFunctionInvocationContext context,
- int index = 0)
- {
- IList autoFunctionInvocationFilters = context.Kernel.AutoFunctionInvocationFilters;
-
- if (autoFunctionInvocationFilters is { Count: > 0 } && index < autoFunctionInvocationFilters.Count)
- {
- await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(
- context,
- (ctx) => this.InvokeFilterOrFunctionAsync(functionCallCallback, ctx, index + 1)
- ).ConfigureAwait(false);
- }
- else
- {
- await functionCallCallback(context).ConfigureAwait(false);
- }
- }
-
- /// Invokes the function asynchronously.
- ///
- /// The function invocation context detailing the function to be invoked and its arguments along with additional request information.
- ///
- /// The to monitor for cancellation requests. The default is .
- /// The result of the function invocation, or if the function invocation returned .
- /// is .
- private async Task InvokeFunctionAsync(AutoFunctionInvocationContext context, CancellationToken cancellationToken)
- {
- Verify.NotNull(context);
-
- using Activity? activity = _activitySource?.StartActivity(context.Function.Name);
-
- long startingTimestamp = 0;
- if (_logger.IsEnabled(LogLevel.Debug))
- {
- startingTimestamp = Stopwatch.GetTimestamp();
- if (_logger.IsEnabled(LogLevel.Trace))
- {
- LogInvokingSensitive(context.Function.Name, LoggingAsJson(context.CallContent.Arguments, context.AIFunction.JsonSerializerOptions));
- }
- else
- {
- LogInvoking(context.Function.Name);
- }
- }
-
- object? result = null;
- try
- {
- CurrentContext = context; // doesn't need to be explicitly reset after, as that's handled automatically at async method exit
- context = await this.OnAutoFunctionInvocationAsync(
- context,
- async (ctx) =>
- {
- // Check if filter requested termination
- if (ctx.Terminate)
- {
- return;
- }
-
- // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any
- // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here,
- // as the called function could in turn telling the model about itself as a possible candidate for invocation.
- result = await context.AIFunction.InvokeAsync(new(context.Arguments), cancellationToken).ConfigureAwait(false);
- ctx.Result = new FunctionResult(ctx.Function, result);
- }).ConfigureAwait(false);
- result = context.Result.GetValue();
- }
- catch (Exception e)
- {
- if (activity is not null)
- {
- _ = activity.SetTag("error.type", e.GetType().FullName)
- .SetStatus(ActivityStatusCode.Error, e.Message);
- }
-
- if (e is OperationCanceledException)
- {
- LogInvocationCanceled(context.Function.Name);
- }
- else
- {
- LogInvocationFailed(context.Function.Name, e);
- }
-
- throw;
- }
- finally
- {
- if (_logger.IsEnabled(LogLevel.Debug))
- {
- TimeSpan elapsed = GetElapsedTime(startingTimestamp);
-
- if (result is not null && _logger.IsEnabled(LogLevel.Trace))
- {
- LogInvocationCompletedSensitive(context.Function.Name, elapsed, LoggingAsJson(result, context.AIFunction.JsonSerializerOptions));
- }
- else
- {
- LogInvocationCompleted(context.Function.Name, elapsed);
- }
- }
- }
-
- return result;
- }
-
- /// Serializes as JSON for logging purposes.
- private static string LoggingAsJson(T value, JsonSerializerOptions? options)
- {
- if (options?.TryGetTypeInfo(typeof(T), out var typeInfo) is true ||
- AIJsonUtilities.DefaultOptions.TryGetTypeInfo(typeof(T), out typeInfo))
- {
-#pragma warning disable CA1031 // Do not catch general exception types
- try
- {
- return JsonSerializer.Serialize(value, typeInfo);
- }
- catch
- {
- }
-#pragma warning restore CA1031 // Do not catch general exception types
- }
-
- // If we're unable to get a type info for the value, or if we fail to serialize,
- // return an empty JSON object. We do not want lack of type info to disrupt application behavior with exceptions.
- return "{}";
- }
-
- private static TimeSpan GetElapsedTime(long startingTimestamp) =>
-#if NET
- Stopwatch.GetElapsedTime(startingTimestamp);
-#else
- new((long)((Stopwatch.GetTimestamp() - startingTimestamp) * ((double)TimeSpan.TicksPerSecond / Stopwatch.Frequency)));
-#endif
-
- [LoggerMessage(LogLevel.Debug, "Invoking {MethodName}.", SkipEnabledCheck = true)]
- private partial void LogInvoking(string methodName);
-
- [LoggerMessage(LogLevel.Trace, "Invoking {MethodName}({Arguments}).", SkipEnabledCheck = true)]
- private partial void LogInvokingSensitive(string methodName, string arguments);
-
- [LoggerMessage(LogLevel.Debug, "{MethodName} invocation completed. Duration: {Duration}", SkipEnabledCheck = true)]
- private partial void LogInvocationCompleted(string methodName, TimeSpan duration);
-
- [LoggerMessage(LogLevel.Trace, "{MethodName} invocation completed. Duration: {Duration}. Result: {Result}", SkipEnabledCheck = true)]
- private partial void LogInvocationCompletedSensitive(string methodName, TimeSpan duration, string result);
-
- [LoggerMessage(LogLevel.Debug, "{MethodName} invocation canceled.")]
- private partial void LogInvocationCanceled(string methodName);
-
- [LoggerMessage(LogLevel.Error, "{MethodName} invocation failed.")]
- private partial void LogInvocationFailed(string methodName, Exception error);
-
- /// Provides information about the invocation of a function call.
- public sealed class FunctionInvocationResult
- {
- internal FunctionInvocationResult(bool shouldTerminate, FunctionInvokingChatClient.FunctionInvocationStatus status, FunctionCallContent callContent, object? result, Exception? exception)
- {
- ShouldTerminate = shouldTerminate;
- Status = status;
- CallContent = callContent;
- Result = result;
- Exception = exception;
- }
-
- /// Gets status about how the function invocation completed.
- public FunctionInvokingChatClient.FunctionInvocationStatus Status { get; }
-
- /// Gets the function call content information associated with this invocation.
- public FunctionCallContent CallContent { get; }
-
- /// Gets the result of the function call.
- public object? Result { get; }
-
- /// Gets any exception the function call threw.
- public Exception? Exception { get; }
-
- /// Gets a value indicating whether the caller should terminate the processing loop.
- internal bool ShouldTerminate { get; }
- }
-}
diff --git a/dotnet/src/SemanticKernel.Abstractions/Filters/AutoFunctionInvocation/AutoFunctionInvocationContext.cs b/dotnet/src/SemanticKernel.Abstractions/Filters/AutoFunctionInvocation/AutoFunctionInvocationContext.cs
index d4e66f48cfc3..1a31144e26bb 100644
--- a/dotnet/src/SemanticKernel.Abstractions/Filters/AutoFunctionInvocation/AutoFunctionInvocationContext.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/Filters/AutoFunctionInvocation/AutoFunctionInvocationContext.cs
@@ -21,23 +21,26 @@ public class AutoFunctionInvocationContext : FunctionInvocationContextV2
///
/// Initializes a new instance of the class from an existing .
///
- internal AutoFunctionInvocationContext(Microsoft.Extensions.AI.FunctionInvocationContextV2 invocationContext)
+ internal AutoFunctionInvocationContext(ChatOptions options)
{
- Verify.NotNull(invocationContext);
- Verify.NotNull(invocationContext.Options);
+ Verify.NotNull(options);
// the ChatOptions must be provided with AdditionalProperties.
- Verify.NotNull(invocationContext.Options.AdditionalProperties);
+ Verify.NotNull(options.AdditionalProperties);
- invocationContext.Options.AdditionalProperties.TryGetValue(ChatOptionsExtensions.KernelKey, out var kernel);
+ // The ChatOptions must be provided with the kernel.
+ options.AdditionalProperties.TryGetValue(ChatOptionsExtensions.KernelKey, out var kernel);
Verify.NotNull(kernel);
- invocationContext.Options.AdditionalProperties.TryGetValue(ChatOptionsExtensions.ChatMessageContentKey, out var chatMessageContent);
+ // The ChatOptions must be provided with the chat message content.
+ options.AdditionalProperties.TryGetValue(ChatOptionsExtensions.ChatMessageContentKey, out var chatMessageContent);
Verify.NotNull(chatMessageContent);
- invocationContext.Options.AdditionalProperties.TryGetValue(ChatOptionsExtensions.PromptExecutionSettingsKey, out var executionSettings);
- this.ExecutionSettings = executionSettings;
+ // The ChatOptions can be provided with the execution settings.
+ options.AdditionalProperties.TryGetValue(ChatOptionsExtensions.PromptExecutionSettingsKey, out var executionSettings);
+ this.ExecutionSettings = executionSettings;
+ this.Options = options;
this.Result = new FunctionResult(this.Function) { Culture = kernel.Culture };
}
@@ -90,8 +93,8 @@ public AutoFunctionInvocationContext(
///
public new KernelArguments? Arguments
{
- get => this.CallContent.Arguments is KernelArguments kernelArguments ? kernelArguments : null;
- init => this.CallContent.Arguments = value;
+ get => new(base.Arguments);
+ init => base.Arguments = new(value);
}
///
@@ -191,9 +194,22 @@ public Kernel Kernel
///
public FunctionResult Result { get; set; }
+ ///
+ /// Gets or sets the with which this filter is associated.
+ ///
internal AIFunction AIFunction
{
get => base.Function;
+ set => base.Function = value;
+ }
+
+ ///
+ /// Gets or sets the arguments associated with this invocation context.
+ ///
+ internal AIFunctionArguments AIArguments
+ {
+ get => base.Arguments;
+ set => base.Arguments = value;
}
private static bool IsSameSchema(KernelFunction kernelFunction, AIFunction aiFunction)
From e805c931893ceccc2047f308bd0c5acef2ec1478 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 22 Apr 2025 17:22:35 +0100
Subject: [PATCH 04/24] Using KernelArguments as Specialization of
AIFunctionArguments and related update changes needed
---
.../RestApiOperationRunner.cs | 2 +
.../AI/ChatClient/AIFunctionArgumentsV2.cs | 136 ++++++++++++++++
.../ChatClient/FunctionInvocationContextV2.cs | 4 +-
.../FunctionInvokingChatClientV2.cs | 2 +-
.../KernelFunctionInvokingChatClient.cs | 9 +-
.../AutoFunctionInvocationContext.cs | 30 ++--
.../Functions/KernelArguments.cs | 147 ++----------------
7 files changed, 172 insertions(+), 158 deletions(-)
create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/AIFunctionArgumentsV2.cs
diff --git a/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs b/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs
index 28305a5ec8a4..068f4713118a 100644
--- a/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs
+++ b/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs
@@ -13,6 +13,8 @@
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Http;
+#pragma warning disable CA1859
+
namespace Microsoft.SemanticKernel.Plugins.OpenApi;
///
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/AIFunctionArgumentsV2.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/AIFunctionArgumentsV2.cs
new file mode 100644
index 000000000000..b28bbfdec420
--- /dev/null
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/AIFunctionArgumentsV2.cs
@@ -0,0 +1,136 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+#pragma warning disable SA1111 // Closing parenthesis should be on line of last parameter
+#pragma warning disable SA1112 // Closing parenthesis should be on line of opening parenthesis
+#pragma warning disable SA1114 // Parameter list should follow declaration
+#pragma warning disable CA1710 // Identifiers should have correct suffix
+
+namespace Microsoft.Extensions.AI;
+
+/// Represents arguments to be used with .
+///
+/// is a dictionary of name/value pairs that are used
+/// as inputs to an . However, an instance carries additional non-nominal
+/// information, such as an optional that can be used by
+/// an if it needs to resolve any services from a dependency injection
+/// container.
+///
+public class AIFunctionArgumentsV2 : IDictionary, IReadOnlyDictionary
+{
+ /// The nominal arguments.
+ private readonly Dictionary _arguments;
+
+ /// Initializes a new instance of the class.
+ /// The optional to use for key comparisons.
+ public AIFunctionArgumentsV2(IEqualityComparer? comparer = null)
+ {
+ _arguments = new Dictionary(comparer);
+ }
+
+ ///
+ /// Initializes a new instance of the class containing
+ /// the specified .
+ ///
+ /// The arguments represented by this instance.
+ /// The to be used.
+ ///
+ /// The reference will be stored if the instance is
+ /// already a and no is specified,
+ /// in which case all dictionary operations on this instance will be routed directly to that instance.
+ /// If is not a dictionary or a is specified,
+ /// a shallow clone of its data will be used to populate this instance.
+ /// A is treated as an empty dictionary.
+ ///
+ public AIFunctionArgumentsV2(IDictionary? arguments, IEqualityComparer? comparer = null)
+ {
+ this._arguments = comparer is null
+ ? arguments is null
+ ? []
+ : arguments as Dictionary ??
+ new Dictionary(arguments)
+ : arguments is null
+ ? new Dictionary(comparer)
+ : new Dictionary(arguments, comparer);
+ }
+
+ /// Gets or sets services optionally associated with these arguments.
+ public IServiceProvider? Services { get; set; }
+
+ /// Gets or sets additional context associated with these arguments.
+ ///
+ /// The context is a dictionary of name/value pairs that can be used to store arbitrary
+ /// information for use by an implementation. The meaning of this
+ /// data is left up to the implementer of the .
+ ///
+ public IDictionary? Context { get; set; }
+
+ ///
+ public object? this[string key]
+ {
+ get => _arguments[key];
+ set => _arguments[key] = value;
+ }
+
+ ///
+ public ICollection Keys => _arguments.Keys;
+
+ ///
+ public ICollection Values => _arguments.Values;
+
+ ///
+ public int Count => _arguments.Count;
+
+ ///
+ public bool IsReadOnly => false;
+
+ ///
+ IEnumerable IReadOnlyDictionary.Keys => Keys;
+
+ ///
+ IEnumerable IReadOnlyDictionary.Values => Values;
+
+ ///
+ public void Add(string key, object? value) => _arguments.Add(key, value);
+
+ ///
+ void ICollection>.Add(KeyValuePair item) =>
+ ((ICollection>)_arguments).Add(item);
+
+ ///
+ public void Clear() => _arguments.Clear();
+
+ ///
+ public bool Contains(KeyValuePair item) =>
+ ((ICollection>)_arguments).Contains(item);
+
+ ///
+ public bool ContainsKey(string key) => _arguments.ContainsKey(key);
+
+ ///
+ public void CopyTo(KeyValuePair[] array, int arrayIndex) =>
+ ((ICollection>)_arguments).CopyTo(array, arrayIndex);
+
+ ///
+ public IEnumerator> GetEnumerator() => _arguments.GetEnumerator();
+
+ ///
+ public bool Remove(string key) => _arguments.Remove(key);
+
+ ///
+ bool ICollection>.Remove(KeyValuePair item) =>
+ ((ICollection>)_arguments).Remove(item);
+
+ ///
+ public bool TryGetValue(string key, out object? value) => _arguments.TryGetValue(key, out value);
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ /// Gets the comparer used by the underlying dictionary.
+ protected IEqualityComparer Comparer => _arguments.Comparer;
+}
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.cs
index d096656fba56..457f726b35f3 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.cs
@@ -26,7 +26,7 @@ public partial class FunctionInvocationContextV2
private FunctionCallContent? _callContent;
/// The arguments used with the function.
- private AIFunctionArguments? _arguments;
+ private AIFunctionArgumentsV2? _arguments;
/// Initializes a new instance of the class.
public FunctionInvocationContextV2()
@@ -41,7 +41,7 @@ public AIFunction Function
}
/// Gets or sets the arguments associated with this invocation.
- public AIFunctionArguments Arguments
+ public AIFunctionArgumentsV2 Arguments
{
get => _arguments ??= [];
set => _arguments = Throw.IfNull(value);
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
index e155ad7a68d3..2bd2c2ac1e2b 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
@@ -832,7 +832,7 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul
/// The function invocation context and result.
protected virtual async Task<(FunctionInvocationContextV2 context, object? result)> TryInvokeFunctionAsync(FunctionInvocationContextV2 context, CancellationToken cancellationToken)
{
- var result = await context.Function.InvokeAsync(context.Arguments, cancellationToken);
+ var result = await context.Function.InvokeAsync(new(context.Arguments), cancellationToken);
return (context, result);
}
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
index 0415a6dc648b..1deff697cca2 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
@@ -166,15 +166,10 @@ protected override async Task ProcessFunctionCallAsync
return new(shouldTerminate: false, FunctionInvocationStatus.NotFound, callContent, result: null, exception: null);
}
- //if (callContent.Arguments is not null)
- //{
- // callContent.Arguments = new KernelArguments(callContent.Arguments);
- //}
-
var context = new AutoFunctionInvocationContext(options)
{
AIFunction = function,
- AIArguments = new AIFunctionArguments(callContent.Arguments) { Services = this.FunctionInvocationServices },
+ Arguments = new KernelArguments(callContent.Arguments ?? new Dictionary()) { Services = this.FunctionInvocationServices },
Messages = messages,
CallContent = callContent,
Iteration = iteration,
@@ -322,7 +317,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(
}
else
{
- result = await context.Function.InvokeAsync(context.Arguments, cancellationToken).ConfigureAwait(false);
+ result = await context.Function.InvokeAsync(new(context.Arguments), cancellationToken).ConfigureAwait(false);
}
return (context, result);
diff --git a/dotnet/src/SemanticKernel.Abstractions/Filters/AutoFunctionInvocation/AutoFunctionInvocationContext.cs b/dotnet/src/SemanticKernel.Abstractions/Filters/AutoFunctionInvocation/AutoFunctionInvocationContext.cs
index 1a31144e26bb..cd79f13cca6e 100644
--- a/dotnet/src/SemanticKernel.Abstractions/Filters/AutoFunctionInvocation/AutoFunctionInvocationContext.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/Filters/AutoFunctionInvocation/AutoFunctionInvocationContext.cs
@@ -93,8 +93,25 @@ public AutoFunctionInvocationContext(
///
public new KernelArguments? Arguments
{
- get => new(base.Arguments);
- init => base.Arguments = new(value);
+ get
+ {
+ if (base.Arguments is KernelArguments kernelArguments)
+ {
+ return kernelArguments;
+ }
+
+ throw new InvalidOperationException($"The arguments are not of type {nameof(KernelArguments)}, for those scenarios, use {nameof(this.AIArguments)} instead.");
+ }
+ init => base.Arguments = value ?? new();
+ }
+
+ ///
+ /// Get the with which this filter is associated.
+ ///
+ public AIFunctionArgumentsV2 AIArguments
+ {
+ get => base.Arguments;
+ init => base.Arguments = value;
}
///
@@ -203,15 +220,6 @@ internal AIFunction AIFunction
set => base.Function = value;
}
- ///
- /// Gets or sets the arguments associated with this invocation context.
- ///
- internal AIFunctionArguments AIArguments
- {
- get => base.Arguments;
- set => base.Arguments = value;
- }
-
private static bool IsSameSchema(KernelFunction kernelFunction, AIFunction aiFunction)
{
// Compares the schemas, should be similar.
diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs
index eda736b3f583..7e71ee2c05a1 100644
--- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs
@@ -3,7 +3,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
+using Microsoft.Extensions.AI;
#pragma warning disable CA1710 // Identifiers should have correct suffix
@@ -17,10 +19,8 @@ namespace Microsoft.SemanticKernel;
/// A is a dictionary of argument names and values. It also carries a
/// , accessible via the property.
///
-public sealed class KernelArguments : IDictionary, IReadOnlyDictionary
+public sealed class KernelArguments : AIFunctionArgumentsV2
{
- /// Dictionary of name/values for all the arguments in the instance.
- private readonly Dictionary _arguments;
private IReadOnlyDictionary? _executionSettings;
///
@@ -28,8 +28,8 @@ public sealed class KernelArguments : IDictionary, IReadOnlyDic
///
[JsonConstructor]
public KernelArguments()
+ : base(StringComparer.OrdinalIgnoreCase)
{
- this._arguments = new(StringComparer.OrdinalIgnoreCase);
}
///
@@ -37,7 +37,7 @@ public KernelArguments()
///
/// The prompt execution settings.
public KernelArguments(PromptExecutionSettings? executionSettings)
- : this(executionSettings is null ? null : [executionSettings])
+ : this(executionSettings: executionSettings is null ? null : [executionSettings])
{
}
@@ -46,8 +46,8 @@ public KernelArguments(PromptExecutionSettings? executionSettings)
///
/// The prompt execution settings.
public KernelArguments(IEnumerable? executionSettings)
+ : base(StringComparer.OrdinalIgnoreCase)
{
- this._arguments = new(StringComparer.OrdinalIgnoreCase);
if (executionSettings is not null)
{
var newExecutionSettings = new Dictionary();
@@ -80,10 +80,8 @@ public KernelArguments(IEnumerable? executionSettings)
/// Otherwise, if the source is a , its are used.
///
public KernelArguments(IDictionary source, Dictionary? executionSettings = null)
+ : base(source, StringComparer.OrdinalIgnoreCase)
{
- Verify.NotNull(source);
-
- this._arguments = new(source, StringComparer.OrdinalIgnoreCase);
this.ExecutionSettings = executionSettings ?? (source as KernelArguments)?.ExecutionSettings;
}
@@ -115,37 +113,6 @@ public IReadOnlyDictionary? ExecutionSettings
}
}
- ///
- /// Gets the number of arguments contained in the .
- ///
- public int Count => this._arguments.Count;
-
- /// Adds the specified argument name and value to the .
- /// The name of the argument to add.
- /// The value of the argument to add.
- /// is null.
- /// An argument with the same name already exists in the .
- public void Add(string name, object? value)
- {
- Verify.NotNull(name);
- this._arguments.Add(name, value);
- }
-
- /// Removes the argument value with the specified name from the .
- /// The name of the argument value to remove.
- /// is null.
- public bool Remove(string name)
- {
- Verify.NotNull(name);
- return this._arguments.Remove(name);
- }
-
- /// Removes all arguments names and values from the .
- ///
- /// This does not affect the property. To clear it as well, set it to null.
- ///
- public void Clear() => this._arguments.Clear();
-
/// Determines whether the contains an argument with the specified name.
/// The name of the argument to locate.
/// true if the arguments contains an argument with the specified named; otherwise, false.
@@ -153,103 +120,9 @@ public bool Remove(string name)
public bool ContainsName(string name)
{
Verify.NotNull(name);
- return this._arguments.ContainsKey(name);
- }
-
- /// Gets the value associated with the specified argument name.
- /// The name of the argument value to get.
- ///
- /// When this method returns, contains the value associated with the specified name,
- /// if the name is found; otherwise, null.
- ///
- /// true if the arguments contains an argument with the specified name; otherwise, false.
- /// is null.
- public bool TryGetValue(string name, out object? value)
- {
- Verify.NotNull(name);
- return this._arguments.TryGetValue(name, out value);
- }
-
- /// Gets or sets the value associated with the specified argument name.
- /// The name of the argument value to get or set.
- /// is null.
- public object? this[string name]
- {
- get
- {
- Verify.NotNull(name);
- return this._arguments[name];
- }
- set
- {
- Verify.NotNull(name);
- this._arguments[name] = value;
- }
- }
-
- /// Gets an of all of the arguments' names.
- public ICollection Names => this._arguments.Keys;
-
- /// Gets an of all of the arguments' values.
- public ICollection Values => this._arguments.Values;
-
- #region Interface implementations
- ///
- ICollection IDictionary.Keys => this._arguments.Keys;
-
- ///
- IEnumerable IReadOnlyDictionary.Keys => this._arguments.Keys;
-
- ///
- IEnumerable IReadOnlyDictionary.Values => this._arguments.Values;
-
- ///
- bool ICollection>.IsReadOnly => false;
-
- ///
- object? IReadOnlyDictionary.this[string key] => this._arguments[key];
-
- ///
- object? IDictionary.this[string key]
- {
- get => this._arguments[key];
- set => this._arguments[key] = value;
+ return base.ContainsKey(name);
}
- ///
- void IDictionary.Add(string key, object? value) => this._arguments.Add(key, value);
-
- ///
- bool IDictionary.ContainsKey(string key) => this._arguments.ContainsKey(key);
-
- ///
- bool IDictionary.Remove(string key) => this._arguments.Remove(key);
-
- ///
- bool IDictionary.TryGetValue(string key, out object? value) => this._arguments.TryGetValue(key, out value);
-
- ///
- void ICollection>.Add(KeyValuePair item) => this._arguments.Add(item.Key, item.Value);
-
- ///
- bool ICollection>.Contains(KeyValuePair item) => ((ICollection>)this._arguments).Contains(item);
-
- ///
- void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => ((ICollection>)this._arguments).CopyTo(array, arrayIndex);
-
- ///
- bool ICollection>.Remove(KeyValuePair item) => this._arguments.Remove(item.Key);
-
- ///
- IEnumerator> IEnumerable>.GetEnumerator() => this._arguments.GetEnumerator();
-
- ///
- IEnumerator IEnumerable.GetEnumerator() => this._arguments.GetEnumerator();
-
- ///
- bool IReadOnlyDictionary.ContainsKey(string key) => this._arguments.ContainsKey(key);
-
- ///
- bool IReadOnlyDictionary.TryGetValue(string key, out object? value) => this._arguments.TryGetValue(key, out value);
- #endregion
+ /// Gets an of all of the arguments names.
+ public ICollection Names => this.Keys;
}
From 968190f1f056ad3161e52165f509d323483a77fd Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 22 Apr 2025 17:32:57 +0100
Subject: [PATCH 05/24] Fix typo
---
.../AI/ChatClient/FunctionInvokingChatClientV2.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
index 2bd2c2ac1e2b..7c61416872cc 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
@@ -438,7 +438,7 @@ private static void FixupHistories(
else if (lastIterationHadThreadId)
{
// In the very rare case where the inner client returned a response with a thread ID but then
- // returned a subsequent response without one, we want to reconstitue the full history. To do that,
+ // returned a subsequent response without one, we want to reconstitute the full history. To do that,
// we can populate the history with the original chat messages and then all of the response
// messages up until this point, which includes the most recent ones.
augmentedHistory ??= [];
From 193be17fa0da3f3e30ec2d6a8e68812ebf183a97 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 22 Apr 2025 21:20:37 +0100
Subject: [PATCH 06/24] Fix warnings
---
.../AI/ChatClient/AIFunctionArgumentsV2.cs | 4 +++-
.../AI/ChatClient/FunctionInvocationContextV2.Extra.cs | 9 +++++----
.../AI/ChatClient/FunctionInvocationContextV2.cs | 9 +++++++--
.../AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs | 6 +++---
.../AI/ChatClient/FunctionInvokingChatClientV2.cs | 6 +++++-
.../AI/ChatClient/KernelFunctionInvokingChatClient.cs | 1 -
.../Functions/KernelArguments.cs | 2 --
7 files changed, 23 insertions(+), 14 deletions(-)
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/AIFunctionArgumentsV2.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/AIFunctionArgumentsV2.cs
index b28bbfdec420..a9b5077ab540 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/AIFunctionArgumentsV2.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/AIFunctionArgumentsV2.cs
@@ -1,4 +1,6 @@
-// Licensed to the .NET Foundation under one or more agreements.
+#pragma warning disable IDE0073 // The file header does not match the required text
+
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.Extra.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.Extra.cs
index d1a20e00c348..03dfcf1d8246 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.Extra.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.Extra.cs
@@ -1,12 +1,13 @@
-// Licensed to the .NET Foundation under one or more agreements.
+#pragma warning disable IDE0073 // The file header does not match the required text
+
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
-using Microsoft.Shared.Collections;
-using Microsoft.Shared.Diagnostics;
+
+#pragma warning disable IDE0055 // Fix formatting
namespace Microsoft.Extensions.AI;
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.cs
index 457f726b35f3..a1698cea64e6 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvocationContextV2.cs
@@ -1,9 +1,13 @@
-// Licensed to the .NET Foundation under one or more agreements.
+#pragma warning disable IDE0073 // The file header does not match the required text
+
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
-using Microsoft.Shared.Diagnostics;
+
+#pragma warning disable IDE1006 // Naming Styles
+#pragma warning disable IDE0009 // Add this or Me qualification
namespace Microsoft.Extensions.AI;
@@ -15,6 +19,7 @@ public partial class FunctionInvocationContextV2
/// start with this as the target function.
///
private static readonly AIFunction _nopFunction = AIFunctionFactory.Create(() => { }, nameof(FunctionInvocationContext));
+#pragma warning restore IDE1006 // Naming Styles
/// The chat contents associated with the operation that initiated this function call request.
private IList _messages = Array.Empty();
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs
index 12edbbb4d15e..e2aa2b85db11 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs
@@ -1,7 +1,7 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
+#pragma warning disable IDE0073 // The file header does not match the required text
-// Intended for deletion
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Diagnostics.CodeAnalysis;
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
index 7c61416872cc..18b0f23c8444 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.cs
@@ -1,4 +1,6 @@
-// Licensed to the .NET Foundation under one or more agreements.
+#pragma warning disable IDE0073 // The file header does not match the required text
+
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
@@ -18,6 +20,8 @@
#pragma warning disable SA1202 // 'protected' members should come before 'private' members
#pragma warning disable VSTHRD111 // Use ConfigureAwait(bool)
#pragma warning disable CA2007 // Use ConfigureAwait
+#pragma warning disable IDE0009 // Add this or Me qualification
+#pragma warning disable IDE1006 // Naming Styles
namespace Microsoft.Extensions.AI;
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
index 1deff697cca2..d263e3c156d5 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-#pragma warning restore IDE0073 // The file header does not match the required text
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs
index 7e71ee2c05a1..2c6d406e497a 100644
--- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs
@@ -1,9 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.Collections;
using System.Collections.Generic;
-using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Microsoft.Extensions.AI;
From a778718069b4d6f9ff6de3a57e3cbebb8e1d02e2 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 22 Apr 2025 21:24:21 +0100
Subject: [PATCH 07/24] Fix formatting
---
.../AI/ChatClient/AIFunctionArgumentsV2.cs | 1 +
.../FunctionInvokingChatClientV2.Extra.cs | 16 ++++++++--------
.../KernelFunctionInvokingChatClient.cs | 3 ---
3 files changed, 9 insertions(+), 11 deletions(-)
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/AIFunctionArgumentsV2.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/AIFunctionArgumentsV2.cs
index a9b5077ab540..6008d7077a1b 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/AIFunctionArgumentsV2.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/AIFunctionArgumentsV2.cs
@@ -11,6 +11,7 @@
#pragma warning disable SA1112 // Closing parenthesis should be on line of opening parenthesis
#pragma warning disable SA1114 // Parameter list should follow declaration
#pragma warning disable CA1710 // Identifiers should have correct suffix
+#pragma warning disable IDE0009 // Add this or Me qualification
namespace Microsoft.Extensions.AI;
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs
index e2aa2b85db11..c6fc614ed179 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/FunctionInvokingChatClientV2.Extra.cs
@@ -37,22 +37,22 @@ public static T IfNull([NotNull] T argument, [CallerArgumentExpression(nameof
///
/// A message that describes the error.
#if !NET6_0_OR_GREATER
- [MethodImpl(MethodImplOptions.NoInlining)]
+ [MethodImpl(MethodImplOptions.NoInlining)]
#endif
[DoesNotReturn]
public static void InvalidOperationException(string message)
- => throw new InvalidOperationException(message);
+ => throw new InvalidOperationException(message);
///
/// Throws an