diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs index 766376ee00b9..d8342b4991d4 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs @@ -46,7 +46,7 @@ public void ConvertToolCallUpdatesWithEmptyIndexesReturnsEmptyToolCalls() var functionArgumentBuildersByIndex = new Dictionary(); // Act - var toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + var toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); @@ -64,7 +64,7 @@ public void ConvertToolCallUpdatesWithNotEmptyIndexesReturnsNotEmptyToolCalls() var functionArgumentBuildersByIndex = new Dictionary { { 3, new("test-argument") } }; // Act - var toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + var toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIStreamingTextContentTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIStreamingTextContentTests.cs deleted file mode 100644 index a58df5676aca..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIStreamingTextContentTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAIStreamingTextContentTests -{ - [Fact] - public void ToByteArrayWorksCorrectly() - { - // Arrange - var expectedBytes = Encoding.UTF8.GetBytes("content"); - var content = new AzureOpenAIStreamingTextContent("content", 0, "model-id"); - - // Act - var actualBytes = content.ToByteArray(); - - // Assert - Assert.Equal(expectedBytes, actualBytes); - } - - [Theory] - [InlineData(null, "")] - [InlineData("content", "content")] - public void ToStringWorksCorrectly(string? content, string expectedString) - { - // Arrange - var textContent = new AzureOpenAIStreamingTextContent(content!, 0, "model-id"); - - // Act - var actualString = textContent.ToString(); - - // Assert - Assert.Equal(expectedString, actualString); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs index 23412f666e23..5d49fdf91b46 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs @@ -43,7 +43,7 @@ public static async IAsyncEnumerable AddStreamingMe (contentBuilder ??= new()).Append(contentUpdate); } - AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatMessage.ToolCallUpdate, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatMessage.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); // Is always expected to have at least one chunk with the role provided from a streaming message streamedRole ??= chatMessage.Role; @@ -62,7 +62,7 @@ public static async IAsyncEnumerable AddStreamingMe role, contentBuilder?.ToString() ?? string.Empty, messageContents[0].ModelId!, - AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls(ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex), + AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls(ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex), metadata) { AuthorName = streamedName }); } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs index c37321e48c4d..348f65781734 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs @@ -44,7 +44,7 @@ internal AzureOpenAIClientCore( Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); Verify.NotNullOrWhiteSpace(apiKey); - var options = GetOpenAIClientOptions(httpClient); + var options = GetAzureOpenAIClientOptions(httpClient); this.DeploymentOrModelName = deploymentName; this.Endpoint = new Uri(endpoint); @@ -70,7 +70,7 @@ internal AzureOpenAIClientCore( Verify.NotNullOrWhiteSpace(endpoint); Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); - var options = GetOpenAIClientOptions(httpClient); + var options = GetAzureOpenAIClientOptions(httpClient); this.DeploymentOrModelName = deploymentName; this.Endpoint = new Uri(endpoint); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs index e618f27a9b15..361c617f31a0 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs @@ -139,7 +139,7 @@ internal static void TrackStreamingToolingUpdate( /// Dictionary mapping indices to IDs. /// Dictionary mapping indices to names. /// Dictionary mapping indices to arguments. - internal static ChatToolCall[] ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + internal static ChatToolCall[] ConvertToolCallUpdatesToFunctionToolCalls( ref Dictionary? toolCallIdsByIndex, ref Dictionary? functionNamesByIndex, ref Dictionary? functionArgumentBuildersByIndex) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs index 9287499e1621..fce885482899 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs @@ -41,7 +41,7 @@ internal AzureOpenAIStreamingChatMessageContent( Encoding.UTF8, metadata) { - this.ToolCallUpdate = chatUpdate.ToolCallUpdates; + this.ToolCallUpdates = chatUpdate.ToolCallUpdates; this.FinishReason = chatUpdate.FinishReason; this.Items = CreateContentItems(chatUpdate.ContentUpdate); } @@ -51,7 +51,7 @@ internal AzureOpenAIStreamingChatMessageContent( /// /// Author role of the message /// Content of the message - /// Tool call update + /// Tool call updates /// Completion finish reason /// Index of the choice /// The model ID used to generate the content @@ -59,7 +59,7 @@ internal AzureOpenAIStreamingChatMessageContent( internal AzureOpenAIStreamingChatMessageContent( AuthorRole? authorRole, string? content, - IReadOnlyList? tootToolCallUpdate = null, + IReadOnlyList? toolCallUpdates = null, ChatFinishReason? completionsFinishReason = null, int choiceIndex = 0, string? modelId = null, @@ -73,12 +73,12 @@ internal AzureOpenAIStreamingChatMessageContent( Encoding.UTF8, metadata) { - this.ToolCallUpdate = tootToolCallUpdate; + this.ToolCallUpdates = toolCallUpdates; this.FinishReason = completionsFinishReason; } /// Gets any update information in the message about a tool call. - public IReadOnlyList? ToolCallUpdate { get; } + public IReadOnlyList? ToolCallUpdates { get; } /// public override byte[] ToByteArray() => this.Encoding.GetBytes(this.ToString()); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingTextContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingTextContent.cs deleted file mode 100644 index 9d9497fd68d5..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingTextContent.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Azure OpenAI specialized streaming text content. -/// -/// -/// Represents a text content chunk that was streamed from the remote model. -/// -public sealed class AzureOpenAIStreamingTextContent : StreamingTextContent -{ - /// - /// Create a new instance of the class. - /// - /// Text update - /// Index of the choice - /// The model ID used to generate the content - /// Inner chunk object - /// Metadata information - internal AzureOpenAIStreamingTextContent( - string text, - int choiceIndex, - string modelId, - object? innerContentObject = null, - IReadOnlyDictionary? metadata = null) - : base( - text, - choiceIndex, - modelId, - innerContentObject, - Encoding.UTF8, - metadata) - { - } - - /// - public override byte[] ToByteArray() - { - return this.Encoding.GetBytes(this.ToString()); - } - - /// - public override string ToString() - { - return this.Text ?? string.Empty; - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs index 4152f2137409..9dea5efb2cf9 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -123,7 +123,7 @@ internal ClientCore(ILogger? logger = null) unit: "{token}", description: "Number of tokens used"); - private static Dictionary GetChatChoiceMetadata(OpenAIChatCompletion completions) + private static Dictionary GetChatCompletionMetadata(OpenAIChatCompletion completions) { #pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. return new Dictionary(8) @@ -142,7 +142,7 @@ internal ClientCore(ILogger? logger = null) #pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } - private static Dictionary GetResponseMetadata(StreamingChatCompletionUpdate completionUpdate) + private static Dictionary GetChatCompletionMetadata(StreamingChatCompletionUpdate completionUpdate) { return new Dictionary(4) { @@ -265,47 +265,47 @@ internal async Task> GetChatMessageContentsAsy ValidateMaxTokens(chatExecutionSettings.MaxTokens); - var chatMessages = CreateChatCompletionMessages(chatExecutionSettings, chat); + var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); for (int requestIndex = 0; ; requestIndex++) { var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); - var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); + var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); // Make the request. - OpenAIChatCompletion? responseData = null; - AzureOpenAIChatMessageContent responseContent; + OpenAIChatCompletion? chatCompletion = null; + AzureOpenAIChatMessageContent chatMessageContent; using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) { try { - responseData = (await RunRequestAsync(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatAsync(chatMessages, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; + chatCompletion = (await RunRequestAsync(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatAsync(chatForRequest, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; - this.LogUsage(responseData.Usage); + this.LogUsage(chatCompletion.Usage); } catch (Exception ex) when (activity is not null) { activity.SetError(ex); - if (responseData != null) + if (chatCompletion != null) { // Capture available metadata even if the operation failed. activity - .SetResponseId(responseData.Id) - .SetPromptTokenUsage(responseData.Usage.InputTokens) - .SetCompletionTokenUsage(responseData.Usage.OutputTokens); + .SetResponseId(chatCompletion.Id) + .SetPromptTokenUsage(chatCompletion.Usage.InputTokens) + .SetCompletionTokenUsage(chatCompletion.Usage.OutputTokens); } throw; } - responseContent = this.GetChatMessage(responseData); - activity?.SetCompletionResponse([responseContent], responseData.Usage.InputTokens, responseData.Usage.OutputTokens); + chatMessageContent = this.CreateChatMessageContent(chatCompletion); + activity?.SetCompletionResponse([chatMessageContent], chatCompletion.Usage.InputTokens, chatCompletion.Usage.OutputTokens); } // If we don't want to attempt to invoke any functions, just return the result. if (!toolCallingConfig.AutoInvoke) { - return [responseContent]; + return [chatMessageContent]; } Debug.Assert(kernel is not null); @@ -315,37 +315,37 @@ internal async Task> GetChatMessageContentsAsy // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool // is specified. - if (responseData.ToolCalls.Count == 0) + if (chatCompletion.ToolCalls.Count == 0) { - return [responseContent]; + return [chatMessageContent]; } if (this.Logger.IsEnabled(LogLevel.Debug)) { - this.Logger.LogDebug("Tool requests: {Requests}", responseData.ToolCalls.Count); + this.Logger.LogDebug("Tool requests: {Requests}", chatCompletion.ToolCalls.Count); } if (this.Logger.IsEnabled(LogLevel.Trace)) { - this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", responseData.ToolCalls.OfType().Select(ftc => $"{ftc.FunctionName}({ftc.FunctionArguments})"))); + this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", chatCompletion.ToolCalls.OfType().Select(ftc => $"{ftc.FunctionName}({ftc.FunctionArguments})"))); } // Add the original assistant message to the chat messages; this is required for the service // to understand the tool call responses. Also add the result message to the caller's chat // history: if they don't want it, they can remove it, but this makes the data available, // including metadata like usage. - chatMessages.Add(GetRequestMessage(responseData)); - chat.Add(responseContent); + chatForRequest.Add(CreateRequestMessage(chatCompletion)); + chat.Add(chatMessageContent); // We must send back 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. - for (int toolCallIndex = 0; toolCallIndex < responseContent.ToolCalls.Count; toolCallIndex++) + for (int toolCallIndex = 0; toolCallIndex < chatMessageContent.ToolCalls.Count; toolCallIndex++) { - ChatToolCall functionToolCall = responseContent.ToolCalls[toolCallIndex]; + ChatToolCall functionToolCall = chatMessageContent.ToolCalls[toolCallIndex]; // We currently only know about function tool calls. If it's anything else, we'll respond with an error. if (functionToolCall.Kind != ChatToolCallKind.Function) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Tool call was not a function call.", functionToolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", functionToolCall, this.Logger); continue; } @@ -357,7 +357,7 @@ internal async Task> GetChatMessageContentsAsy } catch (JsonException) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Function call arguments were invalid JSON.", functionToolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", functionToolCall, this.Logger); continue; } @@ -367,14 +367,14 @@ internal async Task> GetChatMessageContentsAsy if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && !IsRequestableTool(chatOptions, azureOpenAIFunctionToolCall)) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Function call request for a function that wasn't defined.", functionToolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", functionToolCall, this.Logger); continue; } // Find the function in the kernel and populate the arguments. if (!kernel!.Plugins.TryGetFunctionAndArguments(azureOpenAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); continue; } @@ -385,7 +385,7 @@ internal async Task> GetChatMessageContentsAsy Arguments = functionArgs, RequestSequenceIndex = requestIndex, FunctionSequenceIndex = toolCallIndex, - FunctionCount = responseContent.ToolCalls.Count + FunctionCount = chatMessageContent.ToolCalls.Count }; s_inflightAutoInvokes.Value++; @@ -409,7 +409,7 @@ internal async Task> GetChatMessageContentsAsy catch (Exception e) #pragma warning restore CA1031 // Do not catch general exception types { - AddResponseMessage(chatMessages, chat, null, $"Error: Exception while invoking function. {e.Message}", functionToolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, null, $"Error: Exception while invoking function. {e.Message}", functionToolCall, this.Logger); continue; } finally @@ -423,7 +423,7 @@ internal async Task> GetChatMessageContentsAsy object functionResultValue = functionResult.GetValue() ?? string.Empty; var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - AddResponseMessage(chatMessages, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); // If filter requested termination, returning latest function result. if (invocationContext.Terminate) @@ -463,13 +463,13 @@ internal async IAsyncEnumerable GetStrea Dictionary? functionNamesByIndex = null; Dictionary? functionArgumentBuildersByIndex = null; - var chatMessages = CreateChatCompletionMessages(chatExecutionSettings, chat); + var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); for (int requestIndex = 0; ; requestIndex++) { var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); - var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); + var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); // Reset state contentBuilder?.Clear(); @@ -491,7 +491,7 @@ internal async IAsyncEnumerable GetStrea AsyncResultCollection response; try { - response = RunRequest(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatStreamingAsync(chatMessages, chatOptions, cancellationToken)); + response = RunRequest(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatStreamingAsync(chatForRequest, chatOptions, cancellationToken)); } catch (Exception ex) when (activity is not null) { @@ -518,16 +518,16 @@ internal async IAsyncEnumerable GetStrea throw; } - StreamingChatCompletionUpdate update = responseEnumerator.Current; - metadata = GetResponseMetadata(update); - streamedRole ??= update.Role; + StreamingChatCompletionUpdate chatCompletionUpdate = responseEnumerator.Current; + metadata = GetChatCompletionMetadata(chatCompletionUpdate); + streamedRole ??= chatCompletionUpdate.Role; //streamedName ??= update.AuthorName; - finishReason = update.FinishReason ?? default; + finishReason = chatCompletionUpdate.FinishReason ?? default; // If we're intending to invoke function calls, we need to consume that function call information. if (toolCallingConfig.AutoInvoke) { - foreach (var contentPart in update.ContentUpdate) + foreach (var contentPart in chatCompletionUpdate.ContentUpdate) { if (contentPart.Kind == ChatMessageContentPartKind.Text) { @@ -535,12 +535,12 @@ internal async IAsyncEnumerable GetStrea } } - AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(update.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); } - var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(update, 0, this.DeploymentOrModelName, metadata); + var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, this.DeploymentOrModelName, metadata); - foreach (var functionCallUpdate in update.ToolCallUpdates) + foreach (var functionCallUpdate in chatCompletionUpdate.ToolCallUpdates) { // Using the code below to distinguish and skip non - function call related updates. // The Kind property of updates can't be reliably used because it's only initialized for the first update. @@ -563,7 +563,7 @@ internal async IAsyncEnumerable GetStrea } // Translate all entries into ChatCompletionsFunctionToolCall instances. - toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); // Translate all entries into FunctionCallContent instances for diagnostics purposes. @@ -601,8 +601,8 @@ internal async IAsyncEnumerable GetStrea // Add the original assistant message to the chat messages; this is required for the service // to understand the tool call responses. - chatMessages.Add(GetRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); - chat.Add(this.GetChatMessage(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName)); + chatForRequest.Add(CreateRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); + chat.Add(this.CreateChatMessageContent(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName)); // Respond to each tooling request. for (int toolCallIndex = 0; toolCallIndex < toolCalls.Length; toolCallIndex++) @@ -612,7 +612,7 @@ internal async IAsyncEnumerable GetStrea // We currently only know about function tool calls. If it's anything else, we'll respond with an error. if (string.IsNullOrEmpty(toolCall.FunctionName)) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); continue; } @@ -624,7 +624,7 @@ internal async IAsyncEnumerable GetStrea } catch (JsonException) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); continue; } @@ -634,14 +634,14 @@ internal async IAsyncEnumerable GetStrea if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && !IsRequestableTool(chatOptions, openAIFunctionToolCall)) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); continue; } // Find the function in the kernel and populate the arguments. if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); continue; } @@ -676,7 +676,7 @@ internal async IAsyncEnumerable GetStrea catch (Exception e) #pragma warning restore CA1031 // Do not catch general exception types { - AddResponseMessage(chatMessages, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); continue; } finally @@ -690,7 +690,7 @@ internal async IAsyncEnumerable GetStrea object functionResultValue = functionResult.GetValue() ?? string.Empty; var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - AddResponseMessage(chatMessages, chat, stringResult, errorMessage: null, toolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, toolCall, this.Logger); // If filter requested termination, returning latest function result and breaking request iteration loop. if (invocationContext.Terminate) @@ -765,7 +765,7 @@ internal void AddAttribute(string key, string? value) /// Gets options to use for an OpenAIClient /// Custom for HTTP requests. /// An instance of . - internal static AzureOpenAIClientOptions GetOpenAIClientOptions(HttpClient? httpClient) + internal static AzureOpenAIClientOptions GetAzureOpenAIClientOptions(HttpClient? httpClient) { AzureOpenAIClientOptions options = new() { @@ -811,7 +811,7 @@ private static ChatHistory CreateNewChat(string? text = null, AzureOpenAIPromptE return chat; } - private ChatCompletionOptions CreateChatCompletionsOptions( + private ChatCompletionOptions CreateChatCompletionOptions( AzureOpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory, ToolCallingConfig toolCallingConfig, @@ -874,13 +874,13 @@ private static List CreateChatCompletionMessages(AzureOpenAIPromptE foreach (var message in chatHistory) { - messages.AddRange(GetRequestMessages(message, executionSettings.ToolCallBehavior)); + messages.AddRange(CreateRequestMessages(message, executionSettings.ToolCallBehavior)); } return messages; } - private static ChatMessage GetRequestMessage(ChatMessageRole chatRole, string content, string? name, ChatToolCall[]? tools) + private static ChatMessage CreateRequestMessage(ChatMessageRole chatRole, string content, string? name, ChatToolCall[]? tools) { if (chatRole == ChatMessageRole.User) { @@ -900,7 +900,7 @@ private static ChatMessage GetRequestMessage(ChatMessageRole chatRole, string co throw new NotImplementedException($"Role {chatRole} is not implemented"); } - private static List GetRequestMessages(ChatMessageContent message, AzureOpenAIToolCallBehavior? toolCallBehavior) + private static List CreateRequestMessages(ChatMessageContent message, AzureOpenAIToolCallBehavior? toolCallBehavior) { if (message.Role == AuthorRole.System) { @@ -1043,7 +1043,7 @@ private static ChatMessageContentPart GetImageContentItem(ImageContent imageCont throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); } - private static ChatMessage GetRequestMessage(OpenAIChatCompletion completion) + private static ChatMessage CreateRequestMessage(OpenAIChatCompletion completion) { if (completion.Role == ChatMessageRole.System) { @@ -1063,16 +1063,16 @@ private static ChatMessage GetRequestMessage(OpenAIChatCompletion completion) throw new NotSupportedException($"Role {completion.Role} is not supported."); } - private AzureOpenAIChatMessageContent GetChatMessage(OpenAIChatCompletion completion) + private AzureOpenAIChatMessageContent CreateChatMessageContent(OpenAIChatCompletion completion) { - var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentOrModelName, GetChatChoiceMetadata(completion)); + var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentOrModelName, GetChatCompletionMetadata(completion)); message.Items.AddRange(this.GetFunctionCallContents(completion.ToolCalls)); return message; } - private AzureOpenAIChatMessageContent GetChatMessage(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) + private AzureOpenAIChatMessageContent CreateChatMessageContent(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) { var message = new AzureOpenAIChatMessageContent(chatRole, content, this.DeploymentOrModelName, toolCalls, metadata) { diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs index 782889c4542c..f946d09026a0 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs @@ -242,8 +242,8 @@ public static IServiceCollection AddAzureOpenAIChatCompletion( #endregion private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) => - new(new Uri(endpoint), credentials, ClientCore.GetOpenAIClientOptions(httpClient)); + new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, TokenCredential credentials, HttpClient? httpClient) => - new(new Uri(endpoint), credentials, ClientCore.GetOpenAIClientOptions(httpClient)); + new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); } diff --git a/dotnet/src/IntegrationTestsV2/BaseIntegrationTest.cs b/dotnet/src/IntegrationTestsV2/BaseIntegrationTest.cs new file mode 100644 index 000000000000..a86274d4f8ce --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/BaseIntegrationTest.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience; +using Microsoft.SemanticKernel; + +namespace SemanticKernel.IntegrationTestsV2; + +public class BaseIntegrationTest +{ + protected IKernelBuilder CreateKernelBuilder() + { + var builder = Kernel.CreateBuilder(); + + builder.Services.ConfigureHttpClientDefaults(c => + { + c.AddStandardResilienceHandler().Configure(o => + { + o.Retry.ShouldRetryAfterHeader = true; + o.Retry.ShouldHandle = args => ValueTask.FromResult(args.Outcome.Result?.StatusCode is HttpStatusCode.TooManyRequests); + o.CircuitBreaker = new HttpCircuitBreakerStrategyOptions + { + SamplingDuration = TimeSpan.FromSeconds(40.0), // The duration should be least double of an attempt timeout + }; + o.AttemptTimeout = new HttpTimeoutStrategyOptions + { + Timeout = TimeSpan.FromSeconds(20.0) // Doubling the default 10s timeout + }; + }); + }); + + return builder; + } +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs index 04f1be7e45c7..69509508af98 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs @@ -22,7 +22,7 @@ namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. -public sealed class AzureOpenAIChatCompletionTests +public sealed class AzureOpenAIChatCompletionTests : BaseIntegrationTest { [Fact] //[Fact(Skip = "Skipping while we investigate issue with GitHub actions.")] @@ -74,13 +74,15 @@ public async Task AzureOpenAIHttpRetryPolicyTestAsync() var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - this._kernelBuilder.AddAzureOpenAIChatCompletion( + var kernelBuilder = Kernel.CreateBuilder(); + + kernelBuilder.AddAzureOpenAIChatCompletion( deploymentName: azureOpenAIConfiguration!.ChatDeploymentName!, modelId: azureOpenAIConfiguration.ChatModelId, endpoint: azureOpenAIConfiguration.Endpoint, apiKey: "INVALID_KEY"); - this._kernelBuilder.Services.ConfigureHttpClientDefaults(c => + kernelBuilder.Services.ConfigureHttpClientDefaults(c => { // Use a standard resiliency policy, augmented to retry on 401 Unauthorized for this example c.AddStandardResilienceHandler().Configure(o => @@ -94,7 +96,7 @@ public async Task AzureOpenAIHttpRetryPolicyTestAsync() }); }); - var target = this._kernelBuilder.Build(); + var target = kernelBuilder.Build(); var plugins = TestHelpers.ImportSamplePlugins(target, "SummarizePlugin"); @@ -237,7 +239,9 @@ private Kernel CreateAndInitializeKernel(HttpClient? httpClient = null) Assert.NotNull(azureOpenAIConfiguration.Endpoint); Assert.NotNull(azureOpenAIConfiguration.ServiceId); - this._kernelBuilder.AddAzureOpenAIChatCompletion( + var kernelBuilder = base.CreateKernelBuilder(); + + kernelBuilder.AddAzureOpenAIChatCompletion( deploymentName: azureOpenAIConfiguration.ChatDeploymentName, modelId: azureOpenAIConfiguration.ChatModelId, endpoint: azureOpenAIConfiguration.Endpoint, @@ -245,11 +249,10 @@ private Kernel CreateAndInitializeKernel(HttpClient? httpClient = null) serviceId: azureOpenAIConfiguration.ServiceId, httpClient: httpClient); - return this._kernelBuilder.Build(); + return kernelBuilder.Build(); } private const string InputParameterName = "input"; - private readonly IKernelBuilder _kernelBuilder = Kernel.CreateBuilder(); private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs index 5bbbd60c9005..f90102d62834 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs @@ -12,12 +12,11 @@ using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using OpenAI.Chat; using SemanticKernel.IntegrationTests.TestSettings; -using SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; using Xunit; -namespace SemanticKernel.IntegrationTests.Connectors.AzureOpenAI; +namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; -public sealed class AzureOpenAIChatCompletionFunctionCallingTests +public sealed class AzureOpenAIChatCompletionFunctionCallingTests : BaseIntegrationTest { [Fact] public async Task CanAutoInvokeKernelFunctionsAsync() @@ -707,7 +706,7 @@ private Kernel CreateAndInitializeKernel(bool importHelperPlugin = false) Assert.NotNull(azureOpenAIConfiguration.ApiKey); Assert.NotNull(azureOpenAIConfiguration.Endpoint); - var kernelBuilder = Kernel.CreateBuilder(); + var kernelBuilder = base.CreateKernelBuilder(); kernelBuilder.AddAzureOpenAIChatCompletion( deploymentName: azureOpenAIConfiguration.ChatDeploymentName, diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs index 72d5ff34dec4..5847ad29a6d1 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs @@ -17,7 +17,7 @@ namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. -public sealed class AzureOpenAIChatCompletionNonStreamingTests +public sealed class AzureOpenAIChatCompletionNonStreamingTests : BaseIntegrationTest { [Fact] public async Task ChatCompletionShouldUseChatSystemPromptAsync() @@ -158,7 +158,7 @@ private Kernel CreateAndInitializeKernel() Assert.NotNull(azureOpenAIConfiguration.ApiKey); Assert.NotNull(azureOpenAIConfiguration.Endpoint); - var kernelBuilder = Kernel.CreateBuilder(); + var kernelBuilder = base.CreateKernelBuilder(); kernelBuilder.AddAzureOpenAIChatCompletion( deploymentName: azureOpenAIConfiguration.ChatDeploymentName, diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs index 57fb1c73fb72..f340064b2ee3 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs @@ -16,7 +16,7 @@ namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. -public sealed class AzureOpenAIChatCompletionStreamingTests +public sealed class AzureOpenAIChatCompletionStreamingTests : BaseIntegrationTest { [Fact] public async Task ChatCompletionShouldUseChatSystemPromptAsync() @@ -152,7 +152,7 @@ private Kernel CreateAndInitializeKernel() Assert.NotNull(azureOpenAIConfiguration.ApiKey); Assert.NotNull(azureOpenAIConfiguration.Endpoint); - var kernelBuilder = Kernel.CreateBuilder(); + var kernelBuilder = base.CreateKernelBuilder(); kernelBuilder.AddAzureOpenAIChatCompletion( deploymentName: azureOpenAIConfiguration.ChatDeploymentName,