From 4e14f2ff196ee69351767de86acd7b8c069ccbec Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 27 Jun 2024 10:59:10 +0100 Subject: [PATCH 1/5] feat(azure open ai): migrate azure chat completion service to the azure sdk v2 --- ...AzureOpenAIPromptExecutionSettingsTests.cs | 6 +- .../AzureOpenAITestHelper.cs | 11 + .../AzureToolCallBehaviorTests.cs | 68 +- .../AzureOpenAIChatCompletionServiceTests.cs | 99 +- .../AzureOpenAIChatMessageContentTests.cs | 31 +- .../Core/AzureOpenAIFunctionToolCallTests.cs | 10 +- ...reOpenAIPluginCollectionExtensionsTests.cs | 8 +- .../AutoFunctionInvocationFilterTests.cs | 13 +- .../AzureOpenAIFunctionTests.cs | 42 +- .../KernelFunctionMetadataExtensionsTests.cs | 4 +- .../AddHeaderRequestPolicy.cs | 20 - .../AzureOpenAIPromptExecutionSettings.cs | 34 +- .../AzureToolCallBehavior.cs | 56 +- .../AzureOpenAIChatCompletionService.cs | 3 +- .../Connectors.AzureOpenAI.csproj | 3 +- .../Core/AzureOpenAIChatMessageContent.cs | 44 +- .../Core/AzureOpenAIClientCore.cs | 12 +- .../Core/AzureOpenAIFunction.cs | 20 +- .../Core/AzureOpenAIFunctionToolCall.cs | 52 +- .../AzureOpenAIPluginCollectionExtensions.cs | 4 +- .../AzureOpenAIStreamingChatMessageContent.cs | 35 +- .../Connectors.AzureOpenAI/Core/ClientCore.cs | 974 +++++++++--------- 22 files changed, 747 insertions(+), 802 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/AddHeaderRequestPolicy.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs index 0cf1c4e2a9e3..7b50e36c5587 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs @@ -26,12 +26,11 @@ public void ItCreatesOpenAIExecutionSettingsWithCorrectDefaults() Assert.Equal(1, executionSettings.TopP); Assert.Equal(0, executionSettings.FrequencyPenalty); Assert.Equal(0, executionSettings.PresencePenalty); - Assert.Equal(1, executionSettings.ResultsPerPrompt); Assert.Null(executionSettings.StopSequences); Assert.Null(executionSettings.TokenSelectionBiases); Assert.Null(executionSettings.TopLogprobs); Assert.Null(executionSettings.Logprobs); - Assert.Null(executionSettings.AzureChatExtensionsOptions); + Assert.Null(executionSettings.AzureChatDataSource); Assert.Equal(128, executionSettings.MaxTokens); } @@ -45,7 +44,6 @@ public void ItUsesExistingOpenAIExecutionSettings() TopP = 0.7, FrequencyPenalty = 0.7, PresencePenalty = 0.7, - ResultsPerPrompt = 2, StopSequences = new string[] { "foo", "bar" }, ChatSystemPrompt = "chat system prompt", MaxTokens = 128, @@ -231,7 +229,6 @@ public void PromptExecutionSettingsFreezeWorksAsExpected() // Assert Assert.True(executionSettings.IsFrozen); Assert.Throws(() => executionSettings.ModelId = "gpt-4"); - Assert.Throws(() => executionSettings.ResultsPerPrompt = 2); Assert.Throws(() => executionSettings.Temperature = 1); Assert.Throws(() => executionSettings.TopP = 1); Assert.Throws(() => executionSettings.StopSequences?.Add("STOP")); @@ -262,7 +259,6 @@ private static void AssertExecutionSettings(AzureOpenAIPromptExecutionSettings e Assert.Equal(0.7, executionSettings.TopP); Assert.Equal(0.7, executionSettings.FrequencyPenalty); Assert.Equal(0.7, executionSettings.PresencePenalty); - Assert.Equal(2, executionSettings.ResultsPerPrompt); Assert.Equal(new string[] { "foo", "bar" }, executionSettings.StopSequences); Assert.Equal("chat system prompt", executionSettings.ChatSystemPrompt); Assert.Equal(new Dictionary() { { 1, 2 }, { 3, 4 } }, executionSettings.TokenSelectionBiases); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs index 9df4aae40c2d..49aa51c7ce6a 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System.IO; +using System.Net.Http; +using System.Text; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; @@ -17,4 +19,13 @@ internal static string GetTestResponse(string fileName) { return File.ReadAllText($"./TestData/{fileName}"); } + + /// + /// Reads test response from file and create . + /// + /// Name of the file with test response. + internal static StreamContent GetTestResponseAsStream(string fileName) + { + return new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(AzureOpenAITestHelper.GetTestResponse(fileName)))); + } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs index 525dabcd26d2..abb0851221ab 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using OpenAI.Chat; using static Microsoft.SemanticKernel.Connectors.AzureOpenAI.AzureToolCallBehavior; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; @@ -63,13 +63,13 @@ public void KernelFunctionsConfigureOptionsWithNullKernelDoesNotAddTools() { // Arrange var kernelFunctions = new KernelFunctions(autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); // Act - kernelFunctions.ConfigureOptions(null, chatCompletionsOptions); + var options = kernelFunctions.ConfigureOptions(null); // Assert - Assert.Empty(chatCompletionsOptions.Tools); + Assert.Null(options.Choice); + Assert.Null(options.Tools); } [Fact] @@ -77,15 +77,14 @@ public void KernelFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() { // Arrange var kernelFunctions = new KernelFunctions(autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); // Act - kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + var options = kernelFunctions.ConfigureOptions(kernel); // Assert - Assert.Null(chatCompletionsOptions.ToolChoice); - Assert.Empty(chatCompletionsOptions.Tools); + Assert.Null(options.Choice); + Assert.Null(options.Tools); } [Fact] @@ -93,7 +92,6 @@ public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() { // Arrange var kernelFunctions = new KernelFunctions(autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); var plugin = this.GetTestPlugin(); @@ -101,12 +99,12 @@ public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() kernel.Plugins.Add(plugin); // Act - kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + var options = kernelFunctions.ConfigureOptions(kernel); // Assert - Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); + Assert.Equal(ChatToolChoice.Auto, options.Choice); - this.AssertTools(chatCompletionsOptions); + this.AssertTools(options.Tools); } [Fact] @@ -114,14 +112,13 @@ public void EnabledFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() { // Arrange var enabledFunctions = new EnabledFunctions([], autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); // Act - enabledFunctions.ConfigureOptions(null, chatCompletionsOptions); + var options = enabledFunctions.ConfigureOptions(null); // Assert - Assert.Null(chatCompletionsOptions.ToolChoice); - Assert.Empty(chatCompletionsOptions.Tools); + Assert.Null(options.Choice); + Assert.Null(options.Tools); } [Fact] @@ -130,10 +127,9 @@ public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsExc // Arrange var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()); var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); - var chatCompletionsOptions = new ChatCompletionsOptions(); // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(null, chatCompletionsOptions)); + var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(null)); Assert.Equal($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided.", exception.Message); } @@ -143,11 +139,10 @@ public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsEx // Arrange var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()); var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); - var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(kernel, chatCompletionsOptions)); + var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(kernel)); Assert.Equal($"The specified {nameof(EnabledFunctions)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); } @@ -160,18 +155,17 @@ public void EnabledFunctionsConfigureOptionsWithKernelAndPluginsAddsTools(bool a var plugin = this.GetTestPlugin(); var functions = plugin.GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()); var enabledFunctions = new EnabledFunctions(functions, autoInvoke); - var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); kernel.Plugins.Add(plugin); // Act - enabledFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + var options = enabledFunctions.ConfigureOptions(kernel); // Assert - Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); + Assert.Equal(ChatToolChoice.Auto, options.Choice); - this.AssertTools(chatCompletionsOptions); + this.AssertTools(options.Tools); } [Fact] @@ -180,10 +174,9 @@ public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsEx // Arrange var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()).First(); var requiredFunction = new RequiredFunction(function, autoInvoke: true); - var chatCompletionsOptions = new ChatCompletionsOptions(); // Act & Assert - var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(null, chatCompletionsOptions)); + var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(null)); Assert.Equal($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided.", exception.Message); } @@ -193,11 +186,10 @@ public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsE // Arrange var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()).First(); var requiredFunction = new RequiredFunction(function, autoInvoke: true); - var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); // Act & Assert - var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(kernel, chatCompletionsOptions)); + var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(kernel)); Assert.Equal($"The specified {nameof(RequiredFunction)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); } @@ -207,18 +199,17 @@ public void RequiredFunctionConfigureOptionsAddsTools() // Arrange var plugin = this.GetTestPlugin(); var function = plugin.GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); - var chatCompletionsOptions = new ChatCompletionsOptions(); var requiredFunction = new RequiredFunction(function, autoInvoke: true); var kernel = new Kernel(); kernel.Plugins.Add(plugin); // Act - requiredFunction.ConfigureOptions(kernel, chatCompletionsOptions); + var options = requiredFunction.ConfigureOptions(kernel); // Assert - Assert.NotNull(chatCompletionsOptions.ToolChoice); + Assert.NotNull(options.Choice); - this.AssertTools(chatCompletionsOptions); + this.AssertTools(options.Tools); } private KernelPlugin GetTestPlugin() @@ -233,16 +224,15 @@ private KernelPlugin GetTestPlugin() return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); } - private void AssertTools(ChatCompletionsOptions chatCompletionsOptions) + private void AssertTools(IList? tools) { - Assert.Single(chatCompletionsOptions.Tools); - - var tool = chatCompletionsOptions.Tools[0] as ChatCompletionsFunctionToolDefinition; + Assert.NotNull(tools); + var tool = Assert.Single(tools); Assert.NotNull(tool); - Assert.Equal("MyPlugin-MyFunction", tool.Name); - Assert.Equal("Test Function", tool.Description); - Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{\"parameter1\":{\"type\":\"string\"},\"parameter2\":{\"type\":\"string\"}}}", tool.Parameters.ToString()); + Assert.Equal("MyPlugin-MyFunction", tool.FunctionName); + Assert.Equal("Test Function", tool.FunctionDescription); + Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{\"parameter1\":{\"type\":\"string\"},\"parameter2\":{\"type\":\"string\"}}}", tool.FunctionParameters.ToString()); } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index 69c314bdcb46..6921fdbe0706 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -10,6 +10,7 @@ using System.Text.Json; using System.Threading.Tasks; using Azure.AI.OpenAI; +using Azure.AI.OpenAI.Chat; using Azure.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -17,6 +18,7 @@ using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Moq; +using OpenAI.Chat; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.ChatCompletion; @@ -79,7 +81,7 @@ public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFacto public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) { // Arrange & Act - var client = new OpenAIClient("key"); + var client = new AzureOpenAIClient(new Uri("http://host"), "key"); var service = includeLoggerFactory ? new AzureOpenAIChatCompletionService("deployment", client, "model-id", loggerFactory: this._mockLoggerFactory.Object) : new AzureOpenAIChatCompletionService("deployment", client, "model-id"); @@ -106,45 +108,14 @@ public async Task GetTextContentsWorksCorrectlyAsync() Assert.True(result.Count > 0); Assert.Equal("Test chat response", result[0].Text); - var usage = result[0].Metadata?["Usage"] as CompletionsUsage; + var usage = result[0].Metadata?["Usage"] as ChatTokenUsage; Assert.NotNull(usage); - Assert.Equal(55, usage.PromptTokens); - Assert.Equal(100, usage.CompletionTokens); + Assert.Equal(55, usage.InputTokens); + Assert.Equal(100, usage.OutputTokens); Assert.Equal(155, usage.TotalTokens); } - [Fact] - public async Task GetChatMessageContentsWithEmptyChoicesThrowsExceptionAsync() - { - // Arrange - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("{\"id\":\"response-id\",\"object\":\"chat.completion\",\"created\":1704208954,\"model\":\"gpt-4\",\"choices\":[],\"usage\":{\"prompt_tokens\":55,\"completion_tokens\":100,\"total_tokens\":155},\"system_fingerprint\":null}") - }); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => service.GetChatMessageContentsAsync([])); - - Assert.Equal("Chat completions not found", exception.Message); - } - - [Theory] - [InlineData(0)] - [InlineData(129)] - public async Task GetChatMessageContentsWithInvalidResultsPerPromptValueThrowsExceptionAsync(int resultsPerPrompt) - { - // Arrange - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - var settings = new AzureOpenAIPromptExecutionSettings { ResultsPerPrompt = resultsPerPrompt }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => service.GetChatMessageContentsAsync([], settings)); - - Assert.Contains("The value must be in range between", exception.Message, StringComparison.OrdinalIgnoreCase); - } - [Fact] public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync() { @@ -157,22 +128,16 @@ public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync() TopP = 0.5, FrequencyPenalty = 1.6, PresencePenalty = 1.2, - ResultsPerPrompt = 5, Seed = 567, TokenSelectionBiases = new Dictionary { { 2, 3 } }, StopSequences = ["stop_sequence"], Logprobs = true, TopLogprobs = 5, - AzureChatExtensionsOptions = new AzureChatExtensionsOptions + AzureChatDataSource = new AzureSearchChatDataSource() { - Extensions = - { - new AzureSearchChatExtensionConfiguration - { - SearchEndpoint = new Uri("http://test-search-endpoint"), - IndexName = "test-index-name" - } - } + Endpoint = new Uri("http://test-search-endpoint"), + IndexName = "test-index-name", + Authentication = DataSourceAuthentication.FromApiKey("api-key"), } }; @@ -226,7 +191,6 @@ public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync() Assert.Equal(0.5, content.GetProperty("top_p").GetDouble()); Assert.Equal(1.6, content.GetProperty("frequency_penalty").GetDouble()); Assert.Equal(1.2, content.GetProperty("presence_penalty").GetDouble()); - Assert.Equal(5, content.GetProperty("n").GetInt32()); Assert.Equal(567, content.GetProperty("seed").GetInt32()); Assert.Equal(3, content.GetProperty("logit_bias").GetProperty("2").GetInt32()); Assert.Equal("stop_sequence", content.GetProperty("stop")[0].GetString()); @@ -259,7 +223,7 @@ public async Task GetChatMessageContentsHandlesResponseFormatCorrectlyAsync(obje }); // Act - var result = await service.GetChatMessageContentsAsync([], settings); + var result = await service.GetChatMessageContentsAsync(new ChatHistory("System message"), settings); // Assert var requestContent = this._messageHandlerStub.RequestContents[0]; @@ -286,20 +250,20 @@ public async Task GetChatMessageContentsWorksCorrectlyAsync(AzureToolCallBehavio }); // Act - var result = await service.GetChatMessageContentsAsync([], settings, kernel); + var result = await service.GetChatMessageContentsAsync(new ChatHistory("System message"), settings, kernel); // Assert Assert.True(result.Count > 0); Assert.Equal("Test chat response", result[0].Content); - var usage = result[0].Metadata?["Usage"] as CompletionsUsage; + var usage = result[0].Metadata?["Usage"] as ChatTokenUsage; Assert.NotNull(usage); - Assert.Equal(55, usage.PromptTokens); - Assert.Equal(100, usage.CompletionTokens); + Assert.Equal(55, usage.InputTokens); + Assert.Equal(100, usage.OutputTokens); Assert.Equal(155, usage.TotalTokens); - Assert.Equal("stop", result[0].Metadata?["FinishReason"]); + Assert.Equal("Stop", result[0].Metadata?["FinishReason"]); } [Fact] @@ -332,7 +296,7 @@ public async Task GetChatMessageContentsWithFunctionCallAsync() this._messageHandlerStub.ResponsesToReturn = [response1, response2]; // Act - var result = await service.GetChatMessageContentsAsync([], settings, kernel); + var result = await service.GetChatMessageContentsAsync(new ChatHistory("System message"), settings, kernel); // Assert Assert.True(result.Count > 0); @@ -372,7 +336,7 @@ public async Task GetChatMessageContentsWithFunctionCallMaximumAutoInvokeAttempt this._messageHandlerStub.ResponsesToReturn = responses; // Act - var result = await service.GetChatMessageContentsAsync([], settings, kernel); + var result = await service.GetChatMessageContentsAsync(new ChatHistory("System message"), settings, kernel); // Assert Assert.Equal(DefaultMaximumAutoInvokeAttempts, functionCallCount); @@ -405,7 +369,7 @@ public async Task GetChatMessageContentsWithRequiredFunctionCallAsync() this._messageHandlerStub.ResponsesToReturn = [response1, response2]; // Act - var result = await service.GetChatMessageContentsAsync([], settings, kernel); + var result = await service.GetChatMessageContentsAsync(new ChatHistory("System message"), settings, kernel); // Assert Assert.Equal(1, functionCallCount); @@ -447,7 +411,7 @@ public async Task GetStreamingTextContentsWorksCorrectlyAsync() Assert.Equal("Test chat streaming response", enumerator.Current.Text); await enumerator.MoveNextAsync(); - Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); + Assert.Equal("Stop", enumerator.Current.Metadata?["FinishReason"]); } [Fact] @@ -469,7 +433,7 @@ public async Task GetStreamingChatMessageContentsWorksCorrectlyAsync() Assert.Equal("Test chat streaming response", enumerator.Current.Content); await enumerator.MoveNextAsync(); - Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); + Assert.Equal("Stop", enumerator.Current.Metadata?["FinishReason"]); } [Fact] @@ -496,8 +460,8 @@ public async Task GetStreamingChatMessageContentsWithFunctionCallAsync() var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_multiple_function_calls_test_response.txt")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) }; + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_multiple_function_calls_test_response.txt") }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_test_response.txt") }; this._messageHandlerStub.ResponsesToReturn = [response1, response2]; @@ -506,10 +470,10 @@ public async Task GetStreamingChatMessageContentsWithFunctionCallAsync() await enumerator.MoveNextAsync(); Assert.Equal("Test chat streaming response", enumerator.Current.Content); - Assert.Equal("tool_calls", enumerator.Current.Metadata?["FinishReason"]); + Assert.Equal("ToolCalls", enumerator.Current.Metadata?["FinishReason"]); await enumerator.MoveNextAsync(); - Assert.Equal("tool_calls", enumerator.Current.Metadata?["FinishReason"]); + Assert.Equal("ToolCalls", enumerator.Current.Metadata?["FinishReason"]); // Keep looping until the end of stream while (await enumerator.MoveNextAsync()) @@ -544,7 +508,7 @@ public async Task GetStreamingChatMessageContentsWithFunctionCallMaximumAutoInvo for (var i = 0; i < ModelResponsesCount; i++) { - responses.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_single_function_call_test_response.txt")) }); + responses.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_single_function_call_test_response.txt") }); } this._messageHandlerStub.ResponsesToReturn = responses; @@ -579,8 +543,8 @@ public async Task GetStreamingChatMessageContentsWithRequiredFunctionCallAsync() var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_single_function_call_test_response.txt")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) }; + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_single_function_call_test_response.txt") }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_test_response.txt") }; this._messageHandlerStub.ResponsesToReturn = [response1, response2]; @@ -590,7 +554,7 @@ public async Task GetStreamingChatMessageContentsWithRequiredFunctionCallAsync() // Function Tool Call Streaming (One Chunk) await enumerator.MoveNextAsync(); Assert.Equal("Test chat streaming response", enumerator.Current.Content); - Assert.Equal("tool_calls", enumerator.Current.Metadata?["FinishReason"]); + Assert.Equal("ToolCalls", enumerator.Current.Metadata?["FinishReason"]); // Chat Completion Streaming (1st Chunk) await enumerator.MoveNextAsync(); @@ -598,7 +562,7 @@ public async Task GetStreamingChatMessageContentsWithRequiredFunctionCallAsync() // Chat Completion Streaming (2nd Chunk) await enumerator.MoveNextAsync(); - Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); + Assert.Equal("Stop", enumerator.Current.Metadata?["FinishReason"]); Assert.Equal(1, functionCallCount); @@ -949,10 +913,7 @@ public void Dispose() public static TheoryData ResponseFormats => new() { - { new FakeChatCompletionsResponseFormat(), null }, { "json_object", "json_object" }, { "text", "text" } }; - - private sealed class FakeChatCompletionsResponseFormat : ChatCompletionsResponseFormat; } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs index 304e62bc9aeb..76e0b2064439 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs @@ -3,9 +3,9 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using OpenAI.Chat; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; @@ -18,10 +18,10 @@ public sealed class AzureOpenAIChatMessageContentTests public void ConstructorsWorkCorrectly() { // Arrange - List toolCalls = [new FakeChatCompletionsToolCall("id")]; + List toolCalls = [ChatToolCall.CreateFunctionToolCall("id", "name", "args")]; // Act - var content1 = new AzureOpenAIChatMessageContent(new ChatRole("user"), "content1", "model-id1", toolCalls) { AuthorName = "Fred" }; + var content1 = new AzureOpenAIChatMessageContent(ChatMessageRole.User, "content1", "model-id1", toolCalls) { AuthorName = "Fred" }; var content2 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls); // Assert @@ -33,11 +33,9 @@ public void ConstructorsWorkCorrectly() public void GetOpenAIFunctionToolCallsReturnsCorrectList() { // Arrange - List toolCalls = [ - new ChatCompletionsFunctionToolCall("id1", "name", string.Empty), - new ChatCompletionsFunctionToolCall("id2", "name", string.Empty), - new FakeChatCompletionsToolCall("id3"), - new FakeChatCompletionsToolCall("id4")]; + List toolCalls = [ + ChatToolCall.CreateFunctionToolCall("id1", "name", string.Empty), + ChatToolCall.CreateFunctionToolCall("id2", "name", string.Empty)]; var content1 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content", "model-id", toolCalls); var content2 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content", "model-id", []); @@ -64,11 +62,9 @@ public void MetadataIsInitializedCorrectly(bool readOnlyMetadata) new CustomReadOnlyDictionary(new Dictionary { { "key", "value" } }) : new Dictionary { { "key", "value" } }; - List toolCalls = [ - new ChatCompletionsFunctionToolCall("id1", "name", string.Empty), - new ChatCompletionsFunctionToolCall("id2", "name", string.Empty), - new FakeChatCompletionsToolCall("id3"), - new FakeChatCompletionsToolCall("id4")]; + List toolCalls = [ + ChatToolCall.CreateFunctionToolCall("id1", "name", string.Empty), + ChatToolCall.CreateFunctionToolCall("id2", "name", string.Empty)]; // Act var content1 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content1", "model-id1", [], metadata); @@ -82,9 +78,9 @@ public void MetadataIsInitializedCorrectly(bool readOnlyMetadata) Assert.Equal(2, content2.Metadata.Count); Assert.Equal("value", content2.Metadata["key"]); - Assert.IsType>(content2.Metadata["ChatResponseMessage.FunctionToolCalls"]); + Assert.IsType>(content2.Metadata["ChatResponseMessage.FunctionToolCalls"]); - var actualToolCalls = content2.Metadata["ChatResponseMessage.FunctionToolCalls"] as List; + var actualToolCalls = content2.Metadata["ChatResponseMessage.FunctionToolCalls"] as List; Assert.NotNull(actualToolCalls); Assert.Equal(2, actualToolCalls.Count); @@ -96,7 +92,7 @@ private void AssertChatMessageContent( AuthorRole expectedRole, string expectedContent, string expectedModelId, - IReadOnlyList expectedToolCalls, + IReadOnlyList expectedToolCalls, AzureOpenAIChatMessageContent actualContent, string? expectedName = null) { @@ -107,9 +103,6 @@ private void AssertChatMessageContent( Assert.Same(expectedToolCalls, actualContent.ToolCalls); } - private sealed class FakeChatCompletionsToolCall(string id) : ChatCompletionsToolCall(id) - { } - private sealed class CustomReadOnlyDictionary(IDictionary dictionary) : IReadOnlyDictionary // explicitly not implementing IDictionary<> { public TValue this[TKey key] => dictionary[key]; diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs index 8f16c6ea7db2..766376ee00b9 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Text; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using OpenAI.Chat; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; @@ -18,7 +18,7 @@ public sealed class AzureOpenAIFunctionToolCallTests public void FullyQualifiedNameReturnsValidName(string toolCallName, string expectedName) { // Arrange - var toolCall = new ChatCompletionsFunctionToolCall("id", toolCallName, string.Empty); + var toolCall = ChatToolCall.CreateFunctionToolCall("id", toolCallName, string.Empty); var openAIFunctionToolCall = new AzureOpenAIFunctionToolCall(toolCall); // Act & Assert @@ -30,7 +30,7 @@ public void FullyQualifiedNameReturnsValidName(string toolCallName, string expec public void ToStringReturnsCorrectValue() { // Arrange - var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin_MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n}"); + var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin_MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n}"); var openAIFunctionToolCall = new AzureOpenAIFunctionToolCall(toolCall); // Act & Assert @@ -75,7 +75,7 @@ public void ConvertToolCallUpdatesWithNotEmptyIndexesReturnsNotEmptyToolCalls() var toolCall = toolCalls[0]; Assert.Equal("test-id", toolCall.Id); - Assert.Equal("test-function", toolCall.Name); - Assert.Equal("test-argument", toolCall.Arguments); + Assert.Equal("test-function", toolCall.FunctionName); + Assert.Equal("test-argument", toolCall.FunctionArguments); } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs index bbfb636196d3..e0642abc52e1 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using OpenAI.Chat; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; @@ -18,7 +18,7 @@ public void TryGetFunctionAndArgumentsWithNonExistingFunctionReturnsFalse() var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin"); var plugins = new KernelPluginCollection([plugin]); - var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin_MyFunction", string.Empty); + var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin_MyFunction", string.Empty); // Act var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); @@ -37,7 +37,7 @@ public void TryGetFunctionAndArgumentsWithoutArgumentsReturnsTrue() var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); var plugins = new KernelPluginCollection([plugin]); - var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin-MyFunction", string.Empty); + var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin-MyFunction", string.Empty); // Act var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); @@ -56,7 +56,7 @@ public void TryGetFunctionAndArgumentsWithArgumentsReturnsTrue() var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); var plugins = new KernelPluginCollection([plugin]); - var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin-MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n,\n \"null_argument\": null\n}"); + var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin-MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n,\n \"null_argument\": null\n}"); // Act var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs index 270b055d730c..12a2a739c47a 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs @@ -312,6 +312,7 @@ public async Task FilterCanHandleExceptionAsync() var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("System message"); // Act var result = await chatCompletion.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel); @@ -582,18 +583,18 @@ public void Dispose() private static List GetFunctionCallingResponses() { return [ - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) } + new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("filters_multiple_function_calls_test_response.json") }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("filters_multiple_function_calls_test_response.json") }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_test_response.json") } ]; } private static List GetFunctionCallingStreamingResponses() { return [ - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_streaming_multiple_function_calls_test_response.txt")) }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_streaming_multiple_function_calls_test_response.txt")) }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) } + new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("filters_streaming_multiple_function_calls_test_response.txt") }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("filters_streaming_multiple_function_calls_test_response.txt") }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_test_response.txt") } ]; } #pragma warning restore CA2000 diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs index bd268ef67991..cf83f89bc783 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs @@ -4,9 +4,9 @@ using System.ComponentModel; using System.Linq; using System.Text.Json; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using OpenAI.Chat; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.FunctionCalling; @@ -51,11 +51,11 @@ public void ItCanConvertToFunctionDefinitionWithNoPluginName() AzureOpenAIFunction sut = KernelFunctionFactory.CreateFromMethod(() => { }, "myfunc", "This is a description of the function.").Metadata.ToAzureOpenAIFunction(); // Act - FunctionDefinition result = sut.ToFunctionDefinition(); + ChatTool result = sut.ToFunctionDefinition(); // Assert - Assert.Equal(sut.FunctionName, result.Name); - Assert.Equal(sut.Description, result.Description); + Assert.Equal(sut.FunctionName, result.FunctionName); + Assert.Equal(sut.Description, result.FunctionDescription); } [Fact] @@ -68,7 +68,7 @@ public void ItCanConvertToFunctionDefinitionWithNullParameters() var result = sut.ToFunctionDefinition(); // Assert - Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{}}", result.Parameters.ToString()); + Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{}}", result.FunctionParameters.ToString()); } [Fact] @@ -81,11 +81,11 @@ public void ItCanConvertToFunctionDefinitionWithPluginName() }).GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); // Act - FunctionDefinition result = sut.ToFunctionDefinition(); + ChatTool result = sut.ToFunctionDefinition(); // Assert - Assert.Equal("myplugin-myfunc", result.Name); - Assert.Equal(sut.Description, result.Description); + Assert.Equal("myplugin-myfunc", result.FunctionName); + Assert.Equal(sut.Description, result.FunctionDescription); } [Fact] @@ -103,15 +103,15 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParamete AzureOpenAIFunction sut = plugin.GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); - FunctionDefinition functionDefinition = sut.ToFunctionDefinition(); + ChatTool functionDefinition = sut.ToFunctionDefinition(); var exp = JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)); - var act = JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.Parameters)); + var act = JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.FunctionParameters)); Assert.NotNull(functionDefinition); - Assert.Equal("Tests-TestFunction", functionDefinition.Name); - Assert.Equal("My test function", functionDefinition.Description); - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.Parameters))); + Assert.Equal("Tests-TestFunction", functionDefinition.FunctionName); + Assert.Equal("My test function", functionDefinition.FunctionDescription); + Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.FunctionParameters))); } [Fact] @@ -129,12 +129,12 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParame AzureOpenAIFunction sut = plugin.GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); - FunctionDefinition functionDefinition = sut.ToFunctionDefinition(); + ChatTool functionDefinition = sut.ToFunctionDefinition(); Assert.NotNull(functionDefinition); - Assert.Equal("Tests-TestFunction", functionDefinition.Name); - Assert.Equal("My test function", functionDefinition.Description); - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.Parameters))); + Assert.Equal("Tests-TestFunction", functionDefinition.FunctionName); + Assert.Equal("My test function", functionDefinition.FunctionDescription); + Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.FunctionParameters))); } [Fact] @@ -146,8 +146,8 @@ public void ItCanConvertToFunctionDefinitionsWithNoParameterTypes() parameters: [new KernelParameterMetadata("param1")]).Metadata.ToAzureOpenAIFunction(); // Act - FunctionDefinition result = f.ToFunctionDefinition(); - ParametersData pd = JsonSerializer.Deserialize(result.Parameters.ToString())!; + ChatTool result = f.ToFunctionDefinition(); + ParametersData pd = JsonSerializer.Deserialize(result.FunctionParameters.ToString())!; // Assert Assert.NotNull(pd.properties); @@ -166,8 +166,8 @@ public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescript parameters: [new KernelParameterMetadata("param1") { Description = "something neat" }]).Metadata.ToAzureOpenAIFunction(); // Act - FunctionDefinition result = f.ToFunctionDefinition(); - ParametersData pd = JsonSerializer.Deserialize(result.Parameters.ToString())!; + ChatTool result = f.ToFunctionDefinition(); + ParametersData pd = JsonSerializer.Deserialize(result.FunctionParameters.ToString())!; // Assert Assert.NotNull(pd.properties); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs index ebf7b67a2f9b..67cd371dfe23 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs @@ -196,7 +196,7 @@ public void ItCanCreateValidAzureOpenAIFunctionManualForPlugin() Assert.NotNull(result); Assert.Equal( """{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"type":"string","enum":["Value1","Value2"],"description":"Enum parameter"},"parameter3":{"type":"string","format":"date-time","description":"DateTime parameter"}}}""", - result.Parameters.ToString() + result.FunctionParameters.ToString() ); } @@ -231,7 +231,7 @@ public void ItCanCreateValidAzureOpenAIFunctionManualForPrompt() Assert.NotNull(result); Assert.Equal( """{"type":"object","required":["parameter1","parameter2"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"enum":["Value1","Value2"],"description":"Enum parameter"}}}""", - result.Parameters.ToString() + result.FunctionParameters.ToString() ); } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AddHeaderRequestPolicy.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/AddHeaderRequestPolicy.cs deleted file mode 100644 index 8303b2ceaeaf..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/AddHeaderRequestPolicy.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Azure.Core; -using Azure.Core.Pipeline; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Helper class to inject headers into Azure SDK HTTP pipeline -/// -internal sealed class AddHeaderRequestPolicy(string headerName, string headerValue) : HttpPipelineSynchronousPolicy -{ - private readonly string _headerName = headerName; - private readonly string _headerValue = headerValue; - - public override void OnSendingRequest(HttpMessage message) - { - message.Request.Headers.Add(this._headerName, this._headerValue); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs index 69c305f58f34..3bf30f28e07e 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs @@ -6,9 +6,10 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; -using Azure.AI.OpenAI; +using Azure.AI.OpenAI.Chat; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Text; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -116,23 +117,6 @@ public IList? StopSequences } } - /// - /// How many completions to generate for each prompt. Default is 1. - /// Note: Because this parameter generates many completions, it can quickly consume your token quota. - /// Use carefully and ensure that you have reasonable settings for max_tokens and stop. - /// - [JsonPropertyName("results_per_prompt")] - public int ResultsPerPrompt - { - get => this._resultsPerPrompt; - - set - { - this.ThrowIfFrozen(); - this._resultsPerPrompt = value; - } - } - /// /// If specified, the system will make a best effort to sample deterministically such that repeated requests with the /// same seed and parameters should return the same result. Determinism is not guaranteed. @@ -153,7 +137,7 @@ public long? Seed /// Gets or sets the response format to use for the completion. /// /// - /// Possible values are: "json_object", "text", object. + /// Possible values are: "json_object", "text", object. /// [Experimental("SKEXP0010")] [JsonPropertyName("response_format")] @@ -293,14 +277,14 @@ public int? TopLogprobs /// [Experimental("SKEXP0010")] [JsonIgnore] - public AzureChatExtensionsOptions? AzureChatExtensionsOptions + public AzureChatDataSource? AzureChatDataSource { - get => this._azureChatExtensionsOptions; + get => this._azureChatDataSource; set { this.ThrowIfFrozen(); - this._azureChatExtensionsOptions = value; + this._azureChatDataSource = value; } } @@ -338,7 +322,6 @@ public override PromptExecutionSettings Clone() FrequencyPenalty = this.FrequencyPenalty, MaxTokens = this.MaxTokens, StopSequences = this.StopSequences is not null ? new List(this.StopSequences) : null, - ResultsPerPrompt = this.ResultsPerPrompt, Seed = this.Seed, ResponseFormat = this.ResponseFormat, TokenSelectionBiases = this.TokenSelectionBiases is not null ? new Dictionary(this.TokenSelectionBiases) : null, @@ -347,7 +330,7 @@ public override PromptExecutionSettings Clone() ChatSystemPrompt = this.ChatSystemPrompt, Logprobs = this.Logprobs, TopLogprobs = this.TopLogprobs, - AzureChatExtensionsOptions = this.AzureChatExtensionsOptions, + AzureChatDataSource = this.AzureChatDataSource, }; } @@ -417,7 +400,6 @@ public static AzureOpenAIPromptExecutionSettings FromExecutionSettingsWithData(P private double _frequencyPenalty; private int? _maxTokens; private IList? _stopSequences; - private int _resultsPerPrompt = 1; private long? _seed; private object? _responseFormat; private IDictionary? _tokenSelectionBiases; @@ -426,7 +408,7 @@ public static AzureOpenAIPromptExecutionSettings FromExecutionSettingsWithData(P private string? _chatSystemPrompt; private bool? _logprobs; private int? _topLogprobs; - private AzureChatExtensionsOptions? _azureChatExtensionsOptions; + private AzureChatDataSource? _azureChatDataSource; #endregion } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs index 4c3baef49268..7983b085d5a1 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs @@ -6,7 +6,7 @@ using System.Diagnostics; using System.Linq; using System.Text.Json; -using Azure.AI.OpenAI; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -118,10 +118,9 @@ private AzureToolCallBehavior(bool autoInvoke) /// true if it's ok to invoke any kernel function requested by the model if it's found; false if a request needs to be validated against an allow list. internal virtual bool AllowAnyRequestedKernelFunction => false; - /// Configures the with any tools this provides. - /// The used for the operation. This can be queried to determine what tools to provide into the . - /// The destination to configure. - internal abstract void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options); + /// Returns list of available tools and the way model should use them. + /// The used for the operation. This can be queried to determine what tools to return. + internal abstract (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel); /// /// Represents a that will provide to the model all available functions from a @@ -133,8 +132,11 @@ internal KernelFunctions(bool autoInvoke) : base(autoInvoke) { } public override string ToString() => $"{nameof(KernelFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0})"; - internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) + internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel) { + ChatToolChoice? choice = null; + List? tools = null; + // If no kernel is provided, we don't have any tools to provide. if (kernel is not null) { @@ -142,13 +144,15 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o IList functions = kernel.Plugins.GetFunctionsMetadata(); if (functions.Count > 0) { - options.ToolChoice = ChatCompletionsToolChoice.Auto; + choice = ChatToolChoice.Auto; for (int i = 0; i < functions.Count; i++) { - options.Tools.Add(new ChatCompletionsFunctionToolDefinition(functions[i].ToAzureOpenAIFunction().ToFunctionDefinition())); + (tools ??= []).Add(functions[i].ToAzureOpenAIFunction().ToFunctionDefinition()); } } } + + return (tools, choice); } internal override bool AllowAnyRequestedKernelFunction => true; @@ -160,26 +164,29 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o internal sealed class EnabledFunctions : AzureToolCallBehavior { private readonly AzureOpenAIFunction[] _openAIFunctions; - private readonly ChatCompletionsFunctionToolDefinition[] _functions; + private readonly ChatTool[] _functions; public EnabledFunctions(IEnumerable functions, bool autoInvoke) : base(autoInvoke) { this._openAIFunctions = functions.ToArray(); - var defs = new ChatCompletionsFunctionToolDefinition[this._openAIFunctions.Length]; + var defs = new ChatTool[this._openAIFunctions.Length]; for (int i = 0; i < defs.Length; i++) { - defs[i] = new ChatCompletionsFunctionToolDefinition(this._openAIFunctions[i].ToFunctionDefinition()); + defs[i] = this._openAIFunctions[i].ToFunctionDefinition(); } this._functions = defs; } - public override string ToString() => $"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {string.Join(", ", this._functions.Select(f => f.Name))}"; + public override string ToString() => $"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {string.Join(", ", this._functions.Select(f => f.FunctionName))}"; - internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) + internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel) { + ChatToolChoice? choice = null; + List? tools = null; + AzureOpenAIFunction[] openAIFunctions = this._openAIFunctions; - ChatCompletionsFunctionToolDefinition[] functions = this._functions; + ChatTool[] functions = this._functions; Debug.Assert(openAIFunctions.Length == functions.Length); if (openAIFunctions.Length > 0) @@ -196,7 +203,7 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o throw new KernelException($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided."); } - options.ToolChoice = ChatCompletionsToolChoice.Auto; + choice = ChatToolChoice.Auto; for (int i = 0; i < openAIFunctions.Length; i++) { // Make sure that if auto-invocation is specified, every enabled function can be found in the kernel. @@ -211,9 +218,11 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o } // Add the function. - options.Tools.Add(functions[i]); + (tools ??= []).Add(functions[i]); } } + + return (tools, choice); } } @@ -221,19 +230,19 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o internal sealed class RequiredFunction : AzureToolCallBehavior { private readonly AzureOpenAIFunction _function; - private readonly ChatCompletionsFunctionToolDefinition _tool; - private readonly ChatCompletionsToolChoice _choice; + private readonly ChatTool _tool; + private readonly ChatToolChoice _choice; public RequiredFunction(AzureOpenAIFunction function, bool autoInvoke) : base(autoInvoke) { this._function = function; - this._tool = new ChatCompletionsFunctionToolDefinition(function.ToFunctionDefinition()); - this._choice = new ChatCompletionsToolChoice(this._tool); + this._tool = function.ToFunctionDefinition(); + this._choice = new ChatToolChoice(this._tool); } - public override string ToString() => $"{nameof(RequiredFunction)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {this._tool.Name}"; + public override string ToString() => $"{nameof(RequiredFunction)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {this._tool.FunctionName}"; - internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) + internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel) { bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; @@ -253,8 +262,7 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o throw new KernelException($"The specified {nameof(RequiredFunction)} function {this._function.FullyQualifiedName} is not available in the kernel."); } - options.ToolChoice = this._choice; - options.Tools.Add(this._tool); + return ([this._tool], this._choice); } /// Gets how many requests are part of a single interaction should include this tool in the request. diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs index e478a301d947..9d771c4f7abb 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs @@ -10,6 +10,7 @@ using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.TextGeneration; +using OpenAI; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -73,7 +74,7 @@ public AzureOpenAIChatCompletionService( /// The to use for logging. If null, no logging will be performed. public AzureOpenAIChatCompletionService( string deploymentName, - OpenAIClient openAIClient, + AzureOpenAIClient openAIClient, string? modelId = null, ILoggerFactory? loggerFactory = null) { diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 8e8f53594708..35c31788610d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -13,6 +13,7 @@ + @@ -25,7 +26,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs index 8cbecc909951..b950b2408332 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Linq; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -13,28 +13,28 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; public sealed class AzureOpenAIChatMessageContent : ChatMessageContent { /// - /// Gets the metadata key for the name property. + /// Gets the metadata key for the tool id. /// - public static string ToolIdProperty => $"{nameof(ChatCompletionsToolCall)}.{nameof(ChatCompletionsToolCall.Id)}"; + public static string ToolIdProperty => "ChatCompletionsToolCall.Id"; /// - /// Gets the metadata key for the list of . + /// Gets the metadata key for the list of . /// - internal static string FunctionToolCallsProperty => $"{nameof(ChatResponseMessage)}.FunctionToolCalls"; + internal static string FunctionToolCallsProperty => "ChatResponseMessage.FunctionToolCalls"; /// /// Initializes a new instance of the class. /// - internal AzureOpenAIChatMessageContent(ChatResponseMessage chatMessage, string modelId, IReadOnlyDictionary? metadata = null) - : base(new AuthorRole(chatMessage.Role.ToString()), chatMessage.Content, modelId, chatMessage, System.Text.Encoding.UTF8, CreateMetadataDictionary(chatMessage.ToolCalls, metadata)) + internal AzureOpenAIChatMessageContent(OpenAI.Chat.ChatCompletion completion, string modelId, IReadOnlyDictionary? metadata = null) + : base(new AuthorRole(completion.Role.ToString()), CreateContentItems(completion.Content), modelId, completion, System.Text.Encoding.UTF8, CreateMetadataDictionary(completion.ToolCalls, metadata)) { - this.ToolCalls = chatMessage.ToolCalls; + this.ToolCalls = completion.ToolCalls; } /// /// Initializes a new instance of the class. /// - internal AzureOpenAIChatMessageContent(ChatRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) + internal AzureOpenAIChatMessageContent(ChatMessageRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) : base(new AuthorRole(role.ToString()), content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) { this.ToolCalls = toolCalls; @@ -43,16 +43,32 @@ internal AzureOpenAIChatMessageContent(ChatRole role, string? content, string mo /// /// Initializes a new instance of the class. /// - internal AzureOpenAIChatMessageContent(AuthorRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) + internal AzureOpenAIChatMessageContent(AuthorRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) : base(role, content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) { this.ToolCalls = toolCalls; } + private static ChatMessageContentItemCollection CreateContentItems(IReadOnlyList contentUpdate) + { + ChatMessageContentItemCollection collection = []; + + foreach (var part in contentUpdate) + { + // We only support text content for now. + if (part.Kind == ChatMessageContentPartKind.Text) + { + collection.Add(new TextContent(part.Text)); + } + } + + return collection; + } + /// /// A list of the tools called by the model. /// - public IReadOnlyList ToolCalls { get; } + public IReadOnlyList ToolCalls { get; } /// /// Retrieve the resulting function from the chat result. @@ -64,7 +80,7 @@ public IReadOnlyList GetOpenAIFunctionToolCalls() foreach (var toolCall in this.ToolCalls) { - if (toolCall is ChatCompletionsFunctionToolCall functionToolCall) + if (toolCall is ChatToolCall functionToolCall) { (functionToolCallList ??= []).Add(new AzureOpenAIFunctionToolCall(functionToolCall)); } @@ -79,7 +95,7 @@ public IReadOnlyList GetOpenAIFunctionToolCalls() } private static IReadOnlyDictionary? CreateMetadataDictionary( - IReadOnlyList toolCalls, + IReadOnlyList toolCalls, IReadOnlyDictionary? original) { // We only need to augment the metadata if there are any tool calls. @@ -107,7 +123,7 @@ public IReadOnlyList GetOpenAIFunctionToolCalls() } // Add the additional entry. - newDictionary.Add(FunctionToolCallsProperty, toolCalls.OfType().ToList()); + newDictionary.Add(FunctionToolCallsProperty, toolCalls.Where(ctc => ctc.Kind == ChatToolCallKind.Function).ToList()); return newDictionary; } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs index e34b191a83b8..ca9311c4a285 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.ClientModel; using System.Net.Http; -using Azure; using Azure.AI.OpenAI; using Azure.Core; using Microsoft.Extensions.Logging; @@ -23,7 +23,7 @@ internal sealed class AzureOpenAIClientCore : ClientCore /// /// OpenAI / Azure OpenAI Client /// - internal override OpenAIClient Client { get; } + internal override AzureOpenAIClient Client { get; } /// /// Initializes a new instance of the class using API Key authentication. @@ -49,7 +49,7 @@ internal AzureOpenAIClientCore( this.DeploymentOrModelName = deploymentName; this.Endpoint = new Uri(endpoint); - this.Client = new OpenAIClient(this.Endpoint, new AzureKeyCredential(apiKey), options); + this.Client = new AzureOpenAIClient(this.Endpoint, new ApiKeyCredential(apiKey), options); } /// @@ -75,7 +75,7 @@ internal AzureOpenAIClientCore( this.DeploymentOrModelName = deploymentName; this.Endpoint = new Uri(endpoint); - this.Client = new OpenAIClient(this.Endpoint, credential, options); + this.Client = new AzureOpenAIClient(this.Endpoint, credential, options); } /// @@ -84,11 +84,11 @@ internal AzureOpenAIClientCore( /// it's up to the caller to configure the client. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom . + /// Custom . /// The to use for logging. If null, no logging will be performed. internal AzureOpenAIClientCore( string deploymentName, - OpenAIClient openAIClient, + AzureOpenAIClient openAIClient, ILogger? logger = null) : base(logger) { Verify.NotNullOrWhiteSpace(deploymentName); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs index 4a3cff49103d..0089b6c29041 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; -using Azure.AI.OpenAI; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -124,10 +124,10 @@ internal AzureOpenAIFunction( /// /// Converts the representation to the Azure SDK's - /// representation. + /// representation. /// - /// A containing all the function information. - public FunctionDefinition ToFunctionDefinition() + /// A containing all the function information. + public ChatTool ToFunctionDefinition() { BinaryData resultParameters = s_zeroFunctionParametersSchema; @@ -155,12 +155,12 @@ public FunctionDefinition ToFunctionDefinition() }); } - return new FunctionDefinition - { - Name = this.FullyQualifiedName, - Description = this.Description, - Parameters = resultParameters, - }; + return ChatTool.CreateFunctionTool + ( + functionName: this.FullyQualifiedName, + functionDescription: this.Description, + functionParameters: resultParameters + ); } /// Gets a for a typeless parameter with the specified description, defaulting to typeof(string) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs index bea73a474d37..e618f27a9b15 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Text; using System.Text.Json; -using Azure.AI.OpenAI; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -16,15 +16,15 @@ public sealed class AzureOpenAIFunctionToolCall { private string? _fullyQualifiedFunctionName; - /// Initialize the from a . - internal AzureOpenAIFunctionToolCall(ChatCompletionsFunctionToolCall functionToolCall) + /// Initialize the from a . + internal AzureOpenAIFunctionToolCall(ChatToolCall functionToolCall) { Verify.NotNull(functionToolCall); - Verify.NotNull(functionToolCall.Name); + Verify.NotNull(functionToolCall.FunctionName); - string fullyQualifiedFunctionName = functionToolCall.Name; + string fullyQualifiedFunctionName = functionToolCall.FunctionName; string functionName = fullyQualifiedFunctionName; - string? arguments = functionToolCall.Arguments; + string? arguments = functionToolCall.FunctionArguments; string? pluginName = null; int separatorPos = fullyQualifiedFunctionName.IndexOf(AzureOpenAIFunction.NameSeparator, StringComparison.Ordinal); @@ -89,43 +89,43 @@ public override string ToString() /// /// Tracks tooling updates from streaming responses. /// - /// The tool call update to incorporate. + /// The tool call updates to incorporate. /// Lazily-initialized dictionary mapping indices to IDs. /// Lazily-initialized dictionary mapping indices to names. /// Lazily-initialized dictionary mapping indices to arguments. internal static void TrackStreamingToolingUpdate( - StreamingToolCallUpdate? update, + IReadOnlyList? updates, ref Dictionary? toolCallIdsByIndex, ref Dictionary? functionNamesByIndex, ref Dictionary? functionArgumentBuildersByIndex) { - if (update is null) + if (updates is null) { // Nothing to track. return; } - // If we have an ID, ensure the index is being tracked. Even if it's not a function update, - // we want to keep track of it so we can send back an error. - if (update.Id is string id) + foreach (var update in updates) { - (toolCallIdsByIndex ??= [])[update.ToolCallIndex] = id; - } + // If we have an ID, ensure the index is being tracked. Even if it's not a function update, + // we want to keep track of it so we can send back an error. + if (update.Id is string id) + { + (toolCallIdsByIndex ??= [])[update.Index] = id; + } - if (update is StreamingFunctionToolCallUpdate ftc) - { // Ensure we're tracking the function's name. - if (ftc.Name is string name) + if (update.FunctionName is string name) { - (functionNamesByIndex ??= [])[ftc.ToolCallIndex] = name; + (functionNamesByIndex ??= [])[update.Index] = name; } // Ensure we're tracking the function's arguments. - if (ftc.ArgumentsUpdate is string argumentsUpdate) + if (update.FunctionArgumentsUpdate is string argumentsUpdate) { - if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(ftc.ToolCallIndex, out StringBuilder? arguments)) + if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(update.Index, out StringBuilder? arguments)) { - functionArgumentBuildersByIndex[ftc.ToolCallIndex] = arguments = new(); + functionArgumentBuildersByIndex[update.Index] = arguments = new(); } arguments.Append(argumentsUpdate); @@ -134,20 +134,20 @@ internal static void TrackStreamingToolingUpdate( } /// - /// Converts the data built up by into an array of s. + /// Converts the data built up by into an array of s. /// /// Dictionary mapping indices to IDs. /// Dictionary mapping indices to names. /// Dictionary mapping indices to arguments. - internal static ChatCompletionsFunctionToolCall[] ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + internal static ChatToolCall[] ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( ref Dictionary? toolCallIdsByIndex, ref Dictionary? functionNamesByIndex, ref Dictionary? functionArgumentBuildersByIndex) { - ChatCompletionsFunctionToolCall[] toolCalls = []; + ChatToolCall[] toolCalls = []; if (toolCallIdsByIndex is { Count: > 0 }) { - toolCalls = new ChatCompletionsFunctionToolCall[toolCallIdsByIndex.Count]; + toolCalls = new ChatToolCall[toolCallIdsByIndex.Count]; int i = 0; foreach (KeyValuePair toolCallIndexAndId in toolCallIdsByIndex) @@ -158,7 +158,7 @@ internal static ChatCompletionsFunctionToolCall[] ConvertToolCallUpdatesToChatCo functionNamesByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionName); functionArgumentBuildersByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionArguments); - toolCalls[i] = new ChatCompletionsFunctionToolCall(toolCallIndexAndId.Value, functionName ?? string.Empty, functionArguments?.ToString() ?? string.Empty); + toolCalls[i] = ChatToolCall.CreateFunctionToolCall(toolCallIndexAndId.Value, functionName ?? string.Empty, functionArguments?.ToString() ?? string.Empty); i++; } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs index c667183f773c..c903127089dd 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; -using Azure.AI.OpenAI; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -20,7 +20,7 @@ public static class AzureOpenAIPluginCollectionExtensions /// if the function was found; otherwise, . public static bool TryGetFunctionAndArguments( this IReadOnlyKernelPluginCollection plugins, - ChatCompletionsFunctionToolCall functionToolCall, + ChatToolCall functionToolCall, [NotNullWhen(true)] out KernelFunction? function, out KernelArguments? arguments) => plugins.TryGetFunctionAndArguments(new AzureOpenAIFunctionToolCall(functionToolCall), out function, out arguments); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs index c1843b185f89..9287499e1621 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Text; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -18,7 +18,7 @@ public sealed class AzureOpenAIStreamingChatMessageContent : StreamingChatMessag /// /// The reason why the completion finished. /// - public CompletionsFinishReason? FinishReason { get; set; } + public ChatFinishReason? FinishReason { get; set; } /// /// Create a new instance of the class. @@ -28,21 +28,22 @@ public sealed class AzureOpenAIStreamingChatMessageContent : StreamingChatMessag /// The model ID used to generate the content /// Additional metadata internal AzureOpenAIStreamingChatMessageContent( - StreamingChatCompletionsUpdate chatUpdate, + StreamingChatCompletionUpdate chatUpdate, int choiceIndex, string modelId, IReadOnlyDictionary? metadata = null) : base( chatUpdate.Role.HasValue ? new AuthorRole(chatUpdate.Role.Value.ToString()) : null, - chatUpdate.ContentUpdate, + null, chatUpdate, choiceIndex, modelId, Encoding.UTF8, metadata) { - this.ToolCallUpdate = chatUpdate.ToolCallUpdate; - this.FinishReason = chatUpdate?.FinishReason; + this.ToolCallUpdate = chatUpdate.ToolCallUpdates; + this.FinishReason = chatUpdate.FinishReason; + this.Items = CreateContentItems(chatUpdate.ContentUpdate); } /// @@ -58,8 +59,8 @@ internal AzureOpenAIStreamingChatMessageContent( internal AzureOpenAIStreamingChatMessageContent( AuthorRole? authorRole, string? content, - StreamingToolCallUpdate? tootToolCallUpdate = null, - CompletionsFinishReason? completionsFinishReason = null, + IReadOnlyList? tootToolCallUpdate = null, + ChatFinishReason? completionsFinishReason = null, int choiceIndex = 0, string? modelId = null, IReadOnlyDictionary? metadata = null) @@ -77,11 +78,27 @@ internal AzureOpenAIStreamingChatMessageContent( } /// Gets any update information in the message about a tool call. - public StreamingToolCallUpdate? ToolCallUpdate { get; } + public IReadOnlyList? ToolCallUpdate { get; } /// public override byte[] ToByteArray() => this.Encoding.GetBytes(this.ToString()); /// public override string ToString() => this.Content ?? string.Empty; + + private static StreamingKernelContentItemCollection CreateContentItems(IReadOnlyList contentUpdate) + { + StreamingKernelContentItemCollection collection = []; + + foreach (var content in contentUpdate) + { + // We only support text content for now. + if (content.Kind == ChatMessageContentPartKind.Text) + { + collection.Add(new StreamingTextContent(content.Text)); + } + } + + return collection; + } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs index dda7578da8ea..7bfbb361c36a 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.ClientModel; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Metrics; @@ -13,13 +15,15 @@ using System.Threading.Tasks; using Azure; using Azure.AI.OpenAI; -using Azure.Core; -using Azure.Core.Pipeline; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Http; +using OpenAI; +using OpenAI.Audio; +using OpenAI.Chat; +using OpenAI.Embeddings; #pragma warning disable CA2208 // Instantiate argument exceptions correctly @@ -31,7 +35,7 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; internal abstract class ClientCore { private const string ModelProvider = "openai"; - private const int MaxResultsPerPrompt = 128; + private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); /// /// The maximum number of auto-invokes that can be in-flight at any given time as part of the current @@ -52,7 +56,7 @@ internal abstract class ClientCore private const int MaxInflightAutoInvokes = 128; /// Singleton tool used when tool call count drops to 0 but we need to supply tools to keep the service happy. - private static readonly ChatCompletionsFunctionToolDefinition s_nonInvocableFunctionTool = new() { Name = "NonInvocableTool" }; + private static readonly ChatTool s_nonInvocableFunctionTool = ChatTool.CreateFunctionTool("NonInvocableTool"); /// Tracking for . private static readonly AsyncLocal s_inflightAutoInvokes = new(); @@ -70,7 +74,7 @@ internal ClientCore(ILogger? logger = null) /// /// OpenAI / Azure OpenAI Client /// - internal abstract OpenAIClient Client { get; } + internal abstract AzureOpenAIClient Client { get; } internal Uri? Endpoint { get; set; } = null; @@ -116,171 +120,169 @@ internal ClientCore(ILogger? logger = null) unit: "{token}", description: "Number of tokens used"); - /// - /// Creates completions for the prompt and settings. - /// - /// The prompt to complete. - /// Execution settings for the completion API. - /// The containing services, plugins, and other state for use throughout the operation. - /// The to monitor for cancellation requests. The default is . - /// Completions generated by the remote model - internal async Task> GetTextResultsAsync( - string prompt, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - CancellationToken cancellationToken = default) - { - AzureOpenAIPromptExecutionSettings textExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings, AzureOpenAIPromptExecutionSettings.DefaultTextMaxTokens); - - ValidateMaxTokens(textExecutionSettings.MaxTokens); - - var options = CreateCompletionsOptions(prompt, textExecutionSettings, this.DeploymentOrModelName); + ///// + ///// Creates completions for the prompt and settings. + ///// + ///// The prompt to complete. + ///// Execution settings for the completion API. + ///// The containing services, plugins, and other state for use throughout the operation. + ///// The to monitor for cancellation requests. The default is . + ///// Completions generated by the remote model + //internal async Task> GetTextResultsAsync( + // string prompt, + // PromptExecutionSettings? executionSettings, + // Kernel? kernel, + // CancellationToken cancellationToken = default) + //{ + // AzureOpenAIPromptExecutionSettings textExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings, AzureOpenAIPromptExecutionSettings.DefaultTextMaxTokens); - Completions? responseData = null; - List responseContent; - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, prompt, textExecutionSettings)) - { - try - { - responseData = (await RunRequestAsync(() => this.Client.GetCompletionsAsync(options, cancellationToken)).ConfigureAwait(false)).Value; - if (responseData.Choices.Count == 0) - { - throw new KernelException("Text completions not found"); - } - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - if (responseData != null) - { - // Capture available metadata even if the operation failed. - activity - .SetResponseId(responseData.Id) - .SetPromptTokenUsage(responseData.Usage.PromptTokens) - .SetCompletionTokenUsage(responseData.Usage.CompletionTokens); - } - throw; - } + // ValidateMaxTokens(textExecutionSettings.MaxTokens); - responseContent = responseData.Choices.Select(choice => new TextContent(choice.Text, this.DeploymentOrModelName, choice, Encoding.UTF8, GetTextChoiceMetadata(responseData, choice))).ToList(); - activity?.SetCompletionResponse(responseContent, responseData.Usage.PromptTokens, responseData.Usage.CompletionTokens); - } + // var options = CreateCompletionsOptions(prompt, textExecutionSettings, this.DeploymentOrModelName); - this.LogUsage(responseData.Usage); + // Completions? responseData = null; + // List responseContent; + // using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, prompt, textExecutionSettings)) + // { + // try + // { + // responseData = (await RunRequestAsync(() => this.Client.GetCompletionsAsync(options, cancellationToken)).ConfigureAwait(false)).Value; + // if (responseData.Choices.Count == 0) + // { + // throw new KernelException("Text completions not found"); + // } + // } + // catch (Exception ex) when (activity is not null) + // { + // activity.SetError(ex); + // if (responseData != null) + // { + // // Capture available metadata even if the operation failed. + // activity + // .SetResponseId(responseData.Id) + // .SetPromptTokenUsage(responseData.Usage.PromptTokens) + // .SetCompletionTokenUsage(responseData.Usage.CompletionTokens); + // } + // throw; + // } + + // responseContent = responseData.Choices.Select(choice => new TextContent(choice.Text, this.DeploymentOrModelName, choice, Encoding.UTF8, GetTextChoiceMetadata(responseData, choice))).ToList(); + // activity?.SetCompletionResponse(responseContent, responseData.Usage.PromptTokens, responseData.Usage.CompletionTokens); + // } - return responseContent; - } + // this.LogUsage(responseData.Usage); - internal async IAsyncEnumerable GetStreamingTextContentsAsync( - string prompt, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - AzureOpenAIPromptExecutionSettings textExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings, AzureOpenAIPromptExecutionSettings.DefaultTextMaxTokens); + // return responseContent; + //} - ValidateMaxTokens(textExecutionSettings.MaxTokens); + //internal async IAsyncEnumerable GetStreamingTextContentsAsync( + // string prompt, + // PromptExecutionSettings? executionSettings, + // Kernel? kernel, + // [EnumeratorCancellation] CancellationToken cancellationToken = default) + //{ + // AzureOpenAIPromptExecutionSettings textExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings, AzureOpenAIPromptExecutionSettings.DefaultTextMaxTokens); - var options = CreateCompletionsOptions(prompt, textExecutionSettings, this.DeploymentOrModelName); + // ValidateMaxTokens(textExecutionSettings.MaxTokens); - using var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, prompt, textExecutionSettings); + // var options = CreateCompletionsOptions(prompt, textExecutionSettings, this.DeploymentOrModelName); - StreamingResponse response; - try - { - response = await RunRequestAsync(() => this.Client.GetCompletionsStreamingAsync(options, cancellationToken)).ConfigureAwait(false); - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } + // using var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, prompt, textExecutionSettings); - var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); - List? streamedContents = activity is not null ? [] : null; - try - { - while (true) - { - try - { - if (!await responseEnumerator.MoveNextAsync()) - { - break; - } - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } + // StreamingResponse response; + // try + // { + // response = await RunRequestAsync(() => this.Client.GetCompletionsStreamingAsync(options, cancellationToken)).ConfigureAwait(false); + // } + // catch (Exception ex) when (activity is not null) + // { + // activity.SetError(ex); + // throw; + // } - Completions completions = responseEnumerator.Current; - foreach (Choice choice in completions.Choices) - { - var openAIStreamingTextContent = new AzureOpenAIStreamingTextContent( - choice.Text, choice.Index, this.DeploymentOrModelName, choice, GetTextChoiceMetadata(completions, choice)); - streamedContents?.Add(openAIStreamingTextContent); - yield return openAIStreamingTextContent; - } - } - } - finally - { - activity?.EndStreaming(streamedContents); - await responseEnumerator.DisposeAsync(); - } - } + // var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); + // List? streamedContents = activity is not null ? [] : null; + // try + // { + // while (true) + // { + // try + // { + // if (!await responseEnumerator.MoveNextAsync()) + // { + // break; + // } + // } + // catch (Exception ex) when (activity is not null) + // { + // activity.SetError(ex); + // throw; + // } + + // Completions completions = responseEnumerator.Current; + // foreach (Choice choice in completions.Choices) + // { + // var openAIStreamingTextContent = new AzureOpenAIStreamingTextContent( + // choice.Text, choice.Index, this.DeploymentOrModelName, choice, GetTextChoiceMetadata(completions, choice)); + // streamedContents?.Add(openAIStreamingTextContent); + // yield return openAIStreamingTextContent; + // } + // } + // } + // finally + // { + // activity?.EndStreaming(streamedContents); + // await responseEnumerator.DisposeAsync(); + // } + //} - private static Dictionary GetTextChoiceMetadata(Completions completions, Choice choice) - { - return new Dictionary(8) - { - { nameof(completions.Id), completions.Id }, - { nameof(completions.Created), completions.Created }, - { nameof(completions.PromptFilterResults), completions.PromptFilterResults }, - { nameof(completions.Usage), completions.Usage }, - { nameof(choice.ContentFilterResults), choice.ContentFilterResults }, + //private static Dictionary GetTextChoiceMetadata(Completions completions, Choice choice) + //{ + // return new Dictionary(8) + // { + // { nameof(completions.Id), completions.Id }, + // { nameof(completions.Created), completions.Created }, + // { nameof(completions.PromptFilterResults), completions.PromptFilterResults }, + // { nameof(completions.Usage), completions.Usage }, + // { nameof(choice.ContentFilterResults), choice.ContentFilterResults }, - // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(choice.FinishReason), choice.FinishReason?.ToString() }, + // // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. + // { nameof(choice.FinishReason), choice.FinishReason?.ToString() }, - { nameof(choice.LogProbabilityModel), choice.LogProbabilityModel }, - { nameof(choice.Index), choice.Index }, - }; - } + // { nameof(choice.LogProbabilityModel), choice.LogProbabilityModel }, + // { nameof(choice.Index), choice.Index }, + // }; + //} - private static Dictionary GetChatChoiceMetadata(ChatCompletions completions, ChatChoice chatChoice) + private static Dictionary GetChatChoiceMetadata(OpenAI.Chat.ChatCompletion 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(12) { { nameof(completions.Id), completions.Id }, - { nameof(completions.Created), completions.Created }, - { nameof(completions.PromptFilterResults), completions.PromptFilterResults }, + { nameof(completions.CreatedAt), completions.CreatedAt }, + { "PromptFilterResults", completions.GetContentFilterResultForPrompt() }, { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, { nameof(completions.Usage), completions.Usage }, - { nameof(chatChoice.ContentFilterResults), chatChoice.ContentFilterResults }, + { "ContentFilterResults", completions.GetContentFilterResultForResponse() }, // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(chatChoice.FinishReason), chatChoice.FinishReason?.ToString() }, - - { nameof(chatChoice.FinishDetails), chatChoice.FinishDetails }, - { nameof(chatChoice.LogProbabilityInfo), chatChoice.LogProbabilityInfo }, - { nameof(chatChoice.Index), chatChoice.Index }, - { nameof(chatChoice.Enhancements), chatChoice.Enhancements }, + { nameof(completions.FinishReason), completions.FinishReason.ToString() }, + { nameof(completions.ContentTokenLogProbabilities), completions.ContentTokenLogProbabilities }, }; +#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(StreamingChatCompletionsUpdate completions) + private static Dictionary GetResponseMetadata(StreamingChatCompletionUpdate completionUpdate) { return new Dictionary(4) { - { nameof(completions.Id), completions.Id }, - { nameof(completions.Created), completions.Created }, - { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, + { nameof(completionUpdate.Id), completionUpdate.Id }, + { nameof(completionUpdate.CreatedAt), completionUpdate.CreatedAt }, + { nameof(completionUpdate.SystemFingerprint), completionUpdate.SystemFingerprint }, // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(completions.FinishReason), completions.FinishReason?.ToString() }, + { nameof(completionUpdate.FinishReason), completionUpdate.FinishReason?.ToString() }, }; } @@ -312,13 +314,13 @@ internal async Task>> GetEmbeddingsAsync( if (data.Count > 0) { - var embeddingsOptions = new EmbeddingsOptions(this.DeploymentOrModelName, data) + var embeddingsOptions = new EmbeddingGenerationOptions() { Dimensions = dimensions }; - var response = await RunRequestAsync(() => this.Client.GetEmbeddingsAsync(embeddingsOptions, cancellationToken)).ConfigureAwait(false); - var embeddings = response.Value.Data; + var response = await RunRequestAsync(() => this.Client.GetEmbeddingClient(this.DeploymentOrModelName).GenerateEmbeddingsAsync(data, embeddingsOptions, cancellationToken)).ConfigureAwait(false); + var embeddings = response.Value; if (embeddings.Count != data.Count) { @@ -327,7 +329,7 @@ internal async Task>> GetEmbeddingsAsync( for (var i = 0; i < embeddings.Count; i++) { - result.Add(embeddings[i].Embedding); + result.Add(embeddings[i].Vector); } } @@ -382,30 +384,36 @@ internal async Task> GetChatMessageContentsAsy { Verify.NotNull(chat); + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", + JsonSerializer.Serialize(chat), + JsonSerializer.Serialize(executionSettings)); + } + // Convert the incoming execution settings to OpenAI settings. AzureOpenAIPromptExecutionSettings chatExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - bool autoInvoke = kernel is not null && chatExecutionSettings.ToolCallBehavior?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; + ValidateMaxTokens(chatExecutionSettings.MaxTokens); - ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); - // Create the Azure SDK ChatCompletionOptions instance from all available information. - var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); + var chatMessages = CreateChatCompletionMessages(chatExecutionSettings, chat); - for (int requestIndex = 1; ; requestIndex++) + for (int requestIndex = 0; ; requestIndex++) { + var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); + + var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); + // Make the request. - ChatCompletions? responseData = null; - List responseContent; + OpenAI.Chat.ChatCompletion? responseData = null; + AzureOpenAIChatMessageContent responseContent; using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) { try { - responseData = (await RunRequestAsync(() => this.Client.GetChatCompletionsAsync(chatOptions, cancellationToken)).ConfigureAwait(false)).Value; + responseData = (await RunRequestAsync(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatAsync(chatMessages, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; + this.LogUsage(responseData.Usage); - if (responseData.Choices.Count == 0) - { - throw new KernelException("Chat completions not found"); - } } catch (Exception ex) when (activity is not null) { @@ -415,21 +423,20 @@ internal async Task> GetChatMessageContentsAsy // Capture available metadata even if the operation failed. activity .SetResponseId(responseData.Id) - .SetPromptTokenUsage(responseData.Usage.PromptTokens) - .SetCompletionTokenUsage(responseData.Usage.CompletionTokens); + .SetPromptTokenUsage(responseData.Usage.InputTokens) + .SetCompletionTokenUsage(responseData.Usage.OutputTokens); } throw; } - responseContent = responseData.Choices.Select(chatChoice => this.GetChatMessage(chatChoice, responseData)).ToList(); - activity?.SetCompletionResponse(responseContent, responseData.Usage.PromptTokens, responseData.Usage.CompletionTokens); + responseContent = this.GetChatMessage(responseData); + activity?.SetCompletionResponse([responseContent], responseData.Usage.InputTokens, responseData.Usage.OutputTokens); } // If we don't want to attempt to invoke any functions, just return the result. - // Or if we are auto-invoking but we somehow end up with other than 1 choice even though only 1 was requested, similarly bail. - if (!autoInvoke || responseData.Choices.Count != 1) + if (!toolCallingConfig.AutoInvoke) { - return responseContent; + return [responseContent]; } Debug.Assert(kernel is not null); @@ -439,51 +446,49 @@ 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. - ChatChoice resultChoice = responseData.Choices[0]; - AzureOpenAIChatMessageContent result = this.GetChatMessage(resultChoice, responseData); - if (result.ToolCalls.Count == 0) + if (responseData.ToolCalls.Count == 0) { - return [result]; + return [responseContent]; } if (this.Logger.IsEnabled(LogLevel.Debug)) { - this.Logger.LogDebug("Tool requests: {Requests}", result.ToolCalls.Count); + this.Logger.LogDebug("Tool requests: {Requests}", responseData.ToolCalls.Count); } if (this.Logger.IsEnabled(LogLevel.Trace)) { - this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", result.ToolCalls.OfType().Select(ftc => $"{ftc.Name}({ftc.Arguments})"))); + this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", responseData.ToolCalls.OfType().Select(ftc => $"{ftc.FunctionName}({ftc.FunctionArguments})"))); } - // Add the original assistant message to the chatOptions; this is required for the service + // 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. - chatOptions.Messages.Add(GetRequestMessage(resultChoice.Message)); - chat.Add(result); + chatMessages.Add(GetRequestMessage(responseData)); + chat.Add(responseContent); // 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 < result.ToolCalls.Count; toolCallIndex++) + for (int toolCallIndex = 0; toolCallIndex < responseContent.ToolCalls.Count; toolCallIndex++) { - ChatCompletionsToolCall toolCall = result.ToolCalls[toolCallIndex]; + ChatToolCall functionToolCall = responseContent.ToolCalls[toolCallIndex]; // We currently only know about function tool calls. If it's anything else, we'll respond with an error. - if (toolCall is not ChatCompletionsFunctionToolCall functionToolCall) + if (functionToolCall.Kind != ChatToolCallKind.Function) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Tool call was not a function call.", functionToolCall, this.Logger); continue; } // Parse the function call arguments. - AzureOpenAIFunctionToolCall? openAIFunctionToolCall; + AzureOpenAIFunctionToolCall? azureOpenAIFunctionToolCall; try { - openAIFunctionToolCall = new(functionToolCall); + azureOpenAIFunctionToolCall = new(functionToolCall); } catch (JsonException) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Function call arguments were invalid JSON.", functionToolCall, this.Logger); continue; } @@ -491,16 +496,16 @@ internal async Task> GetChatMessageContentsAsy // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && - !IsRequestableTool(chatOptions, openAIFunctionToolCall)) + !IsRequestableTool(chatOptions, azureOpenAIFunctionToolCall)) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); + AddResponseMessage(chatMessages, 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(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) + if (!kernel!.Plugins.TryGetFunctionAndArguments(azureOpenAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); continue; } @@ -509,9 +514,9 @@ internal async Task> GetChatMessageContentsAsy AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) { Arguments = functionArgs, - RequestSequenceIndex = requestIndex - 1, + RequestSequenceIndex = requestIndex, FunctionSequenceIndex = toolCallIndex, - FunctionCount = result.ToolCalls.Count + FunctionCount = responseContent.ToolCalls.Count }; s_inflightAutoInvokes.Value++; @@ -535,7 +540,7 @@ internal async Task> GetChatMessageContentsAsy catch (Exception e) #pragma warning restore CA1031 // Do not catch general exception types { - AddResponseMessage(chatOptions, chat, null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, null, $"Error: Exception while invoking function. {e.Message}", functionToolCall, this.Logger); continue; } finally @@ -549,7 +554,7 @@ internal async Task> GetChatMessageContentsAsy object functionResultValue = functionResult.GetValue() ?? string.Empty; var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - AddResponseMessage(chatOptions, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); + AddResponseMessage(chatMessages, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); // If filter requested termination, returning latest function result. if (invocationContext.Terminate) @@ -562,46 +567,6 @@ internal async Task> GetChatMessageContentsAsy return [chat.Last()]; } } - - // Update tool use information for the next go-around based on having completed another iteration. - Debug.Assert(chatExecutionSettings.ToolCallBehavior is not null); - - // Set the tool choice to none. If we end up wanting to use tools, we'll reset it to the desired value. - chatOptions.ToolChoice = ChatCompletionsToolChoice.None; - chatOptions.Tools.Clear(); - - if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts) - { - // Don't add any tools as we've reached the maximum attempts limit. - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts); - } - } - else - { - // Regenerate the tool list as necessary. The invocation of the function(s) could have augmented - // what functions are available in the kernel. - chatExecutionSettings.ToolCallBehavior.ConfigureOptions(kernel, chatOptions); - } - - // Having already sent tools and with tool call information in history, the service can become unhappy ("[] is too short - 'tools'") - // if we don't send any tools in subsequent requests, even if we say not to use any. - if (chatOptions.ToolChoice == ChatCompletionsToolChoice.None) - { - Debug.Assert(chatOptions.Tools.Count == 0); - chatOptions.Tools.Add(s_nonInvocableFunctionTool); - } - - // Disable auto invocation if we've exceeded the allowed limit. - if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts) - { - autoInvoke = false; - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); - } - } } } @@ -613,22 +578,30 @@ internal async IAsyncEnumerable GetStrea { Verify.NotNull(chat); + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", + JsonSerializer.Serialize(chat), + JsonSerializer.Serialize(executionSettings)); + } + AzureOpenAIPromptExecutionSettings chatExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); ValidateMaxTokens(chatExecutionSettings.MaxTokens); - bool autoInvoke = kernel is not null && chatExecutionSettings.ToolCallBehavior?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; - ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); - - var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); - StringBuilder? contentBuilder = null; Dictionary? toolCallIdsByIndex = null; Dictionary? functionNamesByIndex = null; Dictionary? functionArgumentBuildersByIndex = null; - for (int requestIndex = 1; ; requestIndex++) + var chatMessages = CreateChatCompletionMessages(chatExecutionSettings, chat); + + for (int requestIndex = 0; ; requestIndex++) { + var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); + + var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); + // Reset state contentBuilder?.Clear(); toolCallIdsByIndex?.Clear(); @@ -638,18 +611,18 @@ internal async IAsyncEnumerable GetStrea // Stream the response. IReadOnlyDictionary? metadata = null; string? streamedName = null; - ChatRole? streamedRole = default; - CompletionsFinishReason finishReason = default; - ChatCompletionsFunctionToolCall[]? toolCalls = null; + ChatMessageRole? streamedRole = default; + ChatFinishReason finishReason = default; + ChatToolCall[]? toolCalls = null; FunctionCallContent[]? functionCallContents = null; using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) { // Make the request. - StreamingResponse response; + AsyncResultCollection response; try { - response = await RunRequestAsync(() => this.Client.GetChatCompletionsStreamingAsync(chatOptions, cancellationToken)).ConfigureAwait(false); + response = RunRequest(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatStreamingAsync(chatMessages, chatOptions, cancellationToken)); } catch (Exception ex) when (activity is not null) { @@ -676,32 +649,40 @@ internal async IAsyncEnumerable GetStrea throw; } - StreamingChatCompletionsUpdate update = responseEnumerator.Current; + StreamingChatCompletionUpdate update = responseEnumerator.Current; metadata = GetResponseMetadata(update); streamedRole ??= update.Role; - streamedName ??= update.AuthorName; + //streamedName ??= update.AuthorName; finishReason = update.FinishReason ?? default; // If we're intending to invoke function calls, we need to consume that function call information. - if (autoInvoke) + if (toolCallingConfig.AutoInvoke) { - if (update.ContentUpdate is { Length: > 0 } contentUpdate) + var textUpdate = update.ContentUpdate.FirstOrDefault(u => u.Kind == ChatMessageContentPartKind.Text)?.Text; + if (textUpdate is { Length: > 0 }) { - (contentBuilder ??= new()).Append(contentUpdate); + (contentBuilder ??= new()).Append(textUpdate); } - AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(update.ToolCallUpdate, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(update.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); } - var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(update, update.ChoiceIndex ?? 0, this.DeploymentOrModelName, metadata) { AuthorName = streamedName }; + var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(update, 0, this.DeploymentOrModelName, metadata); - if (update.ToolCallUpdate is StreamingFunctionToolCallUpdate functionCallUpdate) + foreach (var functionCallUpdate in update.ToolCallUpdates) { + if (string.IsNullOrEmpty(functionCallUpdate.Id) && + string.IsNullOrEmpty(functionCallUpdate.FunctionName) && + string.IsNullOrEmpty(functionCallUpdate.FunctionArgumentsUpdate)) + { + continue; + } + openAIStreamingChatMessageContent.Items.Add(new StreamingFunctionCallUpdateContent( callId: functionCallUpdate.Id, - name: functionCallUpdate.Name, - arguments: functionCallUpdate.ArgumentsUpdate, - functionCallIndex: functionCallUpdate.ToolCallIndex)); + name: functionCallUpdate.FunctionName, + arguments: functionCallUpdate.FunctionArgumentsUpdate, + functionCallIndex: functionCallUpdate.Index)); } streamedContents?.Add(openAIStreamingChatMessageContent); @@ -726,7 +707,7 @@ internal async IAsyncEnumerable GetStrea // 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 (!autoInvoke || + if (!toolCallingConfig.AutoInvoke || toolCallIdsByIndex is not { Count: > 0 }) { yield break; @@ -738,27 +719,27 @@ internal async IAsyncEnumerable GetStrea // Log the requests if (this.Logger.IsEnabled(LogLevel.Trace)) { - this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", toolCalls.Select(fcr => $"{fcr.Name}({fcr.Arguments})"))); + this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", toolCalls.Select(fcr => $"{fcr.FunctionName}({fcr.FunctionName})"))); } else if (this.Logger.IsEnabled(LogLevel.Debug)) { this.Logger.LogDebug("Function call requests: {Requests}", toolCalls.Length); } - // Add the original assistant message to the chatOptions; this is required for the service + // Add the original assistant message to the chat messages; this is required for the service // to understand the tool call responses. - chatOptions.Messages.Add(GetRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); + chatMessages.Add(GetRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); chat.Add(this.GetChatMessage(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName)); // Respond to each tooling request. for (int toolCallIndex = 0; toolCallIndex < toolCalls.Length; toolCallIndex++) { - ChatCompletionsFunctionToolCall toolCall = toolCalls[toolCallIndex]; + ChatToolCall toolCall = toolCalls[toolCallIndex]; // We currently only know about function tool calls. If it's anything else, we'll respond with an error. - if (string.IsNullOrEmpty(toolCall.Name)) + if (string.IsNullOrEmpty(toolCall.FunctionName)) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); continue; } @@ -770,7 +751,7 @@ internal async IAsyncEnumerable GetStrea } catch (JsonException) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); continue; } @@ -780,14 +761,14 @@ internal async IAsyncEnumerable GetStrea if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && !IsRequestableTool(chatOptions, openAIFunctionToolCall)) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); + AddResponseMessage(chatMessages, 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(chatOptions, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); continue; } @@ -796,7 +777,7 @@ internal async IAsyncEnumerable GetStrea AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) { Arguments = functionArgs, - RequestSequenceIndex = requestIndex - 1, + RequestSequenceIndex = requestIndex, FunctionSequenceIndex = toolCallIndex, FunctionCount = toolCalls.Length }; @@ -822,7 +803,7 @@ internal async IAsyncEnumerable GetStrea catch (Exception e) #pragma warning restore CA1031 // Do not catch general exception types { - AddResponseMessage(chatOptions, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); continue; } finally @@ -836,7 +817,7 @@ internal async IAsyncEnumerable GetStrea object functionResultValue = functionResult.GetValue() ?? string.Empty; var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - AddResponseMessage(chatOptions, chat, stringResult, errorMessage: null, toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, stringResult, errorMessage: null, toolCall, this.Logger); // If filter requested termination, returning latest function result and breaking request iteration loop. if (invocationContext.Terminate) @@ -852,57 +833,17 @@ internal async IAsyncEnumerable GetStrea yield break; } } - - // Update tool use information for the next go-around based on having completed another iteration. - Debug.Assert(chatExecutionSettings.ToolCallBehavior is not null); - - // Set the tool choice to none. If we end up wanting to use tools, we'll reset it to the desired value. - chatOptions.ToolChoice = ChatCompletionsToolChoice.None; - chatOptions.Tools.Clear(); - - if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts) - { - // Don't add any tools as we've reached the maximum attempts limit. - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts); - } - } - else - { - // Regenerate the tool list as necessary. The invocation of the function(s) could have augmented - // what functions are available in the kernel. - chatExecutionSettings.ToolCallBehavior.ConfigureOptions(kernel, chatOptions); - } - - // Having already sent tools and with tool call information in history, the service can become unhappy ("[] is too short - 'tools'") - // if we don't send any tools in subsequent requests, even if we say not to use any. - if (chatOptions.ToolChoice == ChatCompletionsToolChoice.None) - { - Debug.Assert(chatOptions.Tools.Count == 0); - chatOptions.Tools.Add(s_nonInvocableFunctionTool); - } - - // Disable auto invocation if we've exceeded the allowed limit. - if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts) - { - autoInvoke = false; - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); - } - } } } /// Checks if a tool call is for a function that was defined. - private static bool IsRequestableTool(ChatCompletionsOptions options, AzureOpenAIFunctionToolCall ftc) + private static bool IsRequestableTool(ChatCompletionOptions options, AzureOpenAIFunctionToolCall ftc) { - IList tools = options.Tools; + IList tools = options.Tools; for (int i = 0; i < tools.Count; i++) { - if (tools[i] is ChatCompletionsFunctionToolDefinition def && - string.Equals(def.Name, ftc.FullyQualifiedName, StringComparison.OrdinalIgnoreCase)) + if (tools[i].Kind == ChatToolKind.Function && + string.Equals(tools[i].FunctionName, ftc.FullyQualifiedName, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -950,22 +891,21 @@ internal void AddAttribute(string key, string? value) /// Gets options to use for an OpenAIClient /// Custom for HTTP requests. - /// Optional API version. /// An instance of . - internal static OpenAIClientOptions GetOpenAIClientOptions(HttpClient? httpClient, OpenAIClientOptions.ServiceVersion? serviceVersion = null) + internal static AzureOpenAIClientOptions GetOpenAIClientOptions(HttpClient? httpClient) { - OpenAIClientOptions options = serviceVersion is not null ? - new(serviceVersion.Value) : - new(); + AzureOpenAIClientOptions options = new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + }; - options.Diagnostics.ApplicationId = HttpHeaderConstant.Values.UserAgent; - options.AddPolicy(new AddHeaderRequestPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore))), HttpPipelinePosition.PerCall); + options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore))), PipelinePosition.PerCall); if (httpClient is not null) { - options.Transport = new HttpClientTransport(httpClient); - options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. - options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout + options.Transport = new HttpClientPipelineTransport(httpClient); + options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. + options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout } return options; @@ -998,129 +938,86 @@ private static ChatHistory CreateNewChat(string? text = null, AzureOpenAIPromptE return chat; } - private static CompletionsOptions CreateCompletionsOptions(string text, AzureOpenAIPromptExecutionSettings executionSettings, string deploymentOrModelName) - { - if (executionSettings.ResultsPerPrompt is < 1 or > MaxResultsPerPrompt) - { - throw new ArgumentOutOfRangeException($"{nameof(executionSettings)}.{nameof(executionSettings.ResultsPerPrompt)}", executionSettings.ResultsPerPrompt, $"The value must be in range between 1 and {MaxResultsPerPrompt}, inclusive."); - } + //private static CompletionsOptions CreateCompletionsOptions(string text, AzureOpenAIPromptExecutionSettings executionSettings, string deploymentOrModelName) + //{ + // if (executionSettings.ResultsPerPrompt is < 1 or > MaxResultsPerPrompt) + // { + // throw new ArgumentOutOfRangeException($"{nameof(executionSettings)}.{nameof(executionSettings.ResultsPerPrompt)}", executionSettings.ResultsPerPrompt, $"The value must be in range between 1 and {MaxResultsPerPrompt}, inclusive."); + // } - var options = new CompletionsOptions - { - Prompts = { text.Replace("\r\n", "\n") }, // normalize line endings - MaxTokens = executionSettings.MaxTokens, - Temperature = (float?)executionSettings.Temperature, - NucleusSamplingFactor = (float?)executionSettings.TopP, - FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, - PresencePenalty = (float?)executionSettings.PresencePenalty, - Echo = false, - ChoicesPerPrompt = executionSettings.ResultsPerPrompt, - GenerationSampleCount = executionSettings.ResultsPerPrompt, - LogProbabilityCount = executionSettings.TopLogprobs, - User = executionSettings.User, - DeploymentName = deploymentOrModelName - }; + // var options = new CompletionsOptions + // { + // Prompts = { text.Replace("\r\n", "\n") }, // normalize line endings + // MaxTokens = executionSettings.MaxTokens, + // Temperature = (float?)executionSettings.Temperature, + // NucleusSamplingFactor = (float?)executionSettings.TopP, + // FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, + // PresencePenalty = (float?)executionSettings.PresencePenalty, + // Echo = false, + // ChoicesPerPrompt = executionSettings.ResultsPerPrompt, + // GenerationSampleCount = executionSettings.ResultsPerPrompt, + // LogProbabilityCount = executionSettings.TopLogprobs, + // User = executionSettings.User, + // DeploymentName = deploymentOrModelName + // }; - if (executionSettings.TokenSelectionBiases is not null) - { - foreach (var keyValue in executionSettings.TokenSelectionBiases) - { - options.TokenSelectionBiases.Add(keyValue.Key, keyValue.Value); - } - } + // if (executionSettings.TokenSelectionBiases is not null) + // { + // foreach (var keyValue in executionSettings.TokenSelectionBiases) + // { + // options.TokenSelectionBiases.Add(keyValue.Key, keyValue.Value); + // } + // } - if (executionSettings.StopSequences is { Count: > 0 }) - { - foreach (var s in executionSettings.StopSequences) - { - options.StopSequences.Add(s); - } - } + // if (executionSettings.StopSequences is { Count: > 0 }) + // { + // foreach (var s in executionSettings.StopSequences) + // { + // options.StopSequences.Add(s); + // } + // } - return options; - } + // return options; + //} - private ChatCompletionsOptions CreateChatCompletionsOptions( + private ChatCompletionOptions CreateChatCompletionsOptions( AzureOpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory, - Kernel? kernel, - string deploymentOrModelName) + ToolCallingConfig toolCallingConfig, + Kernel? kernel) { - if (executionSettings.ResultsPerPrompt is < 1 or > MaxResultsPerPrompt) - { - throw new ArgumentOutOfRangeException($"{nameof(executionSettings)}.{nameof(executionSettings.ResultsPerPrompt)}", executionSettings.ResultsPerPrompt, $"The value must be in range between 1 and {MaxResultsPerPrompt}, inclusive."); - } - - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", - JsonSerializer.Serialize(chatHistory), - JsonSerializer.Serialize(executionSettings)); - } - - var options = new ChatCompletionsOptions + var options = new ChatCompletionOptions { MaxTokens = executionSettings.MaxTokens, Temperature = (float?)executionSettings.Temperature, - NucleusSamplingFactor = (float?)executionSettings.TopP, + TopP = (float?)executionSettings.TopP, FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, PresencePenalty = (float?)executionSettings.PresencePenalty, - ChoiceCount = executionSettings.ResultsPerPrompt, - DeploymentName = deploymentOrModelName, Seed = executionSettings.Seed, User = executionSettings.User, - LogProbabilitiesPerToken = executionSettings.TopLogprobs, - EnableLogProbabilities = executionSettings.Logprobs, - AzureExtensionsOptions = executionSettings.AzureChatExtensionsOptions + TopLogProbabilityCount = executionSettings.TopLogprobs, + IncludeLogProbabilities = executionSettings.Logprobs, + ResponseFormat = GetResponseFormat(executionSettings) ?? ChatResponseFormat.Text, + ToolChoice = toolCallingConfig.Choice, }; - switch (executionSettings.ResponseFormat) + if (executionSettings.AzureChatDataSource is not null) { - case ChatCompletionsResponseFormat formatObject: - // If the response format is an Azure SDK ChatCompletionsResponseFormat, just pass it along. - options.ResponseFormat = formatObject; - break; - - case string formatString: - // If the response format is a string, map the ones we know about, and ignore the rest. - switch (formatString) - { - case "json_object": - options.ResponseFormat = ChatCompletionsResponseFormat.JsonObject; - break; - - case "text": - options.ResponseFormat = ChatCompletionsResponseFormat.Text; - break; - } - break; - - case JsonElement formatElement: - // This is a workaround for a type mismatch when deserializing a JSON into an object? type property. - // Handling only string formatElement. - if (formatElement.ValueKind == JsonValueKind.String) - { - string formatString = formatElement.GetString() ?? ""; - switch (formatString) - { - case "json_object": - options.ResponseFormat = ChatCompletionsResponseFormat.JsonObject; - break; +#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. + options.AddDataSource(executionSettings.AzureChatDataSource); +#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. + } - case "text": - options.ResponseFormat = ChatCompletionsResponseFormat.Text; - break; - } - } - break; + if (toolCallingConfig.Tools is { Count: > 0 } tools) + { + options.Tools.AddRange(tools); } - executionSettings.ToolCallBehavior?.ConfigureOptions(kernel, options); if (executionSettings.TokenSelectionBiases is not null) { foreach (var keyValue in executionSettings.TokenSelectionBiases) { - options.TokenSelectionBiases.Add(keyValue.Key, keyValue.Value); + options.LogitBiases.Add(keyValue.Key, keyValue.Value); } } @@ -1132,37 +1029,44 @@ private ChatCompletionsOptions CreateChatCompletionsOptions( } } + return options; + } + + private static List CreateChatCompletionMessages(AzureOpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory) + { + List messages = new(); + if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) { - options.Messages.AddRange(GetRequestMessages(new ChatMessageContent(AuthorRole.System, executionSettings!.ChatSystemPrompt), executionSettings.ToolCallBehavior)); + messages.AddRange(GetRequestMessages(new ChatMessageContent(AuthorRole.System, executionSettings!.ChatSystemPrompt), executionSettings.ToolCallBehavior)); } foreach (var message in chatHistory) { - options.Messages.AddRange(GetRequestMessages(message, executionSettings.ToolCallBehavior)); + messages.AddRange(GetRequestMessages(message, executionSettings.ToolCallBehavior)); } - return options; + return messages; } - private static ChatRequestMessage GetRequestMessage(ChatRole chatRole, string contents, string? name, ChatCompletionsFunctionToolCall[]? tools) + private static ChatMessage GetRequestMessage(ChatMessageRole chatRole, string content, string? name, ChatToolCall[]? tools) { - if (chatRole == ChatRole.User) + if (chatRole == ChatMessageRole.User) { - return new ChatRequestUserMessage(contents) { Name = name }; + return new UserChatMessage(content) { ParticipantName = name }; } - if (chatRole == ChatRole.System) + if (chatRole == ChatMessageRole.System) { - return new ChatRequestSystemMessage(contents) { Name = name }; + return new SystemChatMessage(content) { ParticipantName = name }; } - if (chatRole == ChatRole.Assistant) + if (chatRole == ChatMessageRole.Assistant) { - var msg = new ChatRequestAssistantMessage(contents) { Name = name }; + var msg = new AssistantChatMessage(content) { ParticipantName = name }; if (tools is not null) { - foreach (ChatCompletionsFunctionToolCall tool in tools) + foreach (ChatToolCall tool in tools) { msg.ToolCalls.Add(tool); } @@ -1173,11 +1077,11 @@ private static ChatRequestMessage GetRequestMessage(ChatRole chatRole, string co throw new NotImplementedException($"Role {chatRole} is not implemented"); } - private static List GetRequestMessages(ChatMessageContent message, AzureToolCallBehavior? toolCallBehavior) + private static List GetRequestMessages(ChatMessageContent message, AzureToolCallBehavior? toolCallBehavior) { if (message.Role == AuthorRole.System) { - return [new ChatRequestSystemMessage(message.Content) { Name = message.AuthorName }]; + return [new SystemChatMessage(message.Content) { ParticipantName = message.AuthorName }]; } if (message.Role == AuthorRole.Tool) @@ -1187,12 +1091,12 @@ private static List GetRequestMessages(ChatMessageContent me if (message.Metadata?.TryGetValue(AzureOpenAIChatMessageContent.ToolIdProperty, out object? toolId) is true && toolId?.ToString() is string toolIdString) { - return [new ChatRequestToolMessage(message.Content, toolIdString)]; + return [new ToolChatMessage(toolIdString, message.Content)]; } // Handling function results represented by the FunctionResultContent type. // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) - List? toolMessages = null; + List? toolMessages = null; foreach (var item in message.Items) { if (item is not FunctionResultContent resultContent) @@ -1204,13 +1108,13 @@ private static List GetRequestMessages(ChatMessageContent me if (resultContent.Result is Exception ex) { - toolMessages.Add(new ChatRequestToolMessage($"Error: Exception while invoking function. {ex.Message}", resultContent.CallId)); + toolMessages.Add(new ToolChatMessage(resultContent.CallId, $"Error: Exception while invoking function. {ex.Message}")); continue; } var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); - toolMessages.Add(new ChatRequestToolMessage(stringResult ?? string.Empty, resultContent.CallId)); + toolMessages.Add(new ToolChatMessage(resultContent.CallId, stringResult ?? string.Empty)); } if (toolMessages is not null) @@ -1225,33 +1129,33 @@ private static List GetRequestMessages(ChatMessageContent me { if (message.Items is { Count: 1 } && message.Items.FirstOrDefault() is TextContent textContent) { - return [new ChatRequestUserMessage(textContent.Text) { Name = message.AuthorName }]; + return [new UserChatMessage(textContent.Text) { ParticipantName = message.AuthorName }]; } - return [new ChatRequestUserMessage(message.Items.Select(static (KernelContent item) => (ChatMessageContentItem)(item switch + return [new UserChatMessage(message.Items.Select(static (KernelContent item) => (ChatMessageContentPart)(item switch { - TextContent textContent => new ChatMessageTextContentItem(textContent.Text), + TextContent textContent => ChatMessageContentPart.CreateTextMessageContentPart(textContent.Text), ImageContent imageContent => GetImageContentItem(imageContent), _ => throw new NotSupportedException($"Unsupported chat message content type '{item.GetType()}'.") }))) - { Name = message.AuthorName }]; + { ParticipantName = message.AuthorName }]; } if (message.Role == AuthorRole.Assistant) { - var asstMessage = new ChatRequestAssistantMessage(message.Content) { Name = message.AuthorName }; + var toolCalls = new List(); // Handling function calls supplied via either: // ChatCompletionsToolCall.ToolCalls collection items or // ChatMessageContent.Metadata collection item with 'ChatResponseMessage.FunctionToolCalls' key. - IEnumerable? tools = (message as AzureOpenAIChatMessageContent)?.ToolCalls; + IEnumerable? tools = (message as AzureOpenAIChatMessageContent)?.ToolCalls; if (tools is null && message.Metadata?.TryGetValue(AzureOpenAIChatMessageContent.FunctionToolCallsProperty, out object? toolCallsObject) is true) { - tools = toolCallsObject as IEnumerable; + tools = toolCallsObject as IEnumerable; if (tools is null && toolCallsObject is JsonElement { ValueKind: JsonValueKind.Array } array) { int length = array.GetArrayLength(); - var ftcs = new List(length); + var ftcs = new List(length); for (int i = 0; i < length; i++) { JsonElement e = array[i]; @@ -1262,7 +1166,7 @@ private static List GetRequestMessages(ChatMessageContent me name.ValueKind == JsonValueKind.String && arguments.ValueKind == JsonValueKind.String) { - ftcs.Add(new ChatCompletionsFunctionToolCall(id.GetString()!, name.GetString()!, arguments.GetString()!)); + ftcs.Add(ChatToolCall.CreateFunctionToolCall(id.GetString()!, name.GetString()!, arguments.GetString()!)); } } tools = ftcs; @@ -1271,7 +1175,7 @@ private static List GetRequestMessages(ChatMessageContent me if (tools is not null) { - asstMessage.ToolCalls.AddRange(tools); + toolCalls.AddRange(tools); } // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallContent type. @@ -1283,7 +1187,7 @@ private static List GetRequestMessages(ChatMessageContent me continue; } - functionCallIds ??= new HashSet(asstMessage.ToolCalls.Select(t => t.Id)); + functionCallIds ??= new HashSet(toolCalls.Select(t => t.Id)); if (callRequest.Id is null || functionCallIds.Contains(callRequest.Id)) { @@ -1292,69 +1196,60 @@ private static List GetRequestMessages(ChatMessageContent me var argument = JsonSerializer.Serialize(callRequest.Arguments); - asstMessage.ToolCalls.Add(new ChatCompletionsFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, AzureOpenAIFunction.NameSeparator), argument ?? string.Empty)); + toolCalls.Add(ChatToolCall.CreateFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, AzureOpenAIFunction.NameSeparator), argument ?? string.Empty)); } - return [asstMessage]; + return [new AssistantChatMessage(toolCalls, message.Content) { ParticipantName = message.AuthorName }]; } throw new NotSupportedException($"Role {message.Role} is not supported."); } - private static ChatMessageImageContentItem GetImageContentItem(ImageContent imageContent) + private static ChatMessageContentPart GetImageContentItem(ImageContent imageContent) { if (imageContent.Data is { IsEmpty: false } data) { - return new ChatMessageImageContentItem(BinaryData.FromBytes(data), imageContent.MimeType); + return ChatMessageContentPart.CreateImageMessageContentPart(BinaryData.FromBytes(data), imageContent.MimeType); } if (imageContent.Uri is not null) { - return new ChatMessageImageContentItem(imageContent.Uri); + return ChatMessageContentPart.CreateImageMessageContentPart(imageContent.Uri); } throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); } - private static ChatRequestMessage GetRequestMessage(ChatResponseMessage message) + private static ChatMessage GetRequestMessage(OpenAI.Chat.ChatCompletion completion) { - if (message.Role == ChatRole.System) + if (completion.Role == ChatMessageRole.System) { - return new ChatRequestSystemMessage(message.Content); + return ChatMessage.CreateSystemMessage(completion.Content[0].Text); } - if (message.Role == ChatRole.Assistant) + if (completion.Role == ChatMessageRole.Assistant) { - var msg = new ChatRequestAssistantMessage(message.Content); - if (message.ToolCalls is { Count: > 0 } tools) - { - foreach (ChatCompletionsToolCall tool in tools) - { - msg.ToolCalls.Add(tool); - } - } - - return msg; + return ChatMessage.CreateAssistantMessage(completion); } - if (message.Role == ChatRole.User) + if (completion.Role == ChatMessageRole.User) { - return new ChatRequestUserMessage(message.Content); + return ChatMessage.CreateUserMessage(completion.Content); } - throw new NotSupportedException($"Role {message.Role} is not supported."); + throw new NotSupportedException($"Role {completion.Role} is not supported."); } - private AzureOpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompletions responseData) + private AzureOpenAIChatMessageContent GetChatMessage(OpenAI.Chat.ChatCompletion completion) { - var message = new AzureOpenAIChatMessageContent(chatChoice.Message, this.DeploymentOrModelName, GetChatChoiceMetadata(responseData, chatChoice)); + var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentOrModelName, GetChatChoiceMetadata(completion)); - message.Items.AddRange(this.GetFunctionCallContents(chatChoice.Message.ToolCalls)); + message.Items.AddRange(this.GetFunctionCallContents(completion.ToolCalls)); return message; } - private AzureOpenAIChatMessageContent GetChatMessage(ChatRole chatRole, string content, ChatCompletionsFunctionToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) + private AzureOpenAIChatMessageContent GetChatMessage(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) { var message = new AzureOpenAIChatMessageContent(chatRole, content, this.DeploymentOrModelName, toolCalls, metadata) { @@ -1369,7 +1264,7 @@ private AzureOpenAIChatMessageContent GetChatMessage(ChatRole chatRole, string c return message; } - private IEnumerable GetFunctionCallContents(IEnumerable toolCalls) + private IEnumerable GetFunctionCallContents(IEnumerable toolCalls) { List? result = null; @@ -1377,13 +1272,13 @@ private IEnumerable GetFunctionCallContents(IEnumerable(functionToolCall.Arguments); + arguments = JsonSerializer.Deserialize(toolCall.FunctionArguments); if (arguments is not null) { // Iterate over copy of the names to avoid mutating the dictionary while enumerating it @@ -1400,19 +1295,19 @@ private IEnumerable GetFunctionCallContents(IEnumerable GetFunctionCallContents(IEnumerable(); } - private static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory chat, string? result, string? errorMessage, ChatCompletionsToolCall toolCall, ILogger logger) + private static void AddResponseMessage(List chatMessages, ChatHistory chat, string? result, string? errorMessage, ChatToolCall toolCall, ILogger logger) { // Log any error if (errorMessage is not null && logger.IsEnabled(LogLevel.Debug)) @@ -1433,19 +1328,19 @@ private static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatH logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolCall.Id, errorMessage); } - // Add the tool response message to the chat options + // Add the tool response message to the chat messages result ??= errorMessage ?? string.Empty; - chatOptions.Messages.Add(new ChatRequestToolMessage(result, toolCall.Id)); + chatMessages.Add(new ToolChatMessage(toolCall.Id, result)); // Add the tool response message to the chat history. var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { AzureOpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); - if (toolCall is ChatCompletionsFunctionToolCall functionCall) + if (toolCall.Kind == ChatToolCallKind.Function) { // Add an item of type FunctionResultContent to the ChatMessageContent.Items collection in addition to the function result stored as a string in the ChatMessageContent.Content property. // This will enable migration to the new function calling model and facilitate the deprecation of the current one in the future. - var functionName = FunctionName.Parse(functionCall.Name, AzureOpenAIFunction.NameSeparator); - message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, functionCall.Id, result)); + var functionName = FunctionName.Parse(toolCall.FunctionName, AzureOpenAIFunction.NameSeparator); + message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, toolCall.Id, result)); } chat.Add(message); @@ -1459,21 +1354,23 @@ private static void ValidateMaxTokens(int? maxTokens) } } - private static void ValidateAutoInvoke(bool autoInvoke, int resultsPerPrompt) + private static async Task RunRequestAsync(Func> request) { - if (autoInvoke && resultsPerPrompt != 1) + try { - // We can remove this restriction in the future if valuable. However, multiple results per prompt is rare, - // and limiting this significantly curtails the complexity of the implementation. - throw new ArgumentException($"Auto-invocation of tool calls may only be used with a {nameof(AzureOpenAIPromptExecutionSettings.ResultsPerPrompt)} of 1."); + return await request.Invoke().ConfigureAwait(false); + } + catch (RequestFailedException e) + { + throw e.ToHttpOperationException(); } } - private static async Task RunRequestAsync(Func> request) + private static T RunRequest(Func request) { try { - return await request.Invoke().ConfigureAwait(false); + return request.Invoke(); } catch (RequestFailedException e) { @@ -1484,8 +1381,8 @@ private static async Task RunRequestAsync(Func> request) /// /// Captures usage details, including token information. /// - /// Instance of with usage details. - private void LogUsage(CompletionsUsage usage) + /// Instance of with token usage details. + private void LogUsage(ChatTokenUsage usage) { if (usage is null) { @@ -1496,12 +1393,12 @@ private void LogUsage(CompletionsUsage usage) if (this.Logger.IsEnabled(LogLevel.Information)) { this.Logger.LogInformation( - "Prompt tokens: {PromptTokens}. Completion tokens: {CompletionTokens}. Total tokens: {TotalTokens}.", - usage.PromptTokens, usage.CompletionTokens, usage.TotalTokens); + "Prompt tokens: {InputTokens}. Completion tokens: {OutputTokens}. Total tokens: {TotalTokens}.", + usage.InputTokens, usage.OutputTokens, usage.TotalTokens); } - s_promptTokensCounter.Add(usage.PromptTokens); - s_completionTokensCounter.Add(usage.CompletionTokens); + s_promptTokensCounter.Add(usage.InputTokens); + s_completionTokensCounter.Add(usage.OutputTokens); s_totalTokensCounter.Add(usage.TotalTokens); } @@ -1571,4 +1468,95 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context await functionCallCallback(context).ConfigureAwait(false); } } + + private ToolCallingConfig GetToolCallingConfiguration(Kernel? kernel, AzureOpenAIPromptExecutionSettings executionSettings, int requestIndex) + { + if (executionSettings.ToolCallBehavior is null) + { + return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); + } + + if (requestIndex >= executionSettings.ToolCallBehavior.MaximumUseAttempts) + { + // Don't add any tools as we've reached the maximum attempts limit. + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", executionSettings.ToolCallBehavior!.MaximumUseAttempts); + } + + return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); + } + + var (tools, choice) = executionSettings.ToolCallBehavior.ConfigureOptions(kernel); + + bool autoInvoke = kernel is not null && + executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts > 0 && + s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; + + // Disable auto invocation if we've exceeded the allowed limit. + if (requestIndex >= executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts) + { + autoInvoke = false; + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", executionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); + } + } + + return new ToolCallingConfig( + Tools: tools ?? [s_nonInvocableFunctionTool], + Choice: choice ?? ChatToolChoice.None, + AutoInvoke: autoInvoke); + } + + private static ChatResponseFormat? GetResponseFormat(AzureOpenAIPromptExecutionSettings executionSettings) + { + switch (executionSettings.ResponseFormat) + { + case ChatResponseFormat formatObject: + // If the response format is an Azure SDK ChatCompletionsResponseFormat, just pass it along. + return formatObject; + case string formatString: + // If the response format is a string, map the ones we know about, and ignore the rest. + switch (formatString) + { + case "json_object": + return ChatResponseFormat.JsonObject; + + case "text": + return ChatResponseFormat.Text; + } + break; + + case JsonElement formatElement: + // This is a workaround for a type mismatch when deserializing a JSON into an object? type property. + // Handling only string formatElement. + if (formatElement.ValueKind == JsonValueKind.String) + { + string formatString = formatElement.GetString() ?? ""; + switch (formatString) + { + case "json_object": + return ChatResponseFormat.JsonObject; + + case "text": + return ChatResponseFormat.Text; + } + } + break; + } + + return null; + } + + private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) + { + return new GenericActionPipelinePolicy((message) => + { + if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false) + { + message.Request.Headers.Set(headerName, headerValue); + } + }); + } } From 483da86448a9a764fc9bfc14f4f7f49a60e188b4 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 27 Jun 2024 14:59:14 +0100 Subject: [PATCH 2/5] fix: address PR review comments --- .../Core/AzureOpenAIClientCore.cs | 3 +- .../Connectors.AzureOpenAI/Core/ClientCore.cs | 212 ++---------------- 2 files changed, 19 insertions(+), 196 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs index ca9311c4a285..c37321e48c4d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.ClientModel; using System.Net.Http; using Azure.AI.OpenAI; using Azure.Core; @@ -49,7 +48,7 @@ internal AzureOpenAIClientCore( this.DeploymentOrModelName = deploymentName; this.Endpoint = new Uri(endpoint); - this.Client = new AzureOpenAIClient(this.Endpoint, new ApiKeyCredential(apiKey), options); + this.Client = new AzureOpenAIClient(this.Endpoint, apiKey, options); } /// diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs index 7bfbb361c36a..a9352075e1a8 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -34,6 +34,10 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// internal abstract class ClientCore { + private const string CreatedMetadataKey = "Created"; + private const string PromptFilterResultsMetadataKey = "PromptFilterResults"; + private const string ContentFilterResultsMetadataKey = "ContentFilterResults"; + private const string LogProbabilityInfoMetadataKey = "LogProbabilityInfo"; private const string ModelProvider = "openai"; private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); @@ -120,155 +124,21 @@ internal ClientCore(ILogger? logger = null) unit: "{token}", description: "Number of tokens used"); - ///// - ///// Creates completions for the prompt and settings. - ///// - ///// The prompt to complete. - ///// Execution settings for the completion API. - ///// The containing services, plugins, and other state for use throughout the operation. - ///// The to monitor for cancellation requests. The default is . - ///// Completions generated by the remote model - //internal async Task> GetTextResultsAsync( - // string prompt, - // PromptExecutionSettings? executionSettings, - // Kernel? kernel, - // CancellationToken cancellationToken = default) - //{ - // AzureOpenAIPromptExecutionSettings textExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings, AzureOpenAIPromptExecutionSettings.DefaultTextMaxTokens); - - // ValidateMaxTokens(textExecutionSettings.MaxTokens); - - // var options = CreateCompletionsOptions(prompt, textExecutionSettings, this.DeploymentOrModelName); - - // Completions? responseData = null; - // List responseContent; - // using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, prompt, textExecutionSettings)) - // { - // try - // { - // responseData = (await RunRequestAsync(() => this.Client.GetCompletionsAsync(options, cancellationToken)).ConfigureAwait(false)).Value; - // if (responseData.Choices.Count == 0) - // { - // throw new KernelException("Text completions not found"); - // } - // } - // catch (Exception ex) when (activity is not null) - // { - // activity.SetError(ex); - // if (responseData != null) - // { - // // Capture available metadata even if the operation failed. - // activity - // .SetResponseId(responseData.Id) - // .SetPromptTokenUsage(responseData.Usage.PromptTokens) - // .SetCompletionTokenUsage(responseData.Usage.CompletionTokens); - // } - // throw; - // } - - // responseContent = responseData.Choices.Select(choice => new TextContent(choice.Text, this.DeploymentOrModelName, choice, Encoding.UTF8, GetTextChoiceMetadata(responseData, choice))).ToList(); - // activity?.SetCompletionResponse(responseContent, responseData.Usage.PromptTokens, responseData.Usage.CompletionTokens); - // } - - // this.LogUsage(responseData.Usage); - - // return responseContent; - //} - - //internal async IAsyncEnumerable GetStreamingTextContentsAsync( - // string prompt, - // PromptExecutionSettings? executionSettings, - // Kernel? kernel, - // [EnumeratorCancellation] CancellationToken cancellationToken = default) - //{ - // AzureOpenAIPromptExecutionSettings textExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings, AzureOpenAIPromptExecutionSettings.DefaultTextMaxTokens); - - // ValidateMaxTokens(textExecutionSettings.MaxTokens); - - // var options = CreateCompletionsOptions(prompt, textExecutionSettings, this.DeploymentOrModelName); - - // using var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, prompt, textExecutionSettings); - - // StreamingResponse response; - // try - // { - // response = await RunRequestAsync(() => this.Client.GetCompletionsStreamingAsync(options, cancellationToken)).ConfigureAwait(false); - // } - // catch (Exception ex) when (activity is not null) - // { - // activity.SetError(ex); - // throw; - // } - - // var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); - // List? streamedContents = activity is not null ? [] : null; - // try - // { - // while (true) - // { - // try - // { - // if (!await responseEnumerator.MoveNextAsync()) - // { - // break; - // } - // } - // catch (Exception ex) when (activity is not null) - // { - // activity.SetError(ex); - // throw; - // } - - // Completions completions = responseEnumerator.Current; - // foreach (Choice choice in completions.Choices) - // { - // var openAIStreamingTextContent = new AzureOpenAIStreamingTextContent( - // choice.Text, choice.Index, this.DeploymentOrModelName, choice, GetTextChoiceMetadata(completions, choice)); - // streamedContents?.Add(openAIStreamingTextContent); - // yield return openAIStreamingTextContent; - // } - // } - // } - // finally - // { - // activity?.EndStreaming(streamedContents); - // await responseEnumerator.DisposeAsync(); - // } - //} - - //private static Dictionary GetTextChoiceMetadata(Completions completions, Choice choice) - //{ - // return new Dictionary(8) - // { - // { nameof(completions.Id), completions.Id }, - // { nameof(completions.Created), completions.Created }, - // { nameof(completions.PromptFilterResults), completions.PromptFilterResults }, - // { nameof(completions.Usage), completions.Usage }, - // { nameof(choice.ContentFilterResults), choice.ContentFilterResults }, - - // // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - // { nameof(choice.FinishReason), choice.FinishReason?.ToString() }, - - // { nameof(choice.LogProbabilityModel), choice.LogProbabilityModel }, - // { nameof(choice.Index), choice.Index }, - // }; - //} - private static Dictionary GetChatChoiceMetadata(OpenAI.Chat.ChatCompletion 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(12) { { nameof(completions.Id), completions.Id }, - { nameof(completions.CreatedAt), completions.CreatedAt }, - { "PromptFilterResults", completions.GetContentFilterResultForPrompt() }, + { CreatedMetadataKey, completions.CreatedAt }, + { PromptFilterResultsMetadataKey, completions.GetContentFilterResultForPrompt() }, { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, { nameof(completions.Usage), completions.Usage }, - { "ContentFilterResults", completions.GetContentFilterResultForResponse() }, + { ContentFilterResultsMetadataKey, completions.GetContentFilterResultForResponse() }, // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. { nameof(completions.FinishReason), completions.FinishReason.ToString() }, - { nameof(completions.ContentTokenLogProbabilities), completions.ContentTokenLogProbabilities }, + { LogProbabilityInfoMetadataKey, completions.ContentTokenLogProbabilities }, }; #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. } @@ -278,7 +148,7 @@ internal ClientCore(ILogger? logger = null) return new Dictionary(4) { { nameof(completionUpdate.Id), completionUpdate.Id }, - { nameof(completionUpdate.CreatedAt), completionUpdate.CreatedAt }, + { CreatedMetadataKey, completionUpdate.CreatedAt }, { nameof(completionUpdate.SystemFingerprint), completionUpdate.SystemFingerprint }, // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. @@ -658,10 +528,12 @@ internal async IAsyncEnumerable GetStrea // If we're intending to invoke function calls, we need to consume that function call information. if (toolCallingConfig.AutoInvoke) { - var textUpdate = update.ContentUpdate.FirstOrDefault(u => u.Kind == ChatMessageContentPartKind.Text)?.Text; - if (textUpdate is { Length: > 0 }) + foreach (var contentPart in update.ContentUpdate) { - (contentBuilder ??= new()).Append(textUpdate); + if (contentPart.Kind == ChatMessageContentPartKind.Text) + { + (contentBuilder ??= new()).Append(contentPart.Text); + } } AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(update.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); @@ -671,6 +543,8 @@ internal async IAsyncEnumerable GetStrea foreach (var functionCallUpdate in update.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. if (string.IsNullOrEmpty(functionCallUpdate.Id) && string.IsNullOrEmpty(functionCallUpdate.FunctionName) && string.IsNullOrEmpty(functionCallUpdate.FunctionArgumentsUpdate)) @@ -938,48 +812,6 @@ private static ChatHistory CreateNewChat(string? text = null, AzureOpenAIPromptE return chat; } - //private static CompletionsOptions CreateCompletionsOptions(string text, AzureOpenAIPromptExecutionSettings executionSettings, string deploymentOrModelName) - //{ - // if (executionSettings.ResultsPerPrompt is < 1 or > MaxResultsPerPrompt) - // { - // throw new ArgumentOutOfRangeException($"{nameof(executionSettings)}.{nameof(executionSettings.ResultsPerPrompt)}", executionSettings.ResultsPerPrompt, $"The value must be in range between 1 and {MaxResultsPerPrompt}, inclusive."); - // } - - // var options = new CompletionsOptions - // { - // Prompts = { text.Replace("\r\n", "\n") }, // normalize line endings - // MaxTokens = executionSettings.MaxTokens, - // Temperature = (float?)executionSettings.Temperature, - // NucleusSamplingFactor = (float?)executionSettings.TopP, - // FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, - // PresencePenalty = (float?)executionSettings.PresencePenalty, - // Echo = false, - // ChoicesPerPrompt = executionSettings.ResultsPerPrompt, - // GenerationSampleCount = executionSettings.ResultsPerPrompt, - // LogProbabilityCount = executionSettings.TopLogprobs, - // User = executionSettings.User, - // DeploymentName = deploymentOrModelName - // }; - - // if (executionSettings.TokenSelectionBiases is not null) - // { - // foreach (var keyValue in executionSettings.TokenSelectionBiases) - // { - // options.TokenSelectionBiases.Add(keyValue.Key, keyValue.Value); - // } - // } - - // if (executionSettings.StopSequences is { Count: > 0 }) - // { - // foreach (var s in executionSettings.StopSequences) - // { - // options.StopSequences.Add(s); - // } - // } - - // return options; - //} - private ChatCompletionOptions CreateChatCompletionsOptions( AzureOpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory, @@ -1034,7 +866,7 @@ private ChatCompletionOptions CreateChatCompletionsOptions( private static List CreateChatCompletionMessages(AzureOpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory) { - List messages = new(); + List messages = []; if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) { @@ -1063,15 +895,7 @@ private static ChatMessage GetRequestMessage(ChatMessageRole chatRole, string co if (chatRole == ChatMessageRole.Assistant) { - var msg = new AssistantChatMessage(content) { ParticipantName = name }; - if (tools is not null) - { - foreach (ChatToolCall tool in tools) - { - msg.ToolCalls.Add(tool); - } - } - return msg; + return new AssistantChatMessage(tools, content) { ParticipantName = name }; } throw new NotImplementedException($"Role {chatRole} is not implemented"); From e19655bcf23024ecf69689d83a3ee69dc3acda36 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 27 Jun 2024 15:32:20 +0100 Subject: [PATCH 3/5] fix: introducing aliaz for OpenAI.Chat.ChatCompletion. --- .../Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs index a9352075e1a8..a4b50568db36 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -24,6 +24,7 @@ using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; +using AzureChatCompletion = OpenAI.Chat.ChatCompletion; #pragma warning disable CA2208 // Instantiate argument exceptions correctly @@ -124,7 +125,7 @@ internal ClientCore(ILogger? logger = null) unit: "{token}", description: "Number of tokens used"); - private static Dictionary GetChatChoiceMetadata(OpenAI.Chat.ChatCompletion completions) + private static Dictionary GetChatChoiceMetadata(AzureChatCompletion 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(12) @@ -275,7 +276,7 @@ internal async Task> GetChatMessageContentsAsy var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); // Make the request. - OpenAI.Chat.ChatCompletion? responseData = null; + AzureChatCompletion? responseData = null; AzureOpenAIChatMessageContent responseContent; using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) { @@ -1044,7 +1045,7 @@ private static ChatMessageContentPart GetImageContentItem(ImageContent imageCont throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); } - private static ChatMessage GetRequestMessage(OpenAI.Chat.ChatCompletion completion) + private static ChatMessage GetRequestMessage(AzureChatCompletion completion) { if (completion.Role == ChatMessageRole.System) { @@ -1064,7 +1065,7 @@ private static ChatMessage GetRequestMessage(OpenAI.Chat.ChatCompletion completi throw new NotSupportedException($"Role {completion.Role} is not supported."); } - private AzureOpenAIChatMessageContent GetChatMessage(OpenAI.Chat.ChatCompletion completion) + private AzureOpenAIChatMessageContent GetChatMessage(AzureChatCompletion completion) { var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentOrModelName, GetChatChoiceMetadata(completion)); From dabbcfcd5850471d2acfc3565ee0edb5f7913bf5 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 27 Jun 2024 15:51:21 +0100 Subject: [PATCH 4/5] fix: rollback backward compatibility chat content metadata --- .../Connectors.AzureOpenAI/Core/ClientCore.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs index a4b50568db36..de08225b5593 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -24,7 +24,7 @@ using OpenAI.Audio; using OpenAI.Chat; using OpenAI.Embeddings; -using AzureChatCompletion = OpenAI.Chat.ChatCompletion; +using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; #pragma warning disable CA2208 // Instantiate argument exceptions correctly @@ -35,7 +35,6 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// internal abstract class ClientCore { - private const string CreatedMetadataKey = "Created"; private const string PromptFilterResultsMetadataKey = "PromptFilterResults"; private const string ContentFilterResultsMetadataKey = "ContentFilterResults"; private const string LogProbabilityInfoMetadataKey = "LogProbabilityInfo"; @@ -125,13 +124,13 @@ internal ClientCore(ILogger? logger = null) unit: "{token}", description: "Number of tokens used"); - private static Dictionary GetChatChoiceMetadata(AzureChatCompletion completions) + private static Dictionary GetChatChoiceMetadata(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(12) { { nameof(completions.Id), completions.Id }, - { CreatedMetadataKey, completions.CreatedAt }, + { nameof(completions.CreatedAt), completions.CreatedAt }, { PromptFilterResultsMetadataKey, completions.GetContentFilterResultForPrompt() }, { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, { nameof(completions.Usage), completions.Usage }, @@ -149,7 +148,7 @@ internal ClientCore(ILogger? logger = null) return new Dictionary(4) { { nameof(completionUpdate.Id), completionUpdate.Id }, - { CreatedMetadataKey, completionUpdate.CreatedAt }, + { nameof(completionUpdate.CreatedAt), completionUpdate.CreatedAt }, { nameof(completionUpdate.SystemFingerprint), completionUpdate.SystemFingerprint }, // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. @@ -276,7 +275,7 @@ internal async Task> GetChatMessageContentsAsy var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); // Make the request. - AzureChatCompletion? responseData = null; + OpenAIChatCompletion? responseData = null; AzureOpenAIChatMessageContent responseContent; using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) { @@ -1045,7 +1044,7 @@ private static ChatMessageContentPart GetImageContentItem(ImageContent imageCont throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); } - private static ChatMessage GetRequestMessage(AzureChatCompletion completion) + private static ChatMessage GetRequestMessage(OpenAIChatCompletion completion) { if (completion.Role == ChatMessageRole.System) { @@ -1065,7 +1064,7 @@ private static ChatMessage GetRequestMessage(AzureChatCompletion completion) throw new NotSupportedException($"Role {completion.Role} is not supported."); } - private AzureOpenAIChatMessageContent GetChatMessage(AzureChatCompletion completion) + private AzureOpenAIChatMessageContent GetChatMessage(OpenAIChatCompletion completion) { var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentOrModelName, GetChatChoiceMetadata(completion)); From 6b1bf265369bc39f9eeb36155506b33554af8571 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 27 Jun 2024 17:56:48 +0100 Subject: [PATCH 5/5] fix: address PR comments --- .../AzureOpenAITestHelper.cs | 3 +- ...cs => AzureOpenAIToolCallBehaviorTests.cs} | 14 ++-- .../AzureOpenAIChatCompletionServiceTests.cs | 28 +++---- .../ClientResultExceptionExtensionsTests.cs | 53 +++++++++++++ .../RequestFailedExceptionExtensionsTests.cs | 77 ------------------- .../AutoFunctionInvocationFilterTests.cs | 24 +++--- .../AzureOpenAIPromptExecutionSettings.cs | 12 +-- ...vior.cs => AzureOpenAIToolCallBehavior.cs} | 34 ++++---- ....cs => ClientResultExceptionExtensions.cs} | 9 ++- .../Core/AzureOpenAIChatMessageContent.cs | 3 +- .../Connectors.AzureOpenAI/Core/ClientCore.cs | 18 ++--- 11 files changed, 126 insertions(+), 149 deletions(-) rename dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/{AzureToolCallBehaviorTests.cs => AzureOpenAIToolCallBehaviorTests.cs} (94%) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/RequestFailedExceptionExtensionsTests.cs rename dotnet/src/Connectors/Connectors.AzureOpenAI/{AzureToolCallBehavior.cs => AzureOpenAIToolCallBehavior.cs} (88%) rename dotnet/src/Connectors/Connectors.AzureOpenAI/{RequestFailedExceptionExtensions.cs => ClientResultExceptionExtensions.cs} (78%) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs index 49aa51c7ce6a..31a7654fcfc6 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs @@ -2,7 +2,6 @@ using System.IO; using System.Net.Http; -using System.Text; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; @@ -26,6 +25,6 @@ internal static string GetTestResponse(string fileName) /// Name of the file with test response. internal static StreamContent GetTestResponseAsStream(string fileName) { - return new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(AzureOpenAITestHelper.GetTestResponse(fileName)))); + return new StreamContent(File.OpenRead($"./TestData/{fileName}")); } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIToolCallBehaviorTests.cs similarity index 94% rename from dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs rename to dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIToolCallBehaviorTests.cs index abb0851221ab..6baa78faae1e 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIToolCallBehaviorTests.cs @@ -5,20 +5,20 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using OpenAI.Chat; -using static Microsoft.SemanticKernel.Connectors.AzureOpenAI.AzureToolCallBehavior; +using static Microsoft.SemanticKernel.Connectors.AzureOpenAI.AzureOpenAIToolCallBehavior; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; /// -/// Unit tests for +/// Unit tests for /// -public sealed class AzureToolCallBehaviorTests +public sealed class AzureOpenAIToolCallBehaviorTests { [Fact] public void EnableKernelFunctionsReturnsCorrectKernelFunctionsInstance() { // Arrange & Act - var behavior = AzureToolCallBehavior.EnableKernelFunctions; + var behavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions; // Assert Assert.IsType(behavior); @@ -30,7 +30,7 @@ public void AutoInvokeKernelFunctionsReturnsCorrectKernelFunctionsInstance() { // Arrange & Act const int DefaultMaximumAutoInvokeAttempts = 128; - var behavior = AzureToolCallBehavior.AutoInvokeKernelFunctions; + var behavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions; // Assert Assert.IsType(behavior); @@ -42,7 +42,7 @@ public void EnableFunctionsReturnsEnabledFunctionsInstance() { // Arrange & Act List functions = [new("Plugin", "Function", "description", [], null)]; - var behavior = AzureToolCallBehavior.EnableFunctions(functions); + var behavior = AzureOpenAIToolCallBehavior.EnableFunctions(functions); // Assert Assert.IsType(behavior); @@ -52,7 +52,7 @@ public void EnableFunctionsReturnsEnabledFunctionsInstance() public void RequireFunctionReturnsRequiredFunctionInstance() { // Arrange & Act - var behavior = AzureToolCallBehavior.RequireFunction(new("Plugin", "Function", "description", [], null)); + var behavior = AzureOpenAIToolCallBehavior.RequireFunction(new("Plugin", "Function", "description", [], null)); // Assert Assert.IsType(behavior); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index 6921fdbe0706..3b3c90687b45 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -237,7 +237,7 @@ public async Task GetChatMessageContentsHandlesResponseFormatCorrectlyAsync(obje [Theory] [MemberData(nameof(ToolCallBehaviors))] - public async Task GetChatMessageContentsWorksCorrectlyAsync(AzureToolCallBehavior behavior) + public async Task GetChatMessageContentsWorksCorrectlyAsync(AzureOpenAIToolCallBehavior behavior) { // Arrange var kernel = Kernel.CreateBuilder().Build(); @@ -288,7 +288,7 @@ public async Task GetChatMessageContentsWithFunctionCallAsync() kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_multiple_function_calls_test_response.json")) }; using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; @@ -324,7 +324,7 @@ public async Task GetChatMessageContentsWithFunctionCallMaximumAutoInvokeAttempt kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; var responses = new List(); @@ -361,7 +361,7 @@ public async Task GetChatMessageContentsWithRequiredFunctionCallAsync() kernel.Plugins.Add(plugin); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_single_function_call_test_response.json")) }; using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; @@ -458,7 +458,7 @@ public async Task GetStreamingChatMessageContentsWithFunctionCallAsync() kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_multiple_function_calls_test_response.txt") }; using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_test_response.txt") }; @@ -502,7 +502,7 @@ public async Task GetStreamingChatMessageContentsWithFunctionCallMaximumAutoInvo kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; var responses = new List(); @@ -541,7 +541,7 @@ public async Task GetStreamingChatMessageContentsWithRequiredFunctionCallAsync() kernel.Plugins.Add(plugin); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_single_function_call_test_response.txt") }; using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_test_response.txt") }; @@ -700,7 +700,7 @@ public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfT var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Fake prompt"); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; // Act var result = await sut.GetChatMessageContentAsync(chatHistory, settings); @@ -770,7 +770,7 @@ public async Task FunctionCallsShouldBeReturnedToLLMAsync() new ChatMessageContent(AuthorRole.Assistant, items) ]; - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; // Act await sut.GetChatMessageContentAsync(chatHistory, settings); @@ -829,7 +829,7 @@ public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsyn ]) }; - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; // Act await sut.GetChatMessageContentAsync(chatHistory, settings); @@ -874,7 +874,7 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage ]) }; - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; // Act await sut.GetChatMessageContentAsync(chatHistory, settings); @@ -905,10 +905,10 @@ public void Dispose() this._messageHandlerStub.Dispose(); } - public static TheoryData ToolCallBehaviors => new() + public static TheoryData ToolCallBehaviors => new() { - AzureToolCallBehavior.EnableKernelFunctions, - AzureToolCallBehavior.AutoInvokeKernelFunctions + AzureOpenAIToolCallBehavior.EnableKernelFunctions, + AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; public static TheoryData ResponseFormats => new() diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs new file mode 100644 index 000000000000..d810b2d2a470 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; + +/// +/// Unit tests for class. +/// +public sealed class ClientResultExceptionExtensionsTests +{ + [Fact] + public void ToHttpOperationExceptionWithContentReturnsValidException() + { + // Arrange + using var response = new FakeResponse("Response Content", 500); + var exception = new ClientResultException(response); + + // Act + var actualException = exception.ToHttpOperationException(); + + // Assert + Assert.IsType(actualException); + Assert.Equal(HttpStatusCode.InternalServerError, actualException.StatusCode); + Assert.Equal("Response Content", actualException.ResponseContent); + Assert.Same(exception, actualException.InnerException); + } + + #region private + + private sealed class FakeResponse(string responseContent, int status) : PipelineResponse + { + private readonly string _responseContent = responseContent; + public override BinaryData Content => BinaryData.FromString(this._responseContent); + public override int Status { get; } = status; + public override string ReasonPhrase => "Reason Phrase"; + public override Stream? ContentStream { get => null; set => throw new NotImplementedException(); } + protected override PipelineResponseHeaders HeadersCore => throw new NotImplementedException(); + public override BinaryData BufferContent(CancellationToken cancellationToken = default) => new(this._responseContent); + public override ValueTask BufferContentAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public override void Dispose() { } + } + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/RequestFailedExceptionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/RequestFailedExceptionExtensionsTests.cs deleted file mode 100644 index 9fb65039116d..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/RequestFailedExceptionExtensionsTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using Azure; -using Azure.Core; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; - -/// -/// Unit tests for class. -/// -public sealed class RequestFailedExceptionExtensionsTests -{ - [Theory] - [InlineData(0, null)] - [InlineData(500, HttpStatusCode.InternalServerError)] - public void ToHttpOperationExceptionWithStatusReturnsValidException(int responseStatus, HttpStatusCode? httpStatusCode) - { - // Arrange - var exception = new RequestFailedException(responseStatus, "Error Message"); - - // Act - var actualException = exception.ToHttpOperationException(); - - // Assert - Assert.IsType(actualException); - Assert.Equal(httpStatusCode, actualException.StatusCode); - Assert.Equal("Error Message", actualException.Message); - Assert.Same(exception, actualException.InnerException); - } - - [Fact] - public void ToHttpOperationExceptionWithContentReturnsValidException() - { - // Arrange - using var response = new FakeResponse("Response Content", 500); - var exception = new RequestFailedException(response); - - // Act - var actualException = exception.ToHttpOperationException(); - - // Assert - Assert.IsType(actualException); - Assert.Equal(HttpStatusCode.InternalServerError, actualException.StatusCode); - Assert.Equal("Response Content", actualException.ResponseContent); - Assert.Same(exception, actualException.InnerException); - } - - #region private - - private sealed class FakeResponse(string responseContent, int status) : Response - { - private readonly string _responseContent = responseContent; - private readonly IEnumerable _headers = []; - - public override BinaryData Content => BinaryData.FromString(this._responseContent); - public override int Status { get; } = status; - public override string ReasonPhrase => "Reason Phrase"; - public override Stream? ContentStream { get => null; set => throw new NotImplementedException(); } - public override string ClientRequestId { get => "Client Request Id"; set => throw new NotImplementedException(); } - - public override void Dispose() { } - protected override bool ContainsHeader(string name) => throw new NotImplementedException(); - protected override IEnumerable EnumerateHeaders() => this._headers; -#pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). - protected override bool TryGetHeader(string name, out string? value) => throw new NotImplementedException(); - protected override bool TryGetHeaderValues(string name, out IEnumerable? values) => throw new NotImplementedException(); -#pragma warning restore CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). - } - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs index 12a2a739c47a..195f71e2758f 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs @@ -64,7 +64,7 @@ public async Task FiltersAreExecutedCorrectlyAsync() // Act var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions })); // Assert @@ -107,7 +107,7 @@ public async Task FiltersAreExecutedCorrectlyOnStreamingAsync() this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; // Act await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) @@ -167,7 +167,7 @@ public async Task DifferentWaysOfAddingFiltersWorkCorrectlyAsync() var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions })); // Assert @@ -227,7 +227,7 @@ public async Task MultipleFiltersAreExecutedInOrderAsync(bool isStreaming) var arguments = new KernelArguments(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }); // Act @@ -277,7 +277,7 @@ public async Task FilterCanOverrideArgumentsAsync() // Act var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions })); // Assert @@ -309,7 +309,7 @@ public async Task FilterCanHandleExceptionAsync() var chatCompletion = new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; var chatHistory = new ChatHistory(); chatHistory.AddSystemMessage("System message"); @@ -350,7 +350,7 @@ public async Task FilterCanHandleExceptionOnStreamingAsync() var chatCompletion = new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); var chatHistory = new ChatHistory(); - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; // Act await foreach (var item in chatCompletion.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel)) @@ -396,7 +396,7 @@ public async Task FiltersCanSkipFunctionExecutionAsync() // Act var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions })); // Assert @@ -430,7 +430,7 @@ public async Task PreFilterCanTerminateOperationAsync() // Act await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions })); // Assert @@ -460,7 +460,7 @@ public async Task PreFilterCanTerminateOperationOnStreamingAsync() this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; // Act await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) @@ -501,7 +501,7 @@ public async Task PostFilterCanTerminateOperationAsync() // Act var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions })); // Assert @@ -545,7 +545,7 @@ public async Task PostFilterCanTerminateOperationOnStreamingAsync() this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; List streamingContent = []; diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs index 3bf30f28e07e..22141ee8aee0 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs @@ -191,18 +191,18 @@ public IDictionary? TokenSelectionBiases /// To disable all tool calling, set the property to null (the default). /// /// To request that the model use a specific function, set the property to an instance returned - /// from . + /// from . /// /// /// To allow the model to request one of any number of functions, set the property to an - /// instance returned from , called with + /// instance returned from , called with /// a list of the functions available. /// /// /// To allow the model to request one of any of the functions in the supplied , - /// set the property to if the client should simply + /// set the property to if the client should simply /// send the information about the functions and not handle the response in any special manner, or - /// if the client should attempt to automatically + /// if the client should attempt to automatically /// invoke the function and send the result back to the service. /// /// @@ -213,7 +213,7 @@ public IDictionary? TokenSelectionBiases /// the function, and sending back the result. The intermediate messages will be retained in the /// if an instance was provided. /// - public AzureToolCallBehavior? ToolCallBehavior + public AzureOpenAIToolCallBehavior? ToolCallBehavior { get => this._toolCallBehavior; @@ -403,7 +403,7 @@ public static AzureOpenAIPromptExecutionSettings FromExecutionSettingsWithData(P private long? _seed; private object? _responseFormat; private IDictionary? _tokenSelectionBiases; - private AzureToolCallBehavior? _toolCallBehavior; + private AzureOpenAIToolCallBehavior? _toolCallBehavior; private string? _user; private string? _chatSystemPrompt; private bool? _logprobs; diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIToolCallBehavior.cs similarity index 88% rename from dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs rename to dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIToolCallBehavior.cs index 7983b085d5a1..e9dbd224b2a0 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIToolCallBehavior.cs @@ -11,7 +11,7 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// Represents a behavior for Azure OpenAI tool calls. -public abstract class AzureToolCallBehavior +public abstract class AzureOpenAIToolCallBehavior { // NOTE: Right now, the only tools that are available are for function calling. In the future, // this class can be extended to support additional kinds of tools, including composite ones: @@ -45,7 +45,7 @@ public abstract class AzureToolCallBehavior /// /// If no is available, no function information will be provided to the model. /// - public static AzureToolCallBehavior EnableKernelFunctions { get; } = new KernelFunctions(autoInvoke: false); + public static AzureOpenAIToolCallBehavior EnableKernelFunctions { get; } = new KernelFunctions(autoInvoke: false); /// /// Gets an instance that will both provide all of the 's plugins' function information @@ -56,16 +56,16 @@ public abstract class AzureToolCallBehavior /// handling invoking any requested functions and supplying the results back to the model. /// If no is available, no function information will be provided to the model. /// - public static AzureToolCallBehavior AutoInvokeKernelFunctions { get; } = new KernelFunctions(autoInvoke: true); + public static AzureOpenAIToolCallBehavior AutoInvokeKernelFunctions { get; } = new KernelFunctions(autoInvoke: true); /// Gets an instance that will provide the specified list of functions to the model. /// The functions that should be made available to the model. /// true to attempt to automatically handle function call requests; otherwise, false. /// - /// The that may be set into + /// The that may be set into /// to indicate that the specified functions should be made available to the model. /// - public static AzureToolCallBehavior EnableFunctions(IEnumerable functions, bool autoInvoke = false) + public static AzureOpenAIToolCallBehavior EnableFunctions(IEnumerable functions, bool autoInvoke = false) { Verify.NotNull(functions); return new EnabledFunctions(functions, autoInvoke); @@ -75,17 +75,17 @@ public static AzureToolCallBehavior EnableFunctions(IEnumerableThe function the model should request to use. /// true to attempt to automatically handle function call requests; otherwise, false. /// - /// The that may be set into + /// The that may be set into /// to indicate that the specified function should be requested by the model. /// - public static AzureToolCallBehavior RequireFunction(AzureOpenAIFunction function, bool autoInvoke = false) + public static AzureOpenAIToolCallBehavior RequireFunction(AzureOpenAIFunction function, bool autoInvoke = false) { Verify.NotNull(function); return new RequiredFunction(function, autoInvoke); } /// Initializes the instance; prevents external instantiation. - private AzureToolCallBehavior(bool autoInvoke) + private AzureOpenAIToolCallBehavior(bool autoInvoke) { this.MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; } @@ -123,10 +123,10 @@ private AzureToolCallBehavior(bool autoInvoke) internal abstract (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel); /// - /// Represents a that will provide to the model all available functions from a + /// Represents a that will provide to the model all available functions from a /// provided by the client. Setting this will have no effect if no is provided. /// - internal sealed class KernelFunctions : AzureToolCallBehavior + internal sealed class KernelFunctions : AzureOpenAIToolCallBehavior { internal KernelFunctions(bool autoInvoke) : base(autoInvoke) { } @@ -145,9 +145,10 @@ internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOpti if (functions.Count > 0) { choice = ChatToolChoice.Auto; + tools = []; for (int i = 0; i < functions.Count; i++) { - (tools ??= []).Add(functions[i].ToAzureOpenAIFunction().ToFunctionDefinition()); + tools.Add(functions[i].ToAzureOpenAIFunction().ToFunctionDefinition()); } } } @@ -159,9 +160,9 @@ internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOpti } /// - /// Represents a that provides a specified list of functions to the model. + /// Represents a that provides a specified list of functions to the model. /// - internal sealed class EnabledFunctions : AzureToolCallBehavior + internal sealed class EnabledFunctions : AzureOpenAIToolCallBehavior { private readonly AzureOpenAIFunction[] _openAIFunctions; private readonly ChatTool[] _functions; @@ -204,6 +205,7 @@ internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOpti } choice = ChatToolChoice.Auto; + tools = []; for (int i = 0; i < openAIFunctions.Length; i++) { // Make sure that if auto-invocation is specified, every enabled function can be found in the kernel. @@ -218,7 +220,7 @@ internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOpti } // Add the function. - (tools ??= []).Add(functions[i]); + tools.Add(functions[i]); } } @@ -226,8 +228,8 @@ internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOpti } } - /// Represents a that requests the model use a specific function. - internal sealed class RequiredFunction : AzureToolCallBehavior + /// Represents a that requests the model use a specific function. + internal sealed class RequiredFunction : AzureOpenAIToolCallBehavior { private readonly AzureOpenAIFunction _function; private readonly ChatTool _tool; diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/RequestFailedExceptionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/ClientResultExceptionExtensions.cs similarity index 78% rename from dotnet/src/Connectors/Connectors.AzureOpenAI/RequestFailedExceptionExtensions.cs rename to dotnet/src/Connectors/Connectors.AzureOpenAI/ClientResultExceptionExtensions.cs index 3857d0191fbe..fd282797e879 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/RequestFailedExceptionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/ClientResultExceptionExtensions.cs @@ -1,21 +1,22 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel; using System.Net; using Azure; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// -/// Provides extension methods for the class. +/// Provides extension methods for the class. /// -internal static class RequestFailedExceptionExtensions +internal static class ClientResultExceptionExtensions { /// - /// Converts a to an . + /// Converts a to an . /// /// The original . /// An instance. - public static HttpOperationException ToHttpOperationException(this RequestFailedException exception) + public static HttpOperationException ToHttpOperationException(this ClientResultException exception) { const int NoResponseReceived = 0; diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs index b950b2408332..ff7183cb0b12 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs @@ -4,6 +4,7 @@ using System.Linq; using Microsoft.SemanticKernel.ChatCompletion; using OpenAI.Chat; +using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -25,7 +26,7 @@ public sealed class AzureOpenAIChatMessageContent : ChatMessageContent /// /// Initializes a new instance of the class. /// - internal AzureOpenAIChatMessageContent(OpenAI.Chat.ChatCompletion completion, string modelId, IReadOnlyDictionary? metadata = null) + internal AzureOpenAIChatMessageContent(OpenAIChatCompletion completion, string modelId, IReadOnlyDictionary? metadata = null) : base(new AuthorRole(completion.Role.ToString()), CreateContentItems(completion.Content), modelId, completion, System.Text.Encoding.UTF8, CreateMetadataDictionary(completion.ToolCalls, metadata)) { this.ToolCalls = completion.ToolCalls; diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs index de08225b5593..6486d7348144 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -13,7 +13,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Azure; using Azure.AI.OpenAI; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -870,7 +869,7 @@ private static List CreateChatCompletionMessages(AzureOpenAIPromptE if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) { - messages.AddRange(GetRequestMessages(new ChatMessageContent(AuthorRole.System, executionSettings!.ChatSystemPrompt), executionSettings.ToolCallBehavior)); + messages.Add(new SystemChatMessage(executionSettings.ChatSystemPrompt)); } foreach (var message in chatHistory) @@ -901,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, AzureToolCallBehavior? toolCallBehavior) + private static List GetRequestMessages(ChatMessageContent message, AzureOpenAIToolCallBehavior? toolCallBehavior) { if (message.Role == AuthorRole.System) { @@ -1088,9 +1087,9 @@ private AzureOpenAIChatMessageContent GetChatMessage(ChatMessageRole chatRole, s return message; } - private IEnumerable GetFunctionCallContents(IEnumerable toolCalls) + private List GetFunctionCallContents(IEnumerable toolCalls) { - List? result = null; + List result = []; foreach (var toolCall in toolCalls) { @@ -1135,12 +1134,11 @@ private IEnumerable GetFunctionCallContents(IEnumerable(); + return result; } private static void AddResponseMessage(List chatMessages, ChatHistory chat, string? result, string? errorMessage, ChatToolCall toolCall, ILogger logger) @@ -1184,7 +1182,7 @@ private static async Task RunRequestAsync(Func> request) { return await request.Invoke().ConfigureAwait(false); } - catch (RequestFailedException e) + catch (ClientResultException e) { throw e.ToHttpOperationException(); } @@ -1196,7 +1194,7 @@ private static T RunRequest(Func request) { return request.Invoke(); } - catch (RequestFailedException e) + catch (ClientResultException e) { throw e.ToHttpOperationException(); } @@ -1232,7 +1230,7 @@ private void LogUsage(ChatTokenUsage usage) /// The result of the function call. /// The ToolCallBehavior object containing optional settings like JsonSerializerOptions.TypeInfoResolver. /// A string representation of the function result. - private static string? ProcessFunctionResult(object functionResult, AzureToolCallBehavior? toolCallBehavior) + private static string? ProcessFunctionResult(object functionResult, AzureOpenAIToolCallBehavior? toolCallBehavior) { if (functionResult is string stringResult) {