From 19254dcbb0ce21f15a4d7fff8741461b6ce79a27 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 5 Apr 2024 23:02:14 +0100 Subject: [PATCH 01/90] FunctionCallContent and FunctionResultContent classes are added. --- dotnet/SK-dotnet.sln | 1 + .../Example59_OpenAIFunctionCalling.cs | 45 ++-- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 114 +++++++-- .../AzureOpenAIChatCompletionServiceTests.cs | 203 ++++++++++++++++ .../OpenAIChatCompletionServiceTests.cs | 205 +++++++++++++++- .../Connectors/OpenAI/OpenAIToolsTests.cs | 223 +++++++++++++++++- .../src/System/IListExtensions.cs | 35 +++ .../AI/ChatCompletion/ChatHistory.cs | 22 ++ .../Contents/ChatMessageContent.cs | 10 + .../Contents/FunctionCallContent.cs | 113 +++++++++ .../Contents/FunctionResultContent.cs | 66 ++++++ .../Contents/KernelContent.cs | 2 + .../Functions/KernelArguments.cs | 10 + .../AI/ChatCompletion/ChatHistoryTests.cs | 21 ++ .../Contents/ChatMessageContentTests.cs | 49 +++- .../Contents/FunctionCallContentTests.cs | 95 ++++++++ .../Contents/FunctionResultContentTests.cs | 99 ++++++++ .../Utilities/IListExtensionsTests.cs | 27 +++ 18 files changed, 1295 insertions(+), 45 deletions(-) create mode 100644 dotnet/src/InternalUtilities/src/System/IListExtensions.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Utilities/IListExtensionsTests.cs diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index c5720732273f..16fb4fead3fc 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -123,6 +123,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "System", "System", "{3CDE10B2-AE8F-4FC4-8D55-92D4AD32E144}" ProjectSection(SolutionItems) = preProject src\InternalUtilities\src\System\EnvExtensions.cs = src\InternalUtilities\src\System\EnvExtensions.cs + src\InternalUtilities\src\System\IListExtensions.cs = src\InternalUtilities\src\System\IListExtensions.cs src\InternalUtilities\src\System\InternalTypeConverter.cs = src\InternalUtilities\src\System\InternalTypeConverter.cs src\InternalUtilities\src\System\NonNullCollection.cs = src\InternalUtilities\src\System\NonNullCollection.cs src\InternalUtilities\src\System\TypeConverterFactory.cs = src\InternalUtilities\src\System\TypeConverterFactory.cs diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index 3c874fe9e053..53c5453cc28e 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -2,10 +2,7 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text.Json; using System.Threading.Tasks; -using Azure.AI.OpenAI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; @@ -70,40 +67,36 @@ public async Task RunAsync() WriteLine("======== Example 3: Use manual function calling with a non-streaming prompt ========"); { var chat = kernel.GetRequiredService(); - var chatHistory = new ChatHistory(); OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - while (true) - { - var result = (OpenAIChatMessageContent)await chat.GetChatMessageContentAsync(chatHistory, settings, kernel); - if (result.Content is not null) - { - Write(result.Content); - } + ChatMessageContent result = await chat.GetChatMessageContentAsync(chatHistory, settings, kernel); + chatHistory.Add(result); // Adding LLM response containing function calls(requests) to chat history as it's required by LLMs. - List toolCalls = result.ToolCalls.OfType().ToList(); - if (toolCalls.Count == 0) + IEnumerable functionCalls = result.GetFunctionCalls(); // Getting list of function calls. + + foreach (var functionCall in functionCalls) + { + try { - break; - } + FunctionResultContent functionResult = await functionCall.InvokeAsync(kernel); // Executing each function. Can be done in parallel. - chatHistory.Add(result); - foreach (var toolCall in toolCalls) + chatHistory.AddMessage(AuthorRole.Tool, functionResult); // Adding function result to chat history. + } + catch (Exception ex) { - string content = kernel.Plugins.TryGetFunctionAndArguments(toolCall, out KernelFunction? function, out KernelArguments? arguments) ? - JsonSerializer.Serialize((await function.InvokeAsync(kernel, arguments)).GetValue()) : - "Unable to find function. Please try again!"; - - chatHistory.Add(new ChatMessageContent( - AuthorRole.Tool, - content, - metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } })); + chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(functionCall, ex)); // Adding exception to chat history. + // or + //string message = $"Error details that LLM can reason about."; + //chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(functionCall, message)); } } - WriteLine(); + // Sending the functions invocation results to the LLM to get the final response. + WriteLine(await chat.GetChatMessageContentAsync(chatHistory, settings, kernel)); } /* Uncomment this to try in a console chat loop. diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 006fe1fa3aa9..979bc8790726 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -310,7 +310,7 @@ internal async Task> GetChatMessageContentsAsy // 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) { - return responseData.Choices.Select(chatChoice => new OpenAIChatMessageContent(chatChoice.Message, this.DeploymentOrModelName, GetChatChoiceMetadata(responseData, chatChoice))).ToList(); + return responseData.Choices.Select(chatChoice => this.GetChatMessage(chatChoice, responseData)).ToList(); } Debug.Assert(kernel is not null); @@ -321,7 +321,7 @@ internal async Task> GetChatMessageContentsAsy // 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]; - OpenAIChatMessageContent result = new(resultChoice.Message, this.DeploymentOrModelName, GetChatChoiceMetadata(responseData, resultChoice)); + OpenAIChatMessageContent result = this.GetChatMessage(resultChoice, responseData); if (result.ToolCalls.Count == 0) { return new[] { result }; @@ -905,14 +905,14 @@ private static ChatCompletionsOptions CreateChatCompletionsOptions( } } - if (!string.IsNullOrWhiteSpace(executionSettings?.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) + if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) { - options.Messages.Add(GetRequestMessage(new ChatMessageContent(AuthorRole.System, executionSettings!.ChatSystemPrompt))); + options.Messages.AddRange(GetRequestMessages(new ChatMessageContent(AuthorRole.System, executionSettings!.ChatSystemPrompt), executionSettings.ToolCallBehavior)); } foreach (var message in chatHistory) { - options.Messages.Add(GetRequestMessage(message)); + options.Messages.AddRange(GetRequestMessages(message, executionSettings.ToolCallBehavior)); } return options; @@ -946,38 +946,70 @@ private static ChatRequestMessage GetRequestMessage(ChatRole chatRole, string co throw new NotImplementedException($"Role {chatRole} is not implemented"); } - private static ChatRequestMessage GetRequestMessage(ChatMessageContent message) + private static IEnumerable GetRequestMessages(ChatMessageContent message, ToolCallBehavior? toolCallBehavior) { if (message.Role == AuthorRole.System) { - return new ChatRequestSystemMessage(message.Content); + return new[] { new ChatRequestSystemMessage(message.Content) }; } - if (message.Role == AuthorRole.User || message.Role == AuthorRole.Tool) + if (message.Role == AuthorRole.Tool) { + // Handling function call results represented by the TextContent type. + // Example: new ChatMessageContent(AuthorRole.Tool, content, metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }) if (message.Metadata?.TryGetValue(OpenAIChatMessageContent.ToolIdProperty, out object? toolId) is true && toolId?.ToString() is string toolIdString) { - return new ChatRequestToolMessage(message.Content, toolIdString); + return new[] { new ChatRequestToolMessage(message.Content, toolIdString) }; + } + + // Handling function call results represented by the FunctionResultContent type. + // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) + if (message.Items.OfType() is { } resultContents && resultContents.Any()) + { + var toolMessages = new List(); + + foreach (var resultContent in resultContents) + { + if (resultContent.Result is Exception ex) + { + toolMessages.Add(new ChatRequestToolMessage($"Error: Exception while invoking function. {ex.Message}", resultContent.Id)); + continue; + } + + var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); + + toolMessages.Add(new ChatRequestToolMessage(stringResult ?? string.Empty, resultContent.Id)); + } + + return toolMessages; } + throw new NotSupportedException("No function result provided in the tool massage."); + } + + if (message.Role == AuthorRole.User) + { if (message.Items is { Count: 1 } && message.Items.FirstOrDefault() is TextContent textContent) { - return new ChatRequestUserMessage(textContent.Text); + return new[] { new ChatRequestUserMessage(textContent.Text) }; } - return new ChatRequestUserMessage(message.Items.Select(static (KernelContent item) => (ChatMessageContentItem)(item switch + return new[] {new ChatRequestUserMessage(message.Items.Select(static (KernelContent item) => (ChatMessageContentItem)(item switch { TextContent textContent => new ChatMessageTextContentItem(textContent.Text), ImageContent imageContent => new ChatMessageImageContentItem(imageContent.Uri), _ => throw new NotSupportedException($"Unsupported chat message content type '{item.GetType()}'.") - }))); + })))}; } if (message.Role == AuthorRole.Assistant) { var asstMessage = new ChatRequestAssistantMessage(message.Content); + // Handling function calls supplied via either: + // ChatCompletionsToolCall.ToolCalls collection items or + // ChatMessageContent.Metadata collection item with 'ChatResponseMessage.FunctionToolCalls' key. IEnumerable? tools = (message as OpenAIChatMessageContent)?.ToolCalls; if (tools is null && message.Metadata?.TryGetValue(OpenAIChatMessageContent.FunctionToolCallsProperty, out object? toolCallsObject) is true) { @@ -1003,15 +1035,30 @@ private static ChatRequestMessage GetRequestMessage(ChatMessageContent message) } } - if (tools is not null) + // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallContent type. + if (message.Items.OfType() is { } functionCallContents && functionCallContents.Any()) { - foreach (ChatCompletionsToolCall tool in tools) + var ftcs = new List(tools ?? Enumerable.Empty()); + + foreach (var fcContent in functionCallContents) { - asstMessage.ToolCalls.Add(tool); + if (!ftcs.Any(ftc => ftc.Id == fcContent.Id)) + { + var argument = JsonSerializer.Serialize(fcContent.Arguments); + + ftcs.Add(new ChatCompletionsFunctionToolCall(fcContent.Id, fcContent.GetFullyQualifiedName(OpenAIFunction.NameSeparator), argument ?? string.Empty)); + } } + + tools = ftcs; } - return asstMessage; + if (tools is not null) + { + asstMessage.ToolCalls.AddRange(tools); + } + + return new[] { asstMessage }; } throw new NotSupportedException($"Role {message.Role} is not supported."); @@ -1046,6 +1093,41 @@ private static ChatRequestMessage GetRequestMessage(ChatResponseMessage message) throw new NotSupportedException($"Role {message.Role} is not supported."); } + private OpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompletions responseData) + { + var message = new OpenAIChatMessageContent(chatChoice.Message, this.DeploymentOrModelName, GetChatChoiceMetadata(responseData, chatChoice)); + + foreach (var toolCall in chatChoice.Message.ToolCalls) + { + // Adding items of 'FunctionCallContent' type to the 'Items' collection even though the function calls are available via the 'ToolCalls' property. + // This allows consumers to work with functions in an LLM-agnostic way. + if (toolCall is ChatCompletionsFunctionToolCall functionToolCall) + { + KernelArguments? arguments = null; + try + { + arguments = JsonSerializer.Deserialize(functionToolCall.Arguments); + } + catch (JsonException) + { + // If the arguments are not valid JSON, we'll just leave them as null. + // The original arguments and function tool call will be available via the 'InnerContent' property for the connector caller to access. + } + + var content = FunctionCallContent.Create( + fullyQualifiedName: functionToolCall.Name, + id: functionToolCall.Id, + arguments: arguments, + functionNameSeparator: OpenAIFunction.NameSeparator); + content.InnerContent = functionToolCall; + + message.Items.Add(content); + } + } + + return message; + } + private static void ValidateMaxTokens(int? maxTokens) { if (maxTokens.HasValue && maxTokens < 1) diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index 7bd7b25fb381..0c6528d3eb73 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -673,6 +673,209 @@ public async Task GetChatMessageContentsWithChatMessageContentItemCollectionAndS Assert.Equal("image_url", contentItems[1].GetProperty("type").GetString()); } + [Fact] + public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfTypeFunctionCallContentAsync() + { + // Arrange + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_multiple_function_calls_test_response.json")) + }); + + var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Fake prompt"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + // Act + var result = await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + Assert.NotNull(result); + Assert.Equal(4, result.Items.Count); + + var getCurrentWeatherFunctionCall = result.Items[0] as FunctionCallContent; + Assert.NotNull(getCurrentWeatherFunctionCall); + Assert.Equal("GetCurrentWeather", getCurrentWeatherFunctionCall.FunctionName); + Assert.Equal("MyPlugin", getCurrentWeatherFunctionCall.PluginName); + Assert.Equal("1", getCurrentWeatherFunctionCall.Id); + Assert.Equal("Boston, MA", getCurrentWeatherFunctionCall.Arguments?["location"]?.ToString()); + + var functionWithExceptionFunctionCall = result.Items[1] as FunctionCallContent; + Assert.NotNull(functionWithExceptionFunctionCall); + Assert.Equal("FunctionWithException", functionWithExceptionFunctionCall.FunctionName); + Assert.Equal("MyPlugin", functionWithExceptionFunctionCall.PluginName); + Assert.Equal("2", functionWithExceptionFunctionCall.Id); + Assert.Equal("value", functionWithExceptionFunctionCall.Arguments?["argument"]?.ToString()); + + var nonExistentFunctionCall = result.Items[2] as FunctionCallContent; + Assert.NotNull(nonExistentFunctionCall); + Assert.Equal("NonExistentFunction", nonExistentFunctionCall.FunctionName); + Assert.Equal("MyPlugin", nonExistentFunctionCall.PluginName); + Assert.Equal("3", nonExistentFunctionCall.Id); + Assert.Equal("value", nonExistentFunctionCall.Arguments?["argument"]?.ToString()); + + var invalidArgumentsFunctionCall = result.Items[3] as FunctionCallContent; + Assert.NotNull(invalidArgumentsFunctionCall); + Assert.Equal("InvalidArguments", invalidArgumentsFunctionCall.FunctionName); + Assert.Equal("MyPlugin", invalidArgumentsFunctionCall.PluginName); + Assert.Equal("4", invalidArgumentsFunctionCall.Id); + Assert.Null(invalidArgumentsFunctionCall.Arguments); + } + + [Fact] + public async Task FunctionCallsShouldBeReturnedToLLMAsync() + { + // Arrange + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + + var items = new ChatMessageContentItemCollection + { + new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), + new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }) + }; + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.Assistant, items) + }; + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(2, messages.GetArrayLength()); + + var assistantMessage = messages[1]; + Assert.Equal("assistant", assistantMessage.GetProperty("role").GetString()); + + Assert.Equal(2, assistantMessage.GetProperty("tool_calls").GetArrayLength()); + + var tool1 = assistantMessage.GetProperty("tool_calls")[0]; + Assert.Equal("1", tool1.GetProperty("id").GetString()); + Assert.Equal("function", tool1.GetProperty("type").GetString()); + + var function1 = tool1.GetProperty("function"); + Assert.Equal("MyPlugin-GetCurrentWeather", function1.GetProperty("name").GetString()); + Assert.Equal("{\"location\":\"Boston, MA\"}", function1.GetProperty("arguments").GetString()); + + var tool2 = assistantMessage.GetProperty("tool_calls")[1]; + Assert.Equal("2", tool2.GetProperty("id").GetString()); + Assert.Equal("function", tool2.GetProperty("type").GetString()); + + var function2 = tool2.GetProperty("function"); + Assert.Equal("MyPlugin-GetWeatherForecast", function2.GetProperty("name").GetString()); + Assert.Equal("{\"location\":\"Boston, MA\"}", function2.GetProperty("arguments").GetString()); + } + + [Fact] + public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsync() + { + // Arrange + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() + { + new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + }), + new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() + { + new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + }) + }; + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(3, messages.GetArrayLength()); + + var assistantMessage = messages[1]; + Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); + Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); + Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); + + var assistantMessage2 = messages[2]; + Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); + Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); + Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); + } + + [Fact] + public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessageAsync() + { + // Arrange + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() + { + new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + }) + }; + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(3, messages.GetArrayLength()); + + var assistantMessage = messages[1]; + Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); + Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); + Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); + + var assistantMessage2 = messages[2]; + Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); + Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); + Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); + } + public void Dispose() { this._httpClient.Dispose(); diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs index bafa85e49e9a..58b85c1a87f7 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs @@ -150,7 +150,7 @@ public async Task ItAddsIdToChatMessageAsync() this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(ChatCompletionResponse) }; var chatHistory = new ChatHistory(); - chatHistory.AddMessage(AuthorRole.User, "Hello", metadata: new Dictionary() { { OpenAIChatMessageContent.ToolIdProperty, "John Doe" } }); + chatHistory.AddMessage(AuthorRole.Tool, "Hello", metadata: new Dictionary() { { OpenAIChatMessageContent.ToolIdProperty, "John Doe" } }); // Act await chatCompletion.GetChatMessageContentsAsync(chatHistory, this._executionSettings); @@ -320,6 +320,209 @@ public async Task GetChatMessageContentsWithChatMessageContentItemCollectionAndS Assert.Equal("image_url", contentItems[1].GetProperty("type").GetString()); } + [Fact] + public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfTypeFunctionCallContentAsync() + { + // Arrange + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_multiple_function_calls_test_response.json")) + }; + + var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Fake prompt"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + // Act + var result = await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + Assert.NotNull(result); + Assert.Equal(4, result.Items.Count); + + var getCurrentWeatherFunctionCall = result.Items[0] as FunctionCallContent; + Assert.NotNull(getCurrentWeatherFunctionCall); + Assert.Equal("GetCurrentWeather", getCurrentWeatherFunctionCall.FunctionName); + Assert.Equal("MyPlugin", getCurrentWeatherFunctionCall.PluginName); + Assert.Equal("1", getCurrentWeatherFunctionCall.Id); + Assert.Equal("Boston, MA", getCurrentWeatherFunctionCall.Arguments?["location"]?.ToString()); + + var functionWithExceptionFunctionCall = result.Items[1] as FunctionCallContent; + Assert.NotNull(functionWithExceptionFunctionCall); + Assert.Equal("FunctionWithException", functionWithExceptionFunctionCall.FunctionName); + Assert.Equal("MyPlugin", functionWithExceptionFunctionCall.PluginName); + Assert.Equal("2", functionWithExceptionFunctionCall.Id); + Assert.Equal("value", functionWithExceptionFunctionCall.Arguments?["argument"]?.ToString()); + + var nonExistentFunctionCall = result.Items[2] as FunctionCallContent; + Assert.NotNull(nonExistentFunctionCall); + Assert.Equal("NonExistentFunction", nonExistentFunctionCall.FunctionName); + Assert.Equal("MyPlugin", nonExistentFunctionCall.PluginName); + Assert.Equal("3", nonExistentFunctionCall.Id); + Assert.Equal("value", nonExistentFunctionCall.Arguments?["argument"]?.ToString()); + + var invalidArgumentsFunctionCall = result.Items[3] as FunctionCallContent; + Assert.NotNull(invalidArgumentsFunctionCall); + Assert.Equal("InvalidArguments", invalidArgumentsFunctionCall.FunctionName); + Assert.Equal("MyPlugin", invalidArgumentsFunctionCall.PluginName); + Assert.Equal("4", invalidArgumentsFunctionCall.Id); + Assert.Null(invalidArgumentsFunctionCall.Arguments); + } + + [Fact] + public async Task FunctionCallsShouldBeReturnedToLLMAsync() + { + // Arrange + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(ChatCompletionResponse) + }; + + var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + var items = new ChatMessageContentItemCollection + { + new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), + new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }) + }; + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.Assistant, items) + }; + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(2, messages.GetArrayLength()); + + var assistantMessage = messages[1]; + Assert.Equal("assistant", assistantMessage.GetProperty("role").GetString()); + + Assert.Equal(2, assistantMessage.GetProperty("tool_calls").GetArrayLength()); + + var tool1 = assistantMessage.GetProperty("tool_calls")[0]; + Assert.Equal("1", tool1.GetProperty("id").GetString()); + Assert.Equal("function", tool1.GetProperty("type").GetString()); + + var function1 = tool1.GetProperty("function"); + Assert.Equal("MyPlugin-GetCurrentWeather", function1.GetProperty("name").GetString()); + Assert.Equal("{\"location\":\"Boston, MA\"}", function1.GetProperty("arguments").GetString()); + + var tool2 = assistantMessage.GetProperty("tool_calls")[1]; + Assert.Equal("2", tool2.GetProperty("id").GetString()); + Assert.Equal("function", tool2.GetProperty("type").GetString()); + + var function2 = tool2.GetProperty("function"); + Assert.Equal("MyPlugin-GetWeatherForecast", function2.GetProperty("name").GetString()); + Assert.Equal("{\"location\":\"Boston, MA\"}", function2.GetProperty("arguments").GetString()); + } + + [Fact] + public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsync() + { + // Arrange + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(ChatCompletionResponse) + }; + + var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() + { + new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + }), + new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() + { + new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + }) + }; + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(3, messages.GetArrayLength()); + + var assistantMessage = messages[1]; + Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); + Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); + Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); + + var assistantMessage2 = messages[2]; + Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); + Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); + Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); + } + + [Fact] + public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessageAsync() + { + // Arrange + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(ChatCompletionResponse) + }; + + var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() + { + new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + }) + }; + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(3, messages.GetArrayLength()); + + var assistantMessage = messages[1]; + Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); + Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); + Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); + + var assistantMessage2 = messages[2]; + Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); + Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); + Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); + } + public void Dispose() { this._httpClient.Dispose(); diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index 2e2e0bcc429b..2dc53bbeabe6 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -3,10 +3,14 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Text; +using System.Text.Json; using System.Threading.Tasks; +using Azure.AI.OpenAI; using Microsoft.Extensions.Configuration; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using SemanticKernel.IntegrationTests.Planners.Stepwise; using SemanticKernel.IntegrationTests.TestSettings; @@ -183,7 +187,210 @@ public async Task CanAutoInvokeKernelFunctionFromPromptStreamingAsync() Assert.Contains("Transportation", result, StringComparison.InvariantCultureIgnoreCase); } - private Kernel InitializeKernel() + [Fact] + public async Task ConnectorSpecificChatMessageContentClassesCanBeUsedForManualFunctionCallingAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + // Act + var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Current way of handling function calls manually using connector specific chat message content class. + var toolCalls = ((OpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); + + while (toolCalls.Count > 0) + { + // Adding LLM function call request to chat history + chatHistory.Add(result); + + // Iterating over the requested function calls and invoking them + foreach (var toolCall in toolCalls) + { + string content = kernel.Plugins.TryGetFunctionAndArguments(toolCall, out KernelFunction? function, out KernelArguments? arguments) ? + JsonSerializer.Serialize((await function.InvokeAsync(kernel, arguments)).GetValue()) : + "Unable to find function. Please try again!"; + + // Adding the result of the function call to the chat history + chatHistory.Add(new ChatMessageContent( + AuthorRole.Tool, + content, + metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } })); + } + + // Sending the functions invocation results back to the LLM to get the final response + result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + toolCalls = ((OpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); + } + + // Assert + Assert.Contains("rain", result.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManualFunctionCallingAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + // Act + var messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + var functionCalls = messageContent.GetFunctionCalls().ToArray(); + + while (functionCalls.Length != 0) + { + // Adding function call request from LLM to chat history + chatHistory.Add(messageContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + var result = await functionCall.InvokeAsync(kernel); + + chatHistory.AddMessage(AuthorRole.Tool, result); + } + + // Sending the functions invocation results to the LLM to get the final response + messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + functionCalls = messageContent.GetFunctionCalls().ToArray(); + } + + // Assert + Assert.Contains("rain", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExceptionToConnectorAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var completionService = kernel.GetRequiredService(); + + // Act + var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + + var functionCalls = messageContent.GetFunctionCalls().ToArray(); + + while (functionCalls.Length != 0) + { + // Adding function call request from LLM to chat history + chatHistory.Add(messageContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + // Simulating an exception + var exception = new OperationCanceledException("The operation was canceled due to timeout."); + + chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(functionCall, exception)); + } + + // Sending the functions execution results back to the LLM to get the final response + messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + functionCalls = messageContent.GetFunctionCalls().ToArray(); + } + + // Assert + Assert.NotNull(messageContent.Content); + + var failureWords = new List() { "error", "unable", "couldn", "issue", "trouble" }; + Assert.Contains(failureWords, word => messageContent.Content.Contains(word, StringComparison.InvariantCultureIgnoreCase)); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFunctionCallsAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var completionService = kernel.GetRequiredService(); + + // Act + var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + + var functionCalls = messageContent.GetFunctionCalls().ToArray(); + + while (functionCalls.Length > 0) + { + // Adding function call request from LLM to chat history + chatHistory.Add(messageContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + var result = await functionCall.InvokeAsync(kernel); + + chatHistory.AddMessage(AuthorRole.Tool, result); + } + + // Adding a simulated function call to the connector response message + var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); + messageContent.Items.Add(simulatedFunctionCall); + + // Adding a simulated function result to chat history + var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; + chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(simulatedFunctionCall, simulatedFunctionResult)); + + // Sending the functions invocation results back to the LLM to get the final response + messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + functionCalls = messageContent.Items.OfType().ToArray(); + } + + // Assert + Assert.Contains("tornado", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ItFailsIfNoFunctionResultProvidedAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var completionService = kernel.GetRequiredService(); + + // Act + var result = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + + chatHistory.Add(result); + + var exception = await Assert.ThrowsAsync(() => completionService.GetChatMessageContentAsync(chatHistory, settings, kernel)); + + // Assert + Assert.Contains("'tool_calls' must be followed by tool", exception.Message, StringComparison.InvariantCulture); + } + + private Kernel InitializeKernel(bool importHelperPlugin = false) { OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("Planners:OpenAI").Get(); Assert.NotNull(openAIConfiguration); @@ -195,6 +402,20 @@ private Kernel InitializeKernel() var kernel = builder.Build(); + if (importHelperPlugin) + { + kernel.ImportPluginFromFunctions("HelperFunctions", new[] + { + kernel.CreateFunctionFromMethod(() => DateTime.UtcNow.ToString("R"), "GetCurrentUtcTime", "Retrieves the current time in UTC."), + kernel.CreateFunctionFromMethod((string cityName) => + cityName switch + { + "Boston" => "61 and rainy", + _ => "31 and snowing", + }, "Get_Weather_For_City", "Gets the current weather for the specified city"), + }); + } + return kernel; } diff --git a/dotnet/src/InternalUtilities/src/System/IListExtensions.cs b/dotnet/src/InternalUtilities/src/System/IListExtensions.cs new file mode 100644 index 000000000000..c1e3c3a3dbc1 --- /dev/null +++ b/dotnet/src/InternalUtilities/src/System/IListExtensions.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.SemanticKernel; + +[ExcludeFromCodeCoverage] +internal static class IListExtensions +{ + /// + /// Adds a range of elements from the specified source to the target . + /// + /// The type of elements in the list. + /// The target to add elements to. + /// The source containing elements to add to the target . + internal static void AddRange(this IList target, IEnumerable source) + { + if (target is null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + foreach (var item in source) + { + target.Add(item); + } + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatHistory.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatHistory.cs index e15d46965de7..75377751f76e 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatHistory.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatHistory.cs @@ -3,6 +3,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; #pragma warning disable CA1033 // Interface methods should be callable by child types @@ -69,6 +70,27 @@ public void AddMessage(AuthorRole authorRole, string content, Encoding? encoding public void AddMessage(AuthorRole authorRole, ChatMessageContentItemCollection contentItems, Encoding? encoding = null, IReadOnlyDictionary? metadata = null) => this.Add(new ChatMessageContent(authorRole, contentItems, null, null, encoding, metadata)); + /// + /// Adds with content items./> + /// + /// Role of the message author + /// The content items. + [Experimental("SKEXP0001")] + public void AddMessage(AuthorRole authorRole, params KernelContent[] items) + { + Verify.NotNull(authorRole); + Verify.NotNull(items); + + var collection = new ChatMessageContentItemCollection(); + + foreach (KernelContent item in items) + { + collection.Add(item); + } + + this.AddMessage(authorRole, collection); + } + /// /// Add a user message to the chat history /// diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/ChatMessageContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/ChatMessageContent.cs index 685094399728..7ec196bdc92c 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/ChatMessageContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/ChatMessageContent.cs @@ -161,6 +161,16 @@ public ChatMessageContent( this._items = items; } + /// + /// Returns a list of function calls. + /// + /// The list of function calls. + [Experimental("SKEXP0001")] + public IEnumerable GetFunctionCalls() + { + return this.Items.OfType(); + } + /// public override string ToString() { diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs new file mode 100644 index 000000000000..ac855a473150 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel; + +/// +/// Represents a function call requested by LLM. +/// +[Experimental("SKEXP0001")] +public sealed class FunctionCallContent : KernelContent +{ + /// + /// The function call ID. + /// + public string? Id { get; private set; } + + /// + /// The plugin name. + /// + public string? PluginName { get; private set; } + + /// + /// The function name. + /// + public string FunctionName { get; private set; } + + /// + /// The kernel arguments. + /// + public KernelArguments? Arguments { get; private set; } + + /// + /// Gets the fully-qualified name of the function. + /// + /// The function name separator. + /// Fully-qualified name of the function. + public string GetFullyQualifiedName(string functionNameSeparator = "-") + { + return string.IsNullOrEmpty(this.PluginName) ? this.FunctionName : $"{this.PluginName}{functionNameSeparator}{this.FunctionName}"; + } + + /// + /// Creates a new instance of the class. + /// + /// The function name. + /// The plugin name. + /// The function call ID. + /// The function original arguments. + [JsonConstructor] + public FunctionCallContent(string functionName, string? pluginName = null, string? id = null, KernelArguments? arguments = null) + { + Verify.NotNull(functionName); + + this.FunctionName = functionName; + this.Id = id; + this.PluginName = pluginName; + this.Arguments = arguments; + } + + /// + /// Creates a new instance of the class. + /// + /// Fully-qualified name of the function. + /// The function call ID. + /// The function original arguments. + /// The function name separator. + public static FunctionCallContent Create(string fullyQualifiedName, string? id, KernelArguments? arguments = null, string functionNameSeparator = "-") + { + Verify.NotNull(fullyQualifiedName); + + string? pluginName = null; + string functionName = fullyQualifiedName; + + int separatorPos = fullyQualifiedName.IndexOf(functionNameSeparator, StringComparison.Ordinal); + if (separatorPos >= 0) + { + pluginName = fullyQualifiedName.AsSpan(0, separatorPos).Trim().ToString(); + functionName = fullyQualifiedName.AsSpan(separatorPos + functionNameSeparator.Length).Trim().ToString(); + } + + return new FunctionCallContent( + functionName: functionName, + pluginName: pluginName, + id: id, + arguments: arguments); + } + + /// + /// Invokes the function represented by the function call content type. + /// + /// The containing services, plugins, and other state for use throughout the operation. + /// The to monitor for cancellation requests. The default is . + /// The result of the function's execution. + public async Task InvokeAsync(Kernel kernel, CancellationToken cancellationToken = default) + { + Verify.NotNull(kernel, nameof(kernel)); + + if (kernel.Plugins.TryGetFunction(this.PluginName, this.FunctionName, out KernelFunction? function)) + { + var result = await function.InvokeAsync(kernel, this.Arguments, cancellationToken).ConfigureAwait(false); + + return new FunctionResultContent(this, result); + } + + throw new KeyNotFoundException($"The plugin collection does not contain a plugin and/or function with the specified names. Plugin name - '{this.PluginName}', function name - '{this.FunctionName}'."); + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs new file mode 100644 index 000000000000..a9447d816b0d --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel; + +/// +/// Represents the result of a function call. +/// +[Experimental("SKEXP0001")] +public sealed class FunctionResultContent : KernelContent +{ + /// + /// The function call ID. + /// + [JsonIgnore] + public string? Id => this.FunctionCall.Id; + + /// + /// The plugin name. + /// + [JsonIgnore] + public string? PluginName => this.FunctionCall.PluginName; + + /// + /// The function name. + /// + [JsonIgnore] + public string FunctionName => this.FunctionCall.FunctionName; + + /// + /// The result of the function call. + /// + public object? Result { get; set; } + + /// + /// The function call. + /// + public FunctionCallContent FunctionCall { get; private set; } + + /// + /// Creates a new instance of the class. + /// + /// The function call. + /// The function result. + [JsonConstructor] + public FunctionResultContent(FunctionCallContent functionCall, object? result = null) + { + Verify.NotNull(functionCall, nameof(functionCall)); + + this.FunctionCall = functionCall; + this.Result = result; + } + + /// + /// Creates a new instance of the class. + /// + /// The function call content. + /// The function result. + public FunctionResultContent(FunctionCallContent functionCallContent, FunctionResult result) : + this(functionCallContent, result.Value) + { + this.InnerContent = result; + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/KernelContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/KernelContent.cs index cc5d02a05c19..de6d035b90e1 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/KernelContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/KernelContent.cs @@ -16,6 +16,8 @@ namespace Microsoft.SemanticKernel; #pragma warning restore SKEXP0010 #pragma warning disable SKEXP0001 [JsonDerivedType(typeof(AudioContent), typeDiscriminator: nameof(AudioContent))] +[JsonDerivedType(typeof(FunctionCallContent), typeDiscriminator: nameof(FunctionCallContent))] +[JsonDerivedType(typeof(FunctionResultContent), typeDiscriminator: nameof(FunctionResultContent))] #pragma warning restore SKEXP0001 public abstract class KernelContent { diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs index 4f77ab473909..1f0b31192c2f 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs @@ -3,6 +3,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Text.Json.Serialization; #pragma warning disable CA1710 // Identifiers should have correct suffix @@ -21,6 +22,15 @@ public sealed class KernelArguments : IDictionary, IReadOnlyDic /// Dictionary of name/values for all the arguments in the instance. private readonly Dictionary _arguments; + /// + /// Initializes a new instance of the class with the specified AI execution settings. + /// + [JsonConstructor] + public KernelArguments() + { + this._arguments = new(StringComparer.OrdinalIgnoreCase); + } + /// /// Initializes a new instance of the class with the specified AI execution settings. /// diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTests.cs index eec8f6564cb2..df94aad6374c 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTests.cs @@ -39,4 +39,25 @@ public void ItCanBeSerializedAndDeserialized() chatHistoryDeserialized[i].Items.OfType().Single().Text); } } + + [Fact] + public void ItCanAddChatMessageContentWithItemsSpecifiedAsParameters() + { + // Arrange + var sut = new ChatHistory(); + + // Act + sut.AddMessage(AuthorRole.User, new TextContent("Hello"), new ImageContent(), new AudioContent()); + + // Assert + Assert.Single(sut); + + var chatMessage = sut[0]; + Assert.Equal(AuthorRole.User, chatMessage.Role); + + Assert.Equal(3, chatMessage.Items.Count); + Assert.IsType(chatMessage.Items[0]); + Assert.IsType(chatMessage.Items[1]); + Assert.IsType(chatMessage.Items[2]); + } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs index fb06327f4efb..4cf614f718ba 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs @@ -152,17 +152,20 @@ public void ItCanBeSerializeAndDeserialized() ["metadata-key-1"] = "metadata-value-1" }) { MimeType = "mime-type-1" }); + items.Add(new ImageContent(new Uri("https://fake-random-test-host:123"), "model-2", metadata: new Dictionary() { ["metadata-key-2"] = "metadata-value-2" }) { MimeType = "mime-type-2" }); + #pragma warning disable SKEXP0010 items.Add(new BinaryContent(new BinaryData(new[] { 1, 2, 3 }), "model-3", metadata: new Dictionary() { ["metadata-key-3"] = "metadata-value-3" }) { MimeType = "mime-type-3" }); + #pragma warning restore SKEXP0010 #pragma warning disable SKEXP0001 items.Add(new AudioContent(new BinaryData(new[] { 3, 2, 1 }), "model-4", metadata: new Dictionary() @@ -170,18 +173,24 @@ public void ItCanBeSerializeAndDeserialized() ["metadata-key-4"] = "metadata-value-4" }) { MimeType = "mime-type-4" }); + #pragma warning restore SKEXP0001 items.Add(new ImageContent(new BinaryData(new[] { 2, 1, 3 }), "model-5", metadata: new Dictionary() { ["metadata-key-5"] = "metadata-value-5" }) { MimeType = "mime-type-5" }); + items.Add(new TextContent("content-6", "model-6", metadata: new Dictionary() { ["metadata-key-6"] = "metadata-value-6" }) { MimeType = "mime-type-6" }); + items.Add(new FunctionCallContent("function-name", "plugin-name", "function-id", new KernelArguments { ["parameter"] = "argument" })); + + items.Add(new FunctionResultContent(new FunctionCallContent("function-name", "plugin-name", "function-id"), "function-result")); + var sut = new ChatMessageContent(AuthorRole.User, items: items, "message-model", metadata: new Dictionary() { ["message-metadata-key-1"] = "message-metadata-value-1" @@ -202,7 +211,7 @@ public void ItCanBeSerializeAndDeserialized() Assert.Equal("message-metadata-value-1", deserializedMessage.Metadata["message-metadata-key-1"]?.ToString()); Assert.NotNull(deserializedMessage?.Items); - Assert.Equal(6, deserializedMessage.Items.Count); + Assert.Equal(items.Count, deserializedMessage.Items.Count); var textContent = deserializedMessage.Items[0] as TextContent; Assert.NotNull(textContent); @@ -261,5 +270,43 @@ public void ItCanBeSerializeAndDeserialized() Assert.NotNull(textContent.Metadata); Assert.Single(textContent.Metadata); Assert.Equal("metadata-value-6", textContent.Metadata["metadata-key-6"]?.ToString()); + + var functionCallContent = deserializedMessage.Items[6] as FunctionCallContent; + Assert.NotNull(functionCallContent); + Assert.Equal("function-name", functionCallContent.FunctionName); + Assert.Equal("plugin-name", functionCallContent.PluginName); + Assert.Equal("function-id", functionCallContent.Id); + Assert.NotNull(functionCallContent.Arguments); + Assert.Single(functionCallContent.Arguments); + Assert.Equal("argument", functionCallContent.Arguments["parameter"]?.ToString()); + + var functionResultContent = deserializedMessage.Items[7] as FunctionResultContent; + Assert.NotNull(functionResultContent); + Assert.Equal("function-result", functionResultContent.Result?.ToString()); + Assert.Equal("function-name", functionResultContent.FunctionCall.FunctionName); + Assert.Equal("function-id", functionResultContent.FunctionCall.Id); + Assert.Equal("plugin-name", functionResultContent.FunctionCall.PluginName); + } + + [Fact] + public void GetFunctionCallsShouldReturnItemsOfFunctionCallContentType() + { + // Arrange + var items = new ChatMessageContentItemCollection(); + items.Add(new TextContent("content-1")); + items.Add(new FunctionCallContent("function-name", "plugin-name", "function-id", new KernelArguments { ["parameter"] = "argument" })); + items.Add(new FunctionCallContent("function-name-1", "plugin-name-1", "function-id-1", new KernelArguments { ["parameter-1"] = "argument-1" })); + items.Add(new FunctionResultContent(new FunctionCallContent("function-name", "plugin-name", "function-id"), "function-result")); + + var sut = new ChatMessageContent(AuthorRole.Tool, items); + + // Act + var functionCallContents = sut.GetFunctionCalls().ToArray(); + + // Assert + Assert.NotNull(functionCallContents); + Assert.Equal(2, functionCallContents.Length); + + Assert.True(functionCallContents.All(item => item is FunctionCallContent)); } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs new file mode 100644 index 000000000000..a0ed19c586d3 --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.SemanticKernel.Contents; + +public class FunctionCallContentTests +{ + private readonly KernelArguments _arguments; + + public FunctionCallContentTests() + { + this._arguments = []; + } + + [Fact] + public void ItShouldBeInitializedFromFunctionAndPluginName() + { + // Arrange & act + var sut = new FunctionCallContent("f1", "p1", "id", this._arguments); + + // Assert + Assert.Equal("f1", sut.FunctionName); + Assert.Equal("p1", sut.PluginName); + Assert.Equal("id", sut.Id); + Assert.Same(this._arguments, sut.Arguments); + } + + [Fact] + public void ItShouldBeCreatedFromFullyQualifiedNameThatHasPluginNameAndFunctionName() + { + // Arrange & act + var sut = FunctionCallContent.Create("p1.f1", "id", this._arguments, "."); + + // Assert + Assert.Equal("f1", sut.FunctionName); + Assert.Equal("p1", sut.PluginName); + Assert.Equal("id", sut.Id); + Assert.Same(this._arguments, sut.Arguments); + } + + [Fact] + public void ItShouldBeCreatedFromFullyQualifiedNameThatHasFunctionNameOnly() + { + // Arrange & act + var sut = FunctionCallContent.Create("f1", "id", this._arguments); + + // Assert + Assert.Equal("f1", sut.FunctionName); + Assert.Null(sut.PluginName); + Assert.Equal("id", sut.Id); + Assert.Same(this._arguments, sut.Arguments); + } + + [Fact] + public void ItShouldCreateFullyQualifiedName() + { + // Arrange + var sut = new FunctionCallContent("f1", "p1", "id", this._arguments); + + // Act + var fullyQualifiedName = sut.GetFullyQualifiedName("."); + + // Assert + Assert.Equal("p1.f1", fullyQualifiedName); + } + + [Fact] + public async Task ItShouldFindKernelFunctionAndInvokeItAsync() + { + // Arrange + var kernel = new Kernel(); + + KernelArguments? actualArguments = null; + + var function = KernelFunctionFactory.CreateFromMethod((KernelArguments args) => + { + actualArguments = args; + return "result"; + }, "f1"); + + kernel.Plugins.AddFromFunctions("p1", [function]); + + var sut = new FunctionCallContent("f1", "p1", "id", this._arguments); + + // Act + var resultContent = await sut.InvokeAsync(kernel); + + // Assert + Assert.NotNull(resultContent); + Assert.Equal("result", resultContent.Result); + Assert.Same(this._arguments, actualArguments); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs new file mode 100644 index 000000000000..97cd15b9e47e --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.UnitTests.Contents; +public class FunctionResultContentTests +{ + private readonly FunctionCallContent _callContent; + + public FunctionResultContentTests() + { + this._callContent = new FunctionCallContent("f1", "p1", "id", []); + } + + [Fact] + public void ItShouldHaveFunctionIdInitialized() + { + // Arrange & act + var sut = new FunctionResultContent(this._callContent, "result"); + + // Assert + Assert.Equal("id", sut.Id); + } + + [Fact] + public void ItShouldHavePluginNameInitialized() + { + // Arrange & act + var sut = new FunctionResultContent(this._callContent, "result"); + + // Assert + Assert.Equal("p1", sut.PluginName); + } + + [Fact] + public void ItShouldHaveFunctionNameInitialized() + { + // Arrange & act + var sut = new FunctionResultContent(this._callContent, "result"); + + // Assert + Assert.Equal("f1", sut.FunctionName); + } + + [Fact] + public void ItShouldHaveFunctionCallInitialized() + { + // Arrange & act + var sut = new FunctionResultContent(this._callContent, "result"); + + // Assert + Assert.Same(this._callContent, sut.FunctionCall); + } + + [Fact] + public void ItShouldHaveFunctionResultInitialized() + { + // Arrange & act + var sut = new FunctionResultContent(this._callContent, "result"); + + // Assert + Assert.Same("result", sut.Result); + } + + [Fact] + public void ItShouldHaveValueFromFunctionResultAsResultInitialized() + { + // Arrange & act + var function = KernelFunctionFactory.CreateFromMethod(() => { }); + + var functionResult = new FunctionResult(function, "result"); + + var sut = new FunctionResultContent(this._callContent, functionResult); + + // Assert + Assert.Equal("result", sut.Result); + } + + [Fact] + public void ItShouldBeSerializableAndDeserializable() + { + // Arrange + var sut = new FunctionResultContent(this._callContent, "result"); + + // Act + var json = JsonSerializer.Serialize(sut); + + var deserializedSut = JsonSerializer.Deserialize(json); + + // Assert + Assert.NotNull(deserializedSut); + Assert.Equal(sut.Id, deserializedSut.Id); + Assert.Equal(sut.PluginName, deserializedSut.PluginName); + Assert.Equal(sut.FunctionName, deserializedSut.FunctionName); + Assert.Equal(sut.Result, deserializedSut.Result?.ToString()); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/Utilities/IListExtensionsTests.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/IListExtensionsTests.cs new file mode 100644 index 000000000000..1a6934ef9b87 --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/IListExtensionsTests.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.UnitTests.Utilities; + +public class IListExtensionsTests +{ + [Fact] + public void ItShouldAddRangeOfElementsToTargetList() + { + // Arrange + IList target = []; + int[] source = [1, 2, 3]; + + // Act + target.AddRange(source); + + // Assert + Assert.Equal(3, target.Count); + Assert.Equal(1, target[0]); + Assert.Equal(2, target[1]); + Assert.Equal(3, target[2]); + } +} From 6a03050a2649c9bd10eeef434834ab9acb79670a Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 5 Apr 2024 23:17:36 +0100 Subject: [PATCH 02/90] Unnecessary warning suppressions removed --- .../Contents/ChatMessageContentTests.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs index 14f866ba5941..ab167969bdf1 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs @@ -166,22 +166,18 @@ public void ItCanBeSerializeAndDeserialized() }) { MimeType = "mime-type-2" }); -#pragma warning disable SKEXP0010 items.Add(new BinaryContent(new BinaryData(new[] { 1, 2, 3 }), "model-3", metadata: new Dictionary() { ["metadata-key-3"] = "metadata-value-3" }) { MimeType = "mime-type-3" }); -#pragma warning restore SKEXP0010 -#pragma warning disable SKEXP0001 items.Add(new AudioContent(new BinaryData(new[] { 3, 2, 1 }), "model-4", metadata: new Dictionary() { ["metadata-key-4"] = "metadata-value-4" }) { MimeType = "mime-type-4" }); -#pragma warning restore SKEXP0001 items.Add(new ImageContent(new BinaryData(new[] { 2, 1, 3 }), "model-5", metadata: new Dictionary() { ["metadata-key-5"] = "metadata-value-5" From a23f5e7f61f53d7911c0501b01c120594f208ef0 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 10:21:39 +0100 Subject: [PATCH 03/90] Fix funciton calling example --- .../Example59_OpenAIFunctionCalling.cs | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index 53c5453cc28e..7a3ca67129cd 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -73,30 +74,43 @@ public async Task RunAsync() var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - ChatMessageContent result = await chat.GetChatMessageContentAsync(chatHistory, settings, kernel); - chatHistory.Add(result); // Adding LLM response containing function calls(requests) to chat history as it's required by LLMs. - - IEnumerable functionCalls = result.GetFunctionCalls(); // Getting list of function calls. - - foreach (var functionCall in functionCalls) + while (true) { - try + ChatMessageContent result = await chat.GetChatMessageContentAsync(chatHistory, settings, kernel); + if (result.Content is not null) { - FunctionResultContent functionResult = await functionCall.InvokeAsync(kernel); // Executing each function. Can be done in parallel. + Write(result.Content); + } - chatHistory.AddMessage(AuthorRole.Tool, functionResult); // Adding function result to chat history. + IEnumerable functionCalls = result.GetFunctionCalls(); // Getting list of function calls. + + if (!functionCalls.Any()) + { + break; } - catch (Exception ex) + + chatHistory.Add(result); // Adding LLM response containing function calls(requests) to chat history as it's required by LLMs. + + foreach (var functionCall in functionCalls) { - chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(functionCall, ex)); // Adding exception to chat history. - // or - //string message = $"Error details that LLM can reason about."; - //chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(functionCall, message)); + try + { + FunctionResultContent functionResult = await functionCall.InvokeAsync(kernel); // Executing each function. Can be done in parallel. + + chatHistory.AddMessage(AuthorRole.Tool, functionResult); // Adding function result to chat history. + } + catch (Exception ex) + { + chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(functionCall, ex)); + // Adding exception to chat history. + // or + //string message = $"Error details that LLM can reason about."; + //chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(functionCall, message)); + } } - } - // Sending the functions invocation results to the LLM to get the final response. - WriteLine(await chat.GetChatMessageContentAsync(chatHistory, settings, kernel)); + WriteLine(); + } } /* Uncomment this to try in a console chat loop. From a05d88d0dd38b9f37fcb1b2e093caf28501085a7 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 12:06:20 +0100 Subject: [PATCH 04/90] Fix for unit tests --- .../AzureOpenAIChatCompletionServiceTests.cs | 16 ++++++++-------- .../OpenAIChatCompletionServiceTests.cs | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index 471881ec18b5..67d085b1dde1 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -786,9 +786,9 @@ public async Task FunctionCallsShouldBeReturnedToLLMAsync() var optionsJson = JsonSerializer.Deserialize(actualRequestContent); var messages = optionsJson.GetProperty("messages"); - Assert.Equal(2, messages.GetArrayLength()); + Assert.Equal(1, messages.GetArrayLength()); - var assistantMessage = messages[1]; + var assistantMessage = messages[0]; Assert.Equal("assistant", assistantMessage.GetProperty("role").GetString()); Assert.Equal(2, assistantMessage.GetProperty("tool_calls").GetArrayLength()); @@ -845,14 +845,14 @@ public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsyn var optionsJson = JsonSerializer.Deserialize(actualRequestContent); var messages = optionsJson.GetProperty("messages"); - Assert.Equal(3, messages.GetArrayLength()); + Assert.Equal(2, messages.GetArrayLength()); - var assistantMessage = messages[1]; + var assistantMessage = messages[0]; Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); - var assistantMessage2 = messages[2]; + var assistantMessage2 = messages[1]; Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); @@ -890,14 +890,14 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage var optionsJson = JsonSerializer.Deserialize(actualRequestContent); var messages = optionsJson.GetProperty("messages"); - Assert.Equal(3, messages.GetArrayLength()); + Assert.Equal(2, messages.GetArrayLength()); - var assistantMessage = messages[1]; + var assistantMessage = messages[0]; Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); - var assistantMessage2 = messages[2]; + var assistantMessage2 = messages[1]; Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs index b4686907a86c..f4782e0f8f10 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs @@ -409,9 +409,9 @@ public async Task FunctionCallsShouldBeReturnedToLLMAsync() var optionsJson = JsonSerializer.Deserialize(actualRequestContent); var messages = optionsJson.GetProperty("messages"); - Assert.Equal(2, messages.GetArrayLength()); + Assert.Equal(1, messages.GetArrayLength()); - var assistantMessage = messages[1]; + var assistantMessage = messages[0]; Assert.Equal("assistant", assistantMessage.GetProperty("role").GetString()); Assert.Equal(2, assistantMessage.GetProperty("tool_calls").GetArrayLength()); @@ -468,14 +468,14 @@ public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsyn var optionsJson = JsonSerializer.Deserialize(actualRequestContent); var messages = optionsJson.GetProperty("messages"); - Assert.Equal(3, messages.GetArrayLength()); + Assert.Equal(2, messages.GetArrayLength()); - var assistantMessage = messages[1]; + var assistantMessage = messages[0]; Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); - var assistantMessage2 = messages[2]; + var assistantMessage2 = messages[1]; Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); @@ -513,14 +513,14 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage var optionsJson = JsonSerializer.Deserialize(actualRequestContent); var messages = optionsJson.GetProperty("messages"); - Assert.Equal(3, messages.GetArrayLength()); + Assert.Equal(2, messages.GetArrayLength()); - var assistantMessage = messages[1]; + var assistantMessage = messages[0]; Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); - var assistantMessage2 = messages[2]; + var assistantMessage2 = messages[1]; Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); From 704111cd90a1ad891622520cd7ddb5fab57139a4 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 12:54:43 +0100 Subject: [PATCH 05/90] FunctionCallContent.FunctionCall property removed. --- .../Contents/FunctionCallContent.cs | 8 ++--- .../Contents/FunctionResultContent.cs | 35 ++++++++++++------- .../Contents/ChatMessageContentTests.cs | 6 ++-- .../Contents/FunctionResultContentTests.cs | 10 ------ 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs index ac855a473150..87cf62a6be58 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs @@ -18,22 +18,22 @@ public sealed class FunctionCallContent : KernelContent /// /// The function call ID. /// - public string? Id { get; private set; } + public string? Id { get; } /// /// The plugin name. /// - public string? PluginName { get; private set; } + public string? PluginName { get; } /// /// The function name. /// - public string FunctionName { get; private set; } + public string FunctionName { get; } /// /// The kernel arguments. /// - public KernelArguments? Arguments { get; private set; } + public KernelArguments? Arguments { get; } /// /// Gets the fully-qualified name of the function. diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs index a9447d816b0d..a0edb1171e4d 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs @@ -14,42 +14,51 @@ public sealed class FunctionResultContent : KernelContent /// /// The function call ID. /// - [JsonIgnore] - public string? Id => this.FunctionCall.Id; + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Id { get; } /// /// The plugin name. /// - [JsonIgnore] - public string? PluginName => this.FunctionCall.PluginName; - + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? PluginName { get; } /// /// The function name. /// - [JsonIgnore] - public string FunctionName => this.FunctionCall.FunctionName; + public string FunctionName { get; } /// /// The result of the function call. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public object? Result { get; set; } /// - /// The function call. + /// Creates a new instance of the class. /// - public FunctionCallContent FunctionCall { get; private set; } + /// The function name. + /// The plugin name. + /// The function call ID. + /// The function result. + [JsonConstructor] + public FunctionResultContent(string functionName, string? pluginName = null, string? id = null, object? result = null) + { + this.FunctionName = functionName; + this.PluginName = pluginName; + this.Id = id; + this.Result = result; + } /// /// Creates a new instance of the class. /// /// The function call. /// The function result. - [JsonConstructor] public FunctionResultContent(FunctionCallContent functionCall, object? result = null) { - Verify.NotNull(functionCall, nameof(functionCall)); - - this.FunctionCall = functionCall; + this.Id = functionCall.Id; + this.PluginName = functionCall.PluginName; + this.FunctionName = functionCall.FunctionName; this.Result = result; } diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs index ab167969bdf1..a985055ea742 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs @@ -286,9 +286,9 @@ public void ItCanBeSerializeAndDeserialized() var functionResultContent = deserializedMessage.Items[7] as FunctionResultContent; Assert.NotNull(functionResultContent); Assert.Equal("function-result", functionResultContent.Result?.ToString()); - Assert.Equal("function-name", functionResultContent.FunctionCall.FunctionName); - Assert.Equal("function-id", functionResultContent.FunctionCall.Id); - Assert.Equal("plugin-name", functionResultContent.FunctionCall.PluginName); + Assert.Equal("function-name", functionResultContent.FunctionName); + Assert.Equal("function-id", functionResultContent.Id); + Assert.Equal("plugin-name", functionResultContent.PluginName); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs index 97cd15b9e47e..aaef8340be90 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs @@ -44,16 +44,6 @@ public void ItShouldHaveFunctionNameInitialized() Assert.Equal("f1", sut.FunctionName); } - [Fact] - public void ItShouldHaveFunctionCallInitialized() - { - // Arrange & act - var sut = new FunctionResultContent(this._callContent, "result"); - - // Assert - Assert.Same(this._callContent, sut.FunctionCall); - } - [Fact] public void ItShouldHaveFunctionResultInitialized() { From 3267573d2f3a3b9743b64f90730fe44bb3e1c029 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 14:48:52 +0100 Subject: [PATCH 06/90] Adding FunctionResultContent items to the chat message for auto function calling. --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 33 ++++++++++++------- .../Connectors/OpenAI/OpenAIToolsTests.cs | 2 +- .../Contents/FunctionCallContent.cs | 2 +- .../Contents/FunctionResultContent.cs | 29 ++++++++++++++++ .../Contents/FunctionResultContentTests.cs | 26 +++++++++++++++ 5 files changed, 79 insertions(+), 13 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 06f82c45eee9..7a300c3255d1 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -369,7 +369,7 @@ internal async Task> GetChatMessageContentsAsy // 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) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Tool call was not a function call.", toolCall.Id, this.Logger); + AddResponseMessage(chatOptions, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); continue; } @@ -381,7 +381,7 @@ internal async Task> GetChatMessageContentsAsy } catch (JsonException) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall.Id, this.Logger); + AddResponseMessage(chatOptions, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); continue; } @@ -391,14 +391,14 @@ internal async Task> GetChatMessageContentsAsy 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.Id, this.Logger); + AddResponseMessage(chatOptions, 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.Id, this.Logger); + AddResponseMessage(chatOptions, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); continue; } @@ -416,7 +416,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.Id, this.Logger); + AddResponseMessage(chatOptions, chat, null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); continue; } finally @@ -426,21 +426,32 @@ internal async Task> GetChatMessageContentsAsy var stringResult = ProcessFunctionResult(functionResult, chatExecutionSettings.ToolCallBehavior); - AddResponseMessage(chatOptions, chat, stringResult, errorMessage: null, toolCall.Id, this.Logger); + AddResponseMessage(chatOptions, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); - static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory chat, string? result, string? errorMessage, string toolId, ILogger logger) + static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory chat, string? result, string? errorMessage, ChatCompletionsToolCall toolCall, ILogger logger) { // Log any error if (errorMessage is not null && logger.IsEnabled(LogLevel.Debug)) { Debug.Assert(result is null); - logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolId, errorMessage); + logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolCall.Id, errorMessage); } - // Add the tool response message to both the chat options and to the chat history. + // Add the tool response message to the chat options result ??= errorMessage ?? string.Empty; - chatOptions.Messages.Add(new ChatRequestToolMessage(result, toolId)); - chat.AddMessage(AuthorRole.Tool, result, metadata: new Dictionary { { OpenAIChatMessageContent.ToolIdProperty, toolId } }); + chatOptions.Messages.Add(new ChatRequestToolMessage(result, toolCall.Id)); + + // Add the tool response message to the chat history. + var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); + + if (toolCall is ChatCompletionsFunctionToolCall functionCall) + { + // 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. + message.Items.Add(FunctionResultContent.Create(functionCall.Name, functionCall.Id, result)); + } + + chat.Add(message); } } diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index d589093ee710..f8a7a2780a85 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -312,7 +312,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc // Assert Assert.NotNull(messageContent.Content); - var failureWords = new List() { "error", "unable", "couldn", "issue", "trouble" }; + var failureWords = new List() { "error", "unable", "couldn", "issue", "trouble", "difficulties" }; Assert.Contains(failureWords, word => messageContent.Content.Contains(word, StringComparison.InvariantCultureIgnoreCase)); } diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs index 87cf62a6be58..6b3f0f95ec67 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs @@ -70,7 +70,7 @@ public FunctionCallContent(string functionName, string? pluginName = null, strin /// The function call ID. /// The function original arguments. /// The function name separator. - public static FunctionCallContent Create(string fullyQualifiedName, string? id, KernelArguments? arguments = null, string functionNameSeparator = "-") + public static FunctionCallContent Create(string fullyQualifiedName, string? id = null, KernelArguments? arguments = null, string functionNameSeparator = "-") { Verify.NotNull(fullyQualifiedName); diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs index a0edb1171e4d..e5ad1a7d58a4 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; @@ -72,4 +73,32 @@ public FunctionResultContent(FunctionCallContent functionCallContent, FunctionRe { this.InnerContent = result; } + + /// + /// Creates a new instance of the class. + /// + /// Fully-qualified name of the function. + /// The function call ID. + /// The function result. + /// The function name separator. + public static FunctionResultContent Create(string fullyQualifiedName, string? id = null, object? result = null, string functionNameSeparator = "-") + { + Verify.NotNull(fullyQualifiedName); + + string? pluginName = null; + string functionName = fullyQualifiedName; + + int separatorPos = fullyQualifiedName.IndexOf(functionNameSeparator, StringComparison.Ordinal); + if (separatorPos >= 0) + { + pluginName = fullyQualifiedName.AsSpan(0, separatorPos).Trim().ToString(); + functionName = fullyQualifiedName.AsSpan(separatorPos + functionNameSeparator.Length).Trim().ToString(); + } + + return new FunctionResultContent( + functionName: functionName, + pluginName: pluginName, + id: id, + result: result); + } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs index aaef8340be90..49361dd23bdb 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs @@ -86,4 +86,30 @@ public void ItShouldBeSerializableAndDeserializable() Assert.Equal(sut.FunctionName, deserializedSut.FunctionName); Assert.Equal(sut.Result, deserializedSut.Result?.ToString()); } + + [Fact] + public void ItShouldBeCreatedFromFullyQualifiedNameThatHasPluginNameAndFunctionName() + { + // Arrange & act + var sut = FunctionResultContent.Create("p1.f1", "id", "result", "."); + + // Assert + Assert.Equal("f1", sut.FunctionName); + Assert.Equal("p1", sut.PluginName); + Assert.Equal("id", sut.Id); + Assert.Equal("result", sut.Result); + } + + [Fact] + public void ItShouldBeCreatedFromFullyQualifiedNameThatHasFunctionNameOnly() + { + // Arrange & act + var sut = FunctionResultContent.Create("f1", "id", "result"); + + // Assert + Assert.Equal("f1", sut.FunctionName); + Assert.Null(sut.PluginName); + Assert.Equal("id", sut.Id); + Assert.Equal("result", sut.Result); + } } From 79df6882da83ba718d883f187ca1352c160a08a3 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:50:09 +0100 Subject: [PATCH 07/90] Update dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs Co-authored-by: Stephen Toub --- dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 7a300c3255d1..a8d7b872aa35 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1018,7 +1018,7 @@ private static IEnumerable GetRequestMessages(ChatMessageCon return toolMessages; } - throw new NotSupportedException("No function result provided in the tool massage."); + throw new NotSupportedException("No function result provided in the tool message."); } if (message.Role == AuthorRole.User) From 88b1f03166f0d9733c27d0e89f3a85898d4981bd Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:50:36 +0100 Subject: [PATCH 08/90] Update dotnet/src/InternalUtilities/src/System/IListExtensions.cs Co-authored-by: Stephen Toub --- .../InternalUtilities/src/System/IListExtensions.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dotnet/src/InternalUtilities/src/System/IListExtensions.cs b/dotnet/src/InternalUtilities/src/System/IListExtensions.cs index c1e3c3a3dbc1..fda33829db23 100644 --- a/dotnet/src/InternalUtilities/src/System/IListExtensions.cs +++ b/dotnet/src/InternalUtilities/src/System/IListExtensions.cs @@ -27,9 +27,16 @@ internal static void AddRange(this IList target, IEnumerable source) throw new ArgumentNullException(nameof(source)); } - foreach (var item in source) + if (target is List list) { - target.Add(item); + list.AddRange(source); + } + else + { + foreach (var item in source) + { + target.Add(item); + } } } } From e5c327bf5ed7f25b71b3c4cec99b7ccc0d01f9fc Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 8 Apr 2024 17:51:12 +0100 Subject: [PATCH 09/90] Update dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs Co-authored-by: Stephen Toub --- .../KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index 7a3ca67129cd..1da0c4f90e87 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -104,7 +104,7 @@ public async Task RunAsync() chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(functionCall, ex)); // Adding exception to chat history. // or - //string message = $"Error details that LLM can reason about."; + //string message = "Error details that LLM can reason about."; //chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(functionCall, message)); } } From b7322567b00e7d3294e11df2df29369a715a5246 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 8 Apr 2024 18:00:01 +0100 Subject: [PATCH 10/90] Update dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs Co-authored-by: Stephen Toub --- .../Contents/FunctionResultContent.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs index e5ad1a7d58a4..4da61f172a51 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs @@ -23,6 +23,7 @@ public sealed class FunctionResultContent : KernelContent /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? PluginName { get; } + /// /// The function name. /// From 8823ebd2d7ac76a9a233ac27a60d9b0acb7bbc5c Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 18:01:41 +0100 Subject: [PATCH 11/90] A few minor improvments. --- .../KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs | 2 +- .../Contents/FunctionResultContent.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index 1da0c4f90e87..cc5bf0c2140a 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -95,7 +95,7 @@ public async Task RunAsync() { try { - FunctionResultContent functionResult = await functionCall.InvokeAsync(kernel); // Executing each function. Can be done in parallel. + FunctionResultContent functionResult = await functionCall.InvokeAsync(kernel); // Executing each function. chatHistory.AddMessage(AuthorRole.Tool, functionResult); // Adding function result to chat history. } diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs index e5ad1a7d58a4..f9ab278729d1 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs @@ -29,7 +29,7 @@ public sealed class FunctionResultContent : KernelContent public string FunctionName { get; } /// - /// The result of the function call. + /// The result of the function call, the function invocation exception or the custom error message. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public object? Result { get; set; } From ddbb7c2fc408d2162976d64f235cec16b8b5af02 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:03:25 +0100 Subject: [PATCH 12/90] Update dotnet/src/InternalUtilities/src/System/IListExtensions.cs Co-authored-by: Stephen Toub --- .../InternalUtilities/src/System/IListExtensions.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/dotnet/src/InternalUtilities/src/System/IListExtensions.cs b/dotnet/src/InternalUtilities/src/System/IListExtensions.cs index fda33829db23..4a79cc73a422 100644 --- a/dotnet/src/InternalUtilities/src/System/IListExtensions.cs +++ b/dotnet/src/InternalUtilities/src/System/IListExtensions.cs @@ -17,15 +17,8 @@ internal static class IListExtensions /// The source containing elements to add to the target . internal static void AddRange(this IList target, IEnumerable source) { - if (target is null) - { - throw new ArgumentNullException(nameof(target)); - } - - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } + Debug.Assert(target is not null); + Debug.Assert(source is not null); if (target is List list) { From 8569dc4454a76f3a428409bba491ef111748de34 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 20:05:24 +0100 Subject: [PATCH 13/90] Debug.Assert instead of null checks. --- dotnet/src/InternalUtilities/src/System/IListExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/InternalUtilities/src/System/IListExtensions.cs b/dotnet/src/InternalUtilities/src/System/IListExtensions.cs index 4a79cc73a422..7b5e73ae062d 100644 --- a/dotnet/src/InternalUtilities/src/System/IListExtensions.cs +++ b/dotnet/src/InternalUtilities/src/System/IListExtensions.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace Microsoft.SemanticKernel; @@ -26,9 +26,9 @@ internal static void AddRange(this IList target, IEnumerable source) } else { - foreach (var item in source) + foreach (var item in source!) { - target.Add(item); + target!.Add(item); } } } From 145968f0d44d3afdeeae6324be2c092bed163238 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 20:18:51 +0100 Subject: [PATCH 14/90] New ChatHsitory.AddMessage method removed. --- .../Example59_OpenAIFunctionCalling.cs | 6 ++--- .../Connectors/OpenAI/OpenAIToolsTests.cs | 8 +++---- .../AI/ChatCompletion/ChatHistory.cs | 22 ------------------- .../AI/ChatCompletion/ChatHistoryTests.cs | 21 ------------------ 4 files changed, 7 insertions(+), 50 deletions(-) diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index cc5bf0c2140a..69092fedbba4 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -97,15 +97,15 @@ public async Task RunAsync() { FunctionResultContent functionResult = await functionCall.InvokeAsync(kernel); // Executing each function. - chatHistory.AddMessage(AuthorRole.Tool, functionResult); // Adding function result to chat history. + chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { functionResult }); // Adding function result to chat history. } catch (Exception ex) { - chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(functionCall, ex)); + chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionResultContent(functionCall, ex) }); // Adding exception to chat history. // or //string message = "Error details that LLM can reason about."; - //chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(functionCall, message)); + //chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionResultContent(functionCall, message) }); } } diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index f8a7a2780a85..586a7bf7df2a 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -260,7 +260,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManual { var result = await functionCall.InvokeAsync(kernel); - chatHistory.AddMessage(AuthorRole.Tool, result); + chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { result }); } // Sending the functions invocation results to the LLM to get the final response @@ -301,7 +301,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc // Simulating an exception var exception = new OperationCanceledException("The operation was canceled due to timeout."); - chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(functionCall, exception)); + chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionResultContent(functionCall, exception) }); } // Sending the functions execution results back to the LLM to get the final response @@ -344,7 +344,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu { var result = await functionCall.InvokeAsync(kernel); - chatHistory.AddMessage(AuthorRole.Tool, result); + chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { result }); } // Adding a simulated function call to the connector response message @@ -353,7 +353,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu // Adding a simulated function result to chat history var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; - chatHistory.AddMessage(AuthorRole.Tool, new FunctionResultContent(simulatedFunctionCall, simulatedFunctionResult)); + chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionResultContent(simulatedFunctionCall, simulatedFunctionResult) }); // Sending the functions invocation results back to the LLM to get the final response messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatHistory.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatHistory.cs index 75377751f76e..e15d46965de7 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatHistory.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatHistory.cs @@ -3,7 +3,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Text; #pragma warning disable CA1033 // Interface methods should be callable by child types @@ -70,27 +69,6 @@ public void AddMessage(AuthorRole authorRole, string content, Encoding? encoding public void AddMessage(AuthorRole authorRole, ChatMessageContentItemCollection contentItems, Encoding? encoding = null, IReadOnlyDictionary? metadata = null) => this.Add(new ChatMessageContent(authorRole, contentItems, null, null, encoding, metadata)); - /// - /// Adds with content items./> - /// - /// Role of the message author - /// The content items. - [Experimental("SKEXP0001")] - public void AddMessage(AuthorRole authorRole, params KernelContent[] items) - { - Verify.NotNull(authorRole); - Verify.NotNull(items); - - var collection = new ChatMessageContentItemCollection(); - - foreach (KernelContent item in items) - { - collection.Add(item); - } - - this.AddMessage(authorRole, collection); - } - /// /// Add a user message to the chat history /// diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTests.cs index 27f04df7bb3a..5dee7afa14fd 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTests.cs @@ -43,25 +43,4 @@ public void ItCanBeSerializedAndDeserialized() chatHistoryDeserialized[i].Items.OfType().Single().Text); } } - - [Fact] - public void ItCanAddChatMessageContentWithItemsSpecifiedAsParameters() - { - // Arrange - var sut = new ChatHistory(); - - // Act - sut.AddMessage(AuthorRole.User, new TextContent("Hello"), new ImageContent(), new AudioContent()); - - // Assert - Assert.Single(sut); - - var chatMessage = sut[0]; - Assert.Equal(AuthorRole.User, chatMessage.Role); - - Assert.Equal(3, chatMessage.Items.Count); - Assert.IsType(chatMessage.Items[0]); - Assert.IsType(chatMessage.Items[1]); - Assert.IsType(chatMessage.Items[2]); - } } From b996eb4b9353723854ffc5ad944bdce13a2ffad2 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 20:22:29 +0100 Subject: [PATCH 15/90] New ChatMessageContent.GetFunctionCalls method removed. --- .../Example59_OpenAIFunctionCalling.cs | 3 +-- .../Connectors/OpenAI/OpenAIToolsTests.cs | 10 ++++----- .../Contents/ChatMessageContent.cs | 10 --------- .../Contents/ChatMessageContentTests.cs | 22 ------------------- 4 files changed, 6 insertions(+), 39 deletions(-) diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index 69092fedbba4..6520f84bbb1e 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -82,8 +82,7 @@ public async Task RunAsync() Write(result.Content); } - IEnumerable functionCalls = result.GetFunctionCalls(); // Getting list of function calls. - + IEnumerable functionCalls = result.Items.OfType(); // Getting list of function calls. if (!functionCalls.Any()) { break; diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index 586a7bf7df2a..351ce876e729 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -248,7 +248,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManual // Act var messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - var functionCalls = messageContent.GetFunctionCalls().ToArray(); + var functionCalls = messageContent.Items.OfType().ToArray(); while (functionCalls.Length != 0) { @@ -265,7 +265,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManual // Sending the functions invocation results to the LLM to get the final response messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = messageContent.GetFunctionCalls().ToArray(); + functionCalls = messageContent.Items.OfType().ToArray(); } // Assert @@ -288,7 +288,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc // Act var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - var functionCalls = messageContent.GetFunctionCalls().ToArray(); + var functionCalls = messageContent.Items.OfType().ToArray(); while (functionCalls.Length != 0) { @@ -306,7 +306,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc // Sending the functions execution results back to the LLM to get the final response messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = messageContent.GetFunctionCalls().ToArray(); + functionCalls = messageContent.Items.OfType().ToArray(); } // Assert @@ -332,7 +332,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu // Act var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - var functionCalls = messageContent.GetFunctionCalls().ToArray(); + var functionCalls = messageContent.Items.OfType().ToArray(); while (functionCalls.Length > 0) { diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/ChatMessageContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/ChatMessageContent.cs index 84b17d0b2b18..3faea8825a2f 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/ChatMessageContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/ChatMessageContent.cs @@ -169,16 +169,6 @@ public ChatMessageContent( this._items = items; } - /// - /// Returns a list of function calls. - /// - /// The list of function calls. - [Experimental("SKEXP0001")] - public IEnumerable GetFunctionCalls() - { - return this.Items.OfType(); - } - /// public override string ToString() { diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs index a985055ea742..22b1db2bae76 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs @@ -290,26 +290,4 @@ public void ItCanBeSerializeAndDeserialized() Assert.Equal("function-id", functionResultContent.Id); Assert.Equal("plugin-name", functionResultContent.PluginName); } - - [Fact] - public void GetFunctionCallsShouldReturnItemsOfFunctionCallContentType() - { - // Arrange - var items = new ChatMessageContentItemCollection(); - items.Add(new TextContent("content-1")); - items.Add(new FunctionCallContent("function-name", "plugin-name", "function-id", new KernelArguments { ["parameter"] = "argument" })); - items.Add(new FunctionCallContent("function-name-1", "plugin-name-1", "function-id-1", new KernelArguments { ["parameter-1"] = "argument-1" })); - items.Add(new FunctionResultContent(new FunctionCallContent("function-name", "plugin-name", "function-id"), "function-result")); - - var sut = new ChatMessageContent(AuthorRole.Tool, items); - - // Act - var functionCallContents = sut.GetFunctionCalls().ToArray(); - - // Assert - Assert.NotNull(functionCallContents); - Assert.Equal(2, functionCallContents.Length); - - Assert.True(functionCallContents.All(item => item is FunctionCallContent)); - } } From 28c8ac37b9c07acdc2a4ea13c57927905f051615 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 21:19:21 +0100 Subject: [PATCH 16/90] The fully qualified name parsing logic is encapsulated in the FullyQualifiedFunctionName utility class. --- dotnet/SK-dotnet.sln | 15 ++++-- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 12 +++-- .../Functions/FullyQualifiedFunctionName.cs | 52 +++++++++++++++++++ .../Contents/FunctionCallContent.cs | 29 ----------- .../Contents/FunctionResultContent.cs | 29 ----------- .../Contents/FunctionCallContentTests.cs | 26 ---------- .../Contents/FunctionResultContentTests.cs | 26 ---------- .../FullyQualifiedFunctionNameTests.cs | 30 +++++++++++ 8 files changed, 100 insertions(+), 119 deletions(-) create mode 100644 dotnet/src/InternalUtilities/src/Functions/FullyQualifiedFunctionName.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Utilities/FullyQualifiedFunctionNameTests.cs diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index b50ed86f9ca9..084a8534712a 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -90,9 +90,9 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5C246969-D794-4EC3-8E8F-F90D4D166420}" ProjectSection(SolutionItems) = preProject src\InternalUtilities\test\AssertExtensions.cs = src\InternalUtilities\test\AssertExtensions.cs - src\InternalUtilities\test\TestInternalUtilities.props = src\InternalUtilities\test\TestInternalUtilities.props src\InternalUtilities\test\HttpMessageHandlerStub.cs = src\InternalUtilities\test\HttpMessageHandlerStub.cs src\InternalUtilities\test\MultipleHttpMessageHandlerStub.cs = src\InternalUtilities\test\MultipleHttpMessageHandlerStub.cs + src\InternalUtilities\test\TestInternalUtilities.props = src\InternalUtilities\test\TestInternalUtilities.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{958AD708-F048-4FAF-94ED-D2F2B92748B9}" @@ -218,10 +218,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Text", "Text", "{EB2C141A-A ProjectSection(SolutionItems) = preProject src\InternalUtilities\src\Text\JsonOptionsCache.cs = src\InternalUtilities\src\Text\JsonOptionsCache.cs src\InternalUtilities\src\Text\ReadOnlyMemoryConverter.cs = src\InternalUtilities\src\Text\ReadOnlyMemoryConverter.cs + src\InternalUtilities\src\Text\SseData.cs = src\InternalUtilities\src\Text\SseData.cs src\InternalUtilities\src\Text\SseJsonParser.cs = src\InternalUtilities\src\Text\SseJsonParser.cs src\InternalUtilities\src\Text\SseLine.cs = src\InternalUtilities\src\Text\SseLine.cs src\InternalUtilities\src\Text\SseReader.cs = src\InternalUtilities\src\Text\SseReader.cs - src\InternalUtilities\src\Text\SseData.cs = src\InternalUtilities\src\Text\SseData.cs EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linq", "Linq", "{607DD6FA-FA0D-45E6-80BA-22A373609E89}" @@ -233,9 +233,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AzureAISearch.Un EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.HuggingFace.UnitTests", "src\Connectors\Connectors.HuggingFace.UnitTests\Connectors.HuggingFace.UnitTests.csproj", "{1F96837A-61EC-4C8F-904A-07BEBD05FDEE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.Google", "src\Connectors\Connectors.Google\Connectors.Google.csproj", "{6578D31B-2CF3-4FF4-A845-7A0412FEB42E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Google", "src\Connectors\Connectors.Google\Connectors.Google.csproj", "{6578D31B-2CF3-4FF4-A845-7A0412FEB42E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.Google.UnitTests", "src\Connectors\Connectors.Google.UnitTests\Connectors.Google.UnitTests.csproj", "{648CF4FE-4AFC-4EB0-87DB-9C2FE935CA24}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Google.UnitTests", "src\Connectors\Connectors.Google.UnitTests\Connectors.Google.UnitTests.csproj", "{648CF4FE-4AFC-4EB0-87DB-9C2FE935CA24}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HomeAutomation", "samples\HomeAutomation\HomeAutomation.csproj", "{13429BD6-4C4E-45EC-81AD-30BAC380AA60}" EndProject @@ -243,6 +243,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuggingFaceImageTextExample EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Onnx.UnitTests", "src\Connectors\Connectors.Onnx.UnitTests\Connectors.Onnx.UnitTests.csproj", "{D06465FA-0308-494C-920B-D502DA5690CB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Functions", "Functions", "{4DFB3897-0319-4DF2-BCFE-E6E0648297D2}" + ProjectSection(SolutionItems) = preProject + src\InternalUtilities\src\Functions\FullyQualifiedFunctionName.cs = src\InternalUtilities\src\Functions\FullyQualifiedFunctionName.cs + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -653,6 +658,8 @@ Global {13429BD6-4C4E-45EC-81AD-30BAC380AA60} = {FA3720F1-C99A-49B2-9577-A940257098BF} {8EE10EB0-A947-49CC-BCC1-18D93415B9E4} = {FA3720F1-C99A-49B2-9577-A940257098BF} {D06465FA-0308-494C-920B-D502DA5690CB} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} + {4DFB3897-0319-4DF2-BCFE-E6E0648297D2} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} + {328BE2F3-4BFD-490D-B813-C8F054552FF2} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index a8d7b872aa35..c9eb3c1b0d2d 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -448,7 +448,8 @@ static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory c { // 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. - message.Items.Add(FunctionResultContent.Create(functionCall.Name, functionCall.Id, result)); + var fqn = FullyQualifiedFunctionName.Parse(functionCall.Name, OpenAIFunction.NameSeparator); + message.Items.Add(new FunctionResultContent(fqn.FunctionName, fqn.PluginName, functionCall.Id, result)); } chat.Add(message); @@ -1148,11 +1149,12 @@ private OpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompl // The original arguments and function tool call will be available via the 'InnerContent' property for the connector caller to access. } - var content = FunctionCallContent.Create( - fullyQualifiedName: functionToolCall.Name, + var fqn = FullyQualifiedFunctionName.Parse(functionToolCall.Name, OpenAIFunction.NameSeparator); + var content = new FunctionCallContent( + functionName: fqn.FunctionName, + pluginName: fqn.PluginName, id: functionToolCall.Id, - arguments: arguments, - functionNameSeparator: OpenAIFunction.NameSeparator); + arguments: arguments); content.InnerContent = functionToolCall; message.Items.Add(content); diff --git a/dotnet/src/InternalUtilities/src/Functions/FullyQualifiedFunctionName.cs b/dotnet/src/InternalUtilities/src/Functions/FullyQualifiedFunctionName.cs new file mode 100644 index 000000000000..190d70e49583 --- /dev/null +++ b/dotnet/src/InternalUtilities/src/Functions/FullyQualifiedFunctionName.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace Microsoft.SemanticKernel; +internal sealed class FullyQualifiedFunctionName +{ + /// + /// The plugin name. + /// + public string? PluginName { get; } + + /// + /// The function name. + /// + public string FunctionName { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The function name. + /// The plugin name. + public FullyQualifiedFunctionName(string functionName, string? pluginName = null) + { + Verify.NotNull(functionName); + + this.FunctionName = functionName; + this.PluginName = pluginName; + } + + /// + /// Creates a new instance of the class. + /// + /// Fully-qualified name of the function. + /// The function name separator. + public static FullyQualifiedFunctionName Parse(string fullyQualifiedName, string functionNameSeparator = "-") + { + Verify.NotNull(fullyQualifiedName); + + string? pluginName = null; + string functionName = fullyQualifiedName; + + int separatorPos = fullyQualifiedName.IndexOf(functionNameSeparator, StringComparison.Ordinal); + if (separatorPos >= 0) + { + pluginName = fullyQualifiedName.AsSpan(0, separatorPos).Trim().ToString(); + functionName = fullyQualifiedName.AsSpan(separatorPos + functionNameSeparator.Length).Trim().ToString(); + } + + return new FullyQualifiedFunctionName(functionName: functionName, pluginName: pluginName); + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs index 6b3f0f95ec67..639423035da5 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; @@ -63,34 +62,6 @@ public FunctionCallContent(string functionName, string? pluginName = null, strin this.Arguments = arguments; } - /// - /// Creates a new instance of the class. - /// - /// Fully-qualified name of the function. - /// The function call ID. - /// The function original arguments. - /// The function name separator. - public static FunctionCallContent Create(string fullyQualifiedName, string? id = null, KernelArguments? arguments = null, string functionNameSeparator = "-") - { - Verify.NotNull(fullyQualifiedName); - - string? pluginName = null; - string functionName = fullyQualifiedName; - - int separatorPos = fullyQualifiedName.IndexOf(functionNameSeparator, StringComparison.Ordinal); - if (separatorPos >= 0) - { - pluginName = fullyQualifiedName.AsSpan(0, separatorPos).Trim().ToString(); - functionName = fullyQualifiedName.AsSpan(separatorPos + functionNameSeparator.Length).Trim().ToString(); - } - - return new FunctionCallContent( - functionName: functionName, - pluginName: pluginName, - id: id, - arguments: arguments); - } - /// /// Invokes the function represented by the function call content type. /// diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs index 97cac1de3a62..e8ab49eddf25 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; @@ -74,32 +73,4 @@ public FunctionResultContent(FunctionCallContent functionCallContent, FunctionRe { this.InnerContent = result; } - - /// - /// Creates a new instance of the class. - /// - /// Fully-qualified name of the function. - /// The function call ID. - /// The function result. - /// The function name separator. - public static FunctionResultContent Create(string fullyQualifiedName, string? id = null, object? result = null, string functionNameSeparator = "-") - { - Verify.NotNull(fullyQualifiedName); - - string? pluginName = null; - string functionName = fullyQualifiedName; - - int separatorPos = fullyQualifiedName.IndexOf(functionNameSeparator, StringComparison.Ordinal); - if (separatorPos >= 0) - { - pluginName = fullyQualifiedName.AsSpan(0, separatorPos).Trim().ToString(); - functionName = fullyQualifiedName.AsSpan(separatorPos + functionNameSeparator.Length).Trim().ToString(); - } - - return new FunctionResultContent( - functionName: functionName, - pluginName: pluginName, - id: id, - result: result); - } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs index a0ed19c586d3..6498699017d8 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs @@ -27,32 +27,6 @@ public void ItShouldBeInitializedFromFunctionAndPluginName() Assert.Same(this._arguments, sut.Arguments); } - [Fact] - public void ItShouldBeCreatedFromFullyQualifiedNameThatHasPluginNameAndFunctionName() - { - // Arrange & act - var sut = FunctionCallContent.Create("p1.f1", "id", this._arguments, "."); - - // Assert - Assert.Equal("f1", sut.FunctionName); - Assert.Equal("p1", sut.PluginName); - Assert.Equal("id", sut.Id); - Assert.Same(this._arguments, sut.Arguments); - } - - [Fact] - public void ItShouldBeCreatedFromFullyQualifiedNameThatHasFunctionNameOnly() - { - // Arrange & act - var sut = FunctionCallContent.Create("f1", "id", this._arguments); - - // Assert - Assert.Equal("f1", sut.FunctionName); - Assert.Null(sut.PluginName); - Assert.Equal("id", sut.Id); - Assert.Same(this._arguments, sut.Arguments); - } - [Fact] public void ItShouldCreateFullyQualifiedName() { diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs index 49361dd23bdb..aaef8340be90 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs @@ -86,30 +86,4 @@ public void ItShouldBeSerializableAndDeserializable() Assert.Equal(sut.FunctionName, deserializedSut.FunctionName); Assert.Equal(sut.Result, deserializedSut.Result?.ToString()); } - - [Fact] - public void ItShouldBeCreatedFromFullyQualifiedNameThatHasPluginNameAndFunctionName() - { - // Arrange & act - var sut = FunctionResultContent.Create("p1.f1", "id", "result", "."); - - // Assert - Assert.Equal("f1", sut.FunctionName); - Assert.Equal("p1", sut.PluginName); - Assert.Equal("id", sut.Id); - Assert.Equal("result", sut.Result); - } - - [Fact] - public void ItShouldBeCreatedFromFullyQualifiedNameThatHasFunctionNameOnly() - { - // Arrange & act - var sut = FunctionResultContent.Create("f1", "id", "result"); - - // Assert - Assert.Equal("f1", sut.FunctionName); - Assert.Null(sut.PluginName); - Assert.Equal("id", sut.Id); - Assert.Equal("result", sut.Result); - } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Utilities/FullyQualifiedFunctionNameTests.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/FullyQualifiedFunctionNameTests.cs new file mode 100644 index 000000000000..8197a8cd119c --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/FullyQualifiedFunctionNameTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.UnitTests.Utilities; +public class FullyQualifiedFunctionNameTests +{ + [Fact] + public void ItShouldParseFullyQualifiedNameThatHasPluginNameAndFunctionName() + { + // Arrange & act + var sut = FullyQualifiedFunctionName.Parse("p1.f1", "."); + + // Assert + Assert.Equal("f1", sut.FunctionName); + Assert.Equal("p1", sut.PluginName); + } + + [Fact] + public void ItShouldParseFullyQualifiedNameThatHasFunctionNameOnly() + { + // Arrange & act + var sut = FullyQualifiedFunctionName.Parse("f1"); + + // Assert + Assert.Equal("f1", sut.FunctionName); + Assert.Null(sut.PluginName); + } +} From 397795ba834099e74a98e4a8babd13e659cb99a3 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 21:31:14 +0100 Subject: [PATCH 17/90] The `FunctionResultContent.Result` property setter is removed. --- .../Contents/FunctionResultContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs index e8ab49eddf25..6e8e95871c8c 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs @@ -32,7 +32,7 @@ public sealed class FunctionResultContent : KernelContent /// The result of the function call, the function invocation exception or the custom error message. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public object? Result { get; set; } + public object? Result { get; } /// /// Creates a new instance of the class. From f9c493de0b948486009e032a78d9605095fe2b53 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 21:36:20 +0100 Subject: [PATCH 18/90] Default null value of the `executionSettings` parameter is removed for one of the KernelArguments constructors. --- .../SemanticKernel.Abstractions/Functions/KernelArguments.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs index 1f0b31192c2f..d7776f83f24a 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelArguments.cs @@ -35,7 +35,7 @@ public KernelArguments() /// Initializes a new instance of the class with the specified AI execution settings. /// /// The prompt execution settings. - public KernelArguments(PromptExecutionSettings? executionSettings = null) + public KernelArguments(PromptExecutionSettings? executionSettings) { this._arguments = new(StringComparer.OrdinalIgnoreCase); From 795c1977b8fe620f673ff9865a4bcb8cd592eb0c Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 21:50:24 +0100 Subject: [PATCH 19/90] The unnecessary check for the result of the OfType method call is removed. --- .../src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index c9eb3c1b0d2d..ddbee44bacc4 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -999,7 +999,8 @@ private static IEnumerable GetRequestMessages(ChatMessageCon // Handling function call results represented by the FunctionResultContent type. // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) - if (message.Items.OfType() is { } resultContents && resultContents.Any()) + var resultContents = message.Items.OfType().ToArray(); + if (resultContents.Length != 0) { var toolMessages = new List(); @@ -1071,7 +1072,8 @@ private static IEnumerable GetRequestMessages(ChatMessageCon } // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallContent type. - if (message.Items.OfType() is { } functionCallContents && functionCallContents.Any()) + var functionCallContents = message.Items.OfType().ToArray(); + if (functionCallContents.Length != 0) { var ftcs = new List(tools ?? Enumerable.Empty()); From 055a194b033bf8aec9aa1eba14a4fefdb7dbcbdd Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 8 Apr 2024 22:32:31 +0100 Subject: [PATCH 20/90] The `FunctionCallContent.GetFullyQualifiedName` method is moved to the `FunctionName` utility class. --- dotnet/SK-dotnet.sln | 2 +- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 10 ++-- ...alifiedFunctionName.cs => FunctionName.cs} | 36 +++++++++---- .../Contents/FunctionCallContent.cs | 10 ---- .../Contents/FunctionCallContentTests.cs | 13 ----- .../FullyQualifiedFunctionNameTests.cs | 30 ----------- .../Utilities/FunctionNameTests.cs | 50 +++++++++++++++++++ 7 files changed, 82 insertions(+), 69 deletions(-) rename dotnet/src/InternalUtilities/src/Functions/{FullyQualifiedFunctionName.cs => FunctionName.cs} (50%) delete mode 100644 dotnet/src/SemanticKernel.UnitTests/Utilities/FullyQualifiedFunctionNameTests.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 084a8534712a..d6b4130137c4 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -245,7 +245,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Onnx.UnitTests", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Functions", "Functions", "{4DFB3897-0319-4DF2-BCFE-E6E0648297D2}" ProjectSection(SolutionItems) = preProject - src\InternalUtilities\src\Functions\FullyQualifiedFunctionName.cs = src\InternalUtilities\src\Functions\FullyQualifiedFunctionName.cs + src\InternalUtilities\src\Functions\FunctionName.cs = src\InternalUtilities\src\Functions\FunctionName.cs EndProjectSection EndProject Global diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index ddbee44bacc4..c0b3a7b3f876 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -448,8 +448,8 @@ static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory c { // 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 fqn = FullyQualifiedFunctionName.Parse(functionCall.Name, OpenAIFunction.NameSeparator); - message.Items.Add(new FunctionResultContent(fqn.FunctionName, fqn.PluginName, functionCall.Id, result)); + var functionName = FunctionName.Parse(functionCall.Name, OpenAIFunction.NameSeparator); + message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, functionCall.Id, result)); } chat.Add(message); @@ -1083,7 +1083,7 @@ private static IEnumerable GetRequestMessages(ChatMessageCon { var argument = JsonSerializer.Serialize(fcContent.Arguments); - ftcs.Add(new ChatCompletionsFunctionToolCall(fcContent.Id, fcContent.GetFullyQualifiedName(OpenAIFunction.NameSeparator), argument ?? string.Empty)); + ftcs.Add(new ChatCompletionsFunctionToolCall(fcContent.Id, FunctionName.ToFullyQualifiedName(fcContent.FunctionName, fcContent.PluginName, OpenAIFunction.NameSeparator), argument ?? string.Empty)); } } @@ -1151,9 +1151,9 @@ private OpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompl // The original arguments and function tool call will be available via the 'InnerContent' property for the connector caller to access. } - var fqn = FullyQualifiedFunctionName.Parse(functionToolCall.Name, OpenAIFunction.NameSeparator); + var fqn = FunctionName.Parse(functionToolCall.Name, OpenAIFunction.NameSeparator); var content = new FunctionCallContent( - functionName: fqn.FunctionName, + functionName: fqn.Name, pluginName: fqn.PluginName, id: functionToolCall.Id, arguments: arguments); diff --git a/dotnet/src/InternalUtilities/src/Functions/FullyQualifiedFunctionName.cs b/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs similarity index 50% rename from dotnet/src/InternalUtilities/src/Functions/FullyQualifiedFunctionName.cs rename to dotnet/src/InternalUtilities/src/Functions/FunctionName.cs index 190d70e49583..5884cf179860 100644 --- a/dotnet/src/InternalUtilities/src/Functions/FullyQualifiedFunctionName.cs +++ b/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs @@ -3,7 +3,11 @@ using System; namespace Microsoft.SemanticKernel; -internal sealed class FullyQualifiedFunctionName + +/// +/// Represents a function name. +/// +internal sealed class FunctionName { /// /// The plugin name. @@ -13,27 +17,39 @@ internal sealed class FullyQualifiedFunctionName /// /// The function name. /// - public string FunctionName { get; } + public string Name { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The function name. + /// The function name. /// The plugin name. - public FullyQualifiedFunctionName(string functionName, string? pluginName = null) + public FunctionName(string name, string? pluginName = null) { - Verify.NotNull(functionName); + Verify.NotNull(name); - this.FunctionName = functionName; + this.Name = name; this.PluginName = pluginName; } /// - /// Creates a new instance of the class. + /// Gets the fully-qualified name of the function. + /// + /// The function name. + /// The plugin name. + /// The function name separator. + /// Fully-qualified name of the function. + public static string ToFullyQualifiedName(string functionName, string? pluginName = null, string functionNameSeparator = "-") + { + return string.IsNullOrEmpty(pluginName) ? functionName : $"{pluginName}{functionNameSeparator}{functionName}"; + } + + /// + /// Creates a new instance of the class. /// /// Fully-qualified name of the function. /// The function name separator. - public static FullyQualifiedFunctionName Parse(string fullyQualifiedName, string functionNameSeparator = "-") + public static FunctionName Parse(string fullyQualifiedName, string functionNameSeparator = "-") { Verify.NotNull(fullyQualifiedName); @@ -47,6 +63,6 @@ public static FullyQualifiedFunctionName Parse(string fullyQualifiedName, string functionName = fullyQualifiedName.AsSpan(separatorPos + functionNameSeparator.Length).Trim().ToString(); } - return new FullyQualifiedFunctionName(functionName: functionName, pluginName: pluginName); + return new FunctionName(name: functionName, pluginName: pluginName); } } diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs index 639423035da5..e9372da95cd9 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs @@ -34,16 +34,6 @@ public sealed class FunctionCallContent : KernelContent /// public KernelArguments? Arguments { get; } - /// - /// Gets the fully-qualified name of the function. - /// - /// The function name separator. - /// Fully-qualified name of the function. - public string GetFullyQualifiedName(string functionNameSeparator = "-") - { - return string.IsNullOrEmpty(this.PluginName) ? this.FunctionName : $"{this.PluginName}{functionNameSeparator}{this.FunctionName}"; - } - /// /// Creates a new instance of the class. /// diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs index 6498699017d8..7e0efd30e56e 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs @@ -27,19 +27,6 @@ public void ItShouldBeInitializedFromFunctionAndPluginName() Assert.Same(this._arguments, sut.Arguments); } - [Fact] - public void ItShouldCreateFullyQualifiedName() - { - // Arrange - var sut = new FunctionCallContent("f1", "p1", "id", this._arguments); - - // Act - var fullyQualifiedName = sut.GetFullyQualifiedName("."); - - // Assert - Assert.Equal("p1.f1", fullyQualifiedName); - } - [Fact] public async Task ItShouldFindKernelFunctionAndInvokeItAsync() { diff --git a/dotnet/src/SemanticKernel.UnitTests/Utilities/FullyQualifiedFunctionNameTests.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/FullyQualifiedFunctionNameTests.cs deleted file mode 100644 index 8197a8cd119c..000000000000 --- a/dotnet/src/SemanticKernel.UnitTests/Utilities/FullyQualifiedFunctionNameTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.SemanticKernel; -using Xunit; - -namespace SemanticKernel.UnitTests.Utilities; -public class FullyQualifiedFunctionNameTests -{ - [Fact] - public void ItShouldParseFullyQualifiedNameThatHasPluginNameAndFunctionName() - { - // Arrange & act - var sut = FullyQualifiedFunctionName.Parse("p1.f1", "."); - - // Assert - Assert.Equal("f1", sut.FunctionName); - Assert.Equal("p1", sut.PluginName); - } - - [Fact] - public void ItShouldParseFullyQualifiedNameThatHasFunctionNameOnly() - { - // Arrange & act - var sut = FullyQualifiedFunctionName.Parse("f1"); - - // Assert - Assert.Equal("f1", sut.FunctionName); - Assert.Null(sut.PluginName); - } -} diff --git a/dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs new file mode 100644 index 000000000000..bf3077c31808 --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.UnitTests.Utilities; +public class FunctionNameTests +{ + [Fact] + public void ItShouldParseFullyQualifiedNameThatHasPluginNameAndFunctionName() + { + // Arrange & act + var sut = FunctionName.Parse("p1.f1", "."); + + // Assert + Assert.Equal("f1", sut.Name); + Assert.Equal("p1", sut.PluginName); + } + + [Fact] + public void ItShouldParseFullyQualifiedNameThatHasFunctionNameOnly() + { + // Arrange & act + var sut = FunctionName.Parse("f1"); + + // Assert + Assert.Equal("f1", sut.Name); + Assert.Null(sut.PluginName); + } + + [Fact] + public void ItShouldCreateFullyQualifiedNameFromPluginAndFunctionNames() + { + // Act + var fullyQualifiedName = FunctionName.ToFullyQualifiedName("f1", "p1", "."); + + // Assert + Assert.Equal("p1.f1", fullyQualifiedName); + } + + [Fact] + public void ItShouldCreateFullyQualifiedNameFromFunctionName() + { + // Act + var fullyQualifiedName = FunctionName.ToFullyQualifiedName("f1", "."); + + // Assert + Assert.Equal("f1", fullyQualifiedName); + } +} From cab73c5642e41ac6e749f02d1938527c3788508d Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 9 Apr 2024 10:17:24 +0100 Subject: [PATCH 21/90] Logging function parameters deserialization failure --- .../src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index c0b3a7b3f876..9ee1d6f28e33 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1145,8 +1145,12 @@ private OpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompl { arguments = JsonSerializer.Deserialize(functionToolCall.Arguments); } - catch (JsonException) + catch (JsonException ex) { + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug(ex, "Failed to deserialize function arguments ({FunctionName}/{FunctionId}).", functionToolCall.Name, functionToolCall.Id); + } // If the arguments are not valid JSON, we'll just leave them as null. // The original arguments and function tool call will be available via the 'InnerContent' property for the connector caller to access. } From f6d3e9672b4b26f491859063eb12ee50cb88a828 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 9 Apr 2024 10:23:01 +0100 Subject: [PATCH 22/90] The ChatHistory.AddMessage method usage is replaced by ChatHistory.Add one. --- .../KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index 6520f84bbb1e..7e29c8b539af 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -96,11 +96,11 @@ public async Task RunAsync() { FunctionResultContent functionResult = await functionCall.InvokeAsync(kernel); // Executing each function. - chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { functionResult }); // Adding function result to chat history. + chatHistory.Add(new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { functionResult })); // Adding function result to chat history. } catch (Exception ex) { - chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionResultContent(functionCall, ex) }); + chatHistory.Add(new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionResultContent(functionCall, ex) })); // Adding function result to chat history. // Adding exception to chat history. // or //string message = "Error details that LLM can reason about."; From ef6a55c99fe11a97df980f822b3fe68ebac29f9f Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 9 Apr 2024 10:24:30 +0100 Subject: [PATCH 23/90] ChatHistory.AddMessage method usage is replaced by the ChatHistory.Add one. --- .../KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index 7e29c8b539af..93440ea9d104 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -104,7 +104,7 @@ public async Task RunAsync() // Adding exception to chat history. // or //string message = "Error details that LLM can reason about."; - //chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionResultContent(functionCall, message) }); + //chatHistory.Add(new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionResultContent(functionCall, message) })); } } From 636a22ebe1b13b71282dab4f480513b847fb6cf8 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 9 Apr 2024 10:33:17 +0100 Subject: [PATCH 24/90] non-existing project is removed --- dotnet/SK-dotnet.sln | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index d6b4130137c4..ebf1916246b5 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -659,7 +659,6 @@ Global {8EE10EB0-A947-49CC-BCC1-18D93415B9E4} = {FA3720F1-C99A-49B2-9577-A940257098BF} {D06465FA-0308-494C-920B-D502DA5690CB} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} {4DFB3897-0319-4DF2-BCFE-E6E0648297D2} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} - {328BE2F3-4BFD-490D-B813-C8F054552FF2} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} From b377b509b9fa3bfc2ef084549d6f0f9f88e6b120 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 9 Apr 2024 10:47:00 +0100 Subject: [PATCH 25/90] Fix for unit tests --- .../src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs index bf3077c31808..d9882cae8328 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs @@ -42,7 +42,7 @@ public void ItShouldCreateFullyQualifiedNameFromPluginAndFunctionNames() public void ItShouldCreateFullyQualifiedNameFromFunctionName() { // Act - var fullyQualifiedName = FunctionName.ToFullyQualifiedName("f1", "."); + var fullyQualifiedName = FunctionName.ToFullyQualifiedName("f1"); // Assert Assert.Equal("f1", fullyQualifiedName); From c29954df5c07aa6d721a2189cba0505df03870d8 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 9 Apr 2024 11:06:55 +0100 Subject: [PATCH 26/90] FunctionCallContent class is renamed to FunctionCallRequestContent --- .../Example59_OpenAIFunctionCalling.cs | 2 +- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 25 ++++++++++--------- .../AzureOpenAIChatCompletionServiceTests.cs | 20 +++++++-------- .../OpenAIChatCompletionServiceTests.cs | 20 +++++++-------- .../Connectors/OpenAI/OpenAIToolsTests.cs | 14 +++++------ ...ntent.cs => FunctionCallRequestContent.cs} | 6 ++--- .../Contents/FunctionResultContent.cs | 8 +++--- .../Contents/KernelContent.cs | 2 +- .../Contents/ChatMessageContentTests.cs | 6 ++--- ....cs => FunctionCallRequestContentTests.cs} | 8 +++--- .../Contents/FunctionResultContentTests.cs | 4 +-- 11 files changed, 58 insertions(+), 57 deletions(-) rename dotnet/src/SemanticKernel.Abstractions/Contents/{FunctionCallContent.cs => FunctionCallRequestContent.cs} (89%) rename dotnet/src/SemanticKernel.UnitTests/Contents/{FunctionCallContentTests.cs => FunctionCallRequestContentTests.cs} (82%) diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index 93440ea9d104..2fe389b8d355 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -82,7 +82,7 @@ public async Task RunAsync() Write(result.Content); } - IEnumerable functionCalls = result.Items.OfType(); // Getting list of function calls. + IEnumerable functionCalls = result.Items.OfType(); // Getting list of function calls. if (!functionCalls.Any()) { break; diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 9ee1d6f28e33..39436b75a9f2 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1072,18 +1072,18 @@ private static IEnumerable GetRequestMessages(ChatMessageCon } // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallContent type. - var functionCallContents = message.Items.OfType().ToArray(); - if (functionCallContents.Length != 0) + var functionCallRequests = message.Items.OfType().ToArray(); + if (functionCallRequests.Length != 0) { var ftcs = new List(tools ?? Enumerable.Empty()); - foreach (var fcContent in functionCallContents) + foreach (var fcRequest in functionCallRequests) { - if (!ftcs.Any(ftc => ftc.Id == fcContent.Id)) + if (!ftcs.Any(ftc => ftc.Id == fcRequest.Id)) { - var argument = JsonSerializer.Serialize(fcContent.Arguments); + var argument = JsonSerializer.Serialize(fcRequest.Arguments); - ftcs.Add(new ChatCompletionsFunctionToolCall(fcContent.Id, FunctionName.ToFullyQualifiedName(fcContent.FunctionName, fcContent.PluginName, OpenAIFunction.NameSeparator), argument ?? string.Empty)); + ftcs.Add(new ChatCompletionsFunctionToolCall(fcRequest.Id, FunctionName.ToFullyQualifiedName(fcRequest.FunctionName, fcRequest.PluginName, OpenAIFunction.NameSeparator), argument ?? string.Empty)); } } @@ -1155,15 +1155,16 @@ private OpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompl // The original arguments and function tool call will be available via the 'InnerContent' property for the connector caller to access. } - var fqn = FunctionName.Parse(functionToolCall.Name, OpenAIFunction.NameSeparator); - var content = new FunctionCallContent( - functionName: fqn.Name, - pluginName: fqn.PluginName, + var functionName = FunctionName.Parse(functionToolCall.Name, OpenAIFunction.NameSeparator); + + var functionCallRequestContent = new FunctionCallRequestContent( + functionName: functionName.Name, + pluginName: functionName.PluginName, id: functionToolCall.Id, arguments: arguments); - content.InnerContent = functionToolCall; + functionCallRequestContent.InnerContent = functionToolCall; - message.Items.Add(content); + message.Items.Add(functionCallRequestContent); } } diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index 67d085b1dde1..3e9a8ea45d5a 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -723,28 +723,28 @@ public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfT Assert.NotNull(result); Assert.Equal(4, result.Items.Count); - var getCurrentWeatherFunctionCall = result.Items[0] as FunctionCallContent; + var getCurrentWeatherFunctionCall = result.Items[0] as FunctionCallRequestContent; Assert.NotNull(getCurrentWeatherFunctionCall); Assert.Equal("GetCurrentWeather", getCurrentWeatherFunctionCall.FunctionName); Assert.Equal("MyPlugin", getCurrentWeatherFunctionCall.PluginName); Assert.Equal("1", getCurrentWeatherFunctionCall.Id); Assert.Equal("Boston, MA", getCurrentWeatherFunctionCall.Arguments?["location"]?.ToString()); - var functionWithExceptionFunctionCall = result.Items[1] as FunctionCallContent; + var functionWithExceptionFunctionCall = result.Items[1] as FunctionCallRequestContent; Assert.NotNull(functionWithExceptionFunctionCall); Assert.Equal("FunctionWithException", functionWithExceptionFunctionCall.FunctionName); Assert.Equal("MyPlugin", functionWithExceptionFunctionCall.PluginName); Assert.Equal("2", functionWithExceptionFunctionCall.Id); Assert.Equal("value", functionWithExceptionFunctionCall.Arguments?["argument"]?.ToString()); - var nonExistentFunctionCall = result.Items[2] as FunctionCallContent; + var nonExistentFunctionCall = result.Items[2] as FunctionCallRequestContent; Assert.NotNull(nonExistentFunctionCall); Assert.Equal("NonExistentFunction", nonExistentFunctionCall.FunctionName); Assert.Equal("MyPlugin", nonExistentFunctionCall.PluginName); Assert.Equal("3", nonExistentFunctionCall.Id); Assert.Equal("value", nonExistentFunctionCall.Arguments?["argument"]?.ToString()); - var invalidArgumentsFunctionCall = result.Items[3] as FunctionCallContent; + var invalidArgumentsFunctionCall = result.Items[3] as FunctionCallRequestContent; Assert.NotNull(invalidArgumentsFunctionCall); Assert.Equal("InvalidArguments", invalidArgumentsFunctionCall.FunctionName); Assert.Equal("MyPlugin", invalidArgumentsFunctionCall.PluginName); @@ -765,8 +765,8 @@ public async Task FunctionCallsShouldBeReturnedToLLMAsync() var items = new ChatMessageContentItemCollection { - new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), - new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }) + new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), + new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }) }; var chatHistory = new ChatHistory @@ -825,11 +825,11 @@ public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsyn { new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { - new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + new FunctionResultContent(new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), }), new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { - new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + new FunctionResultContent(new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") }) }; @@ -873,8 +873,8 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage { new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { - new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), - new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + new FunctionResultContent(new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + new FunctionResultContent(new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") }) }; diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs index f4782e0f8f10..a6b5d0ee7e34 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs @@ -346,28 +346,28 @@ public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfT Assert.NotNull(result); Assert.Equal(4, result.Items.Count); - var getCurrentWeatherFunctionCall = result.Items[0] as FunctionCallContent; + var getCurrentWeatherFunctionCall = result.Items[0] as FunctionCallRequestContent; Assert.NotNull(getCurrentWeatherFunctionCall); Assert.Equal("GetCurrentWeather", getCurrentWeatherFunctionCall.FunctionName); Assert.Equal("MyPlugin", getCurrentWeatherFunctionCall.PluginName); Assert.Equal("1", getCurrentWeatherFunctionCall.Id); Assert.Equal("Boston, MA", getCurrentWeatherFunctionCall.Arguments?["location"]?.ToString()); - var functionWithExceptionFunctionCall = result.Items[1] as FunctionCallContent; + var functionWithExceptionFunctionCall = result.Items[1] as FunctionCallRequestContent; Assert.NotNull(functionWithExceptionFunctionCall); Assert.Equal("FunctionWithException", functionWithExceptionFunctionCall.FunctionName); Assert.Equal("MyPlugin", functionWithExceptionFunctionCall.PluginName); Assert.Equal("2", functionWithExceptionFunctionCall.Id); Assert.Equal("value", functionWithExceptionFunctionCall.Arguments?["argument"]?.ToString()); - var nonExistentFunctionCall = result.Items[2] as FunctionCallContent; + var nonExistentFunctionCall = result.Items[2] as FunctionCallRequestContent; Assert.NotNull(nonExistentFunctionCall); Assert.Equal("NonExistentFunction", nonExistentFunctionCall.FunctionName); Assert.Equal("MyPlugin", nonExistentFunctionCall.PluginName); Assert.Equal("3", nonExistentFunctionCall.Id); Assert.Equal("value", nonExistentFunctionCall.Arguments?["argument"]?.ToString()); - var invalidArgumentsFunctionCall = result.Items[3] as FunctionCallContent; + var invalidArgumentsFunctionCall = result.Items[3] as FunctionCallRequestContent; Assert.NotNull(invalidArgumentsFunctionCall); Assert.Equal("InvalidArguments", invalidArgumentsFunctionCall.FunctionName); Assert.Equal("MyPlugin", invalidArgumentsFunctionCall.PluginName); @@ -388,8 +388,8 @@ public async Task FunctionCallsShouldBeReturnedToLLMAsync() var items = new ChatMessageContentItemCollection { - new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), - new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }) + new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), + new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }) }; var chatHistory = new ChatHistory @@ -448,11 +448,11 @@ public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsyn { new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { - new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + new FunctionResultContent(new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), }), new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { - new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + new FunctionResultContent(new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") }) }; @@ -496,8 +496,8 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage { new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { - new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), - new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + new FunctionResultContent(new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + new FunctionResultContent(new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") }) }; diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index 351ce876e729..36df4b884f2d 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -248,7 +248,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManual // Act var messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - var functionCalls = messageContent.Items.OfType().ToArray(); + var functionCalls = messageContent.Items.OfType().ToArray(); while (functionCalls.Length != 0) { @@ -265,7 +265,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManual // Sending the functions invocation results to the LLM to get the final response messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = messageContent.Items.OfType().ToArray(); + functionCalls = messageContent.Items.OfType().ToArray(); } // Assert @@ -288,7 +288,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc // Act var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - var functionCalls = messageContent.Items.OfType().ToArray(); + var functionCalls = messageContent.Items.OfType().ToArray(); while (functionCalls.Length != 0) { @@ -306,7 +306,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc // Sending the functions execution results back to the LLM to get the final response messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = messageContent.Items.OfType().ToArray(); + functionCalls = messageContent.Items.OfType().ToArray(); } // Assert @@ -332,7 +332,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu // Act var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - var functionCalls = messageContent.Items.OfType().ToArray(); + var functionCalls = messageContent.Items.OfType().ToArray(); while (functionCalls.Length > 0) { @@ -348,7 +348,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu } // Adding a simulated function call to the connector response message - var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); + var simulatedFunctionCall = new FunctionCallRequestContent("weather-alert", id: "call_123"); messageContent.Items.Add(simulatedFunctionCall); // Adding a simulated function result to chat history @@ -357,7 +357,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu // Sending the functions invocation results back to the LLM to get the final response messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = messageContent.Items.OfType().ToArray(); + functionCalls = messageContent.Items.OfType().ToArray(); } // Assert diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs similarity index 89% rename from dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs rename to dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs index e9372da95cd9..2e7d62e2bf84 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs @@ -12,7 +12,7 @@ namespace Microsoft.SemanticKernel; /// Represents a function call requested by LLM. /// [Experimental("SKEXP0001")] -public sealed class FunctionCallContent : KernelContent +public sealed class FunctionCallRequestContent : KernelContent { /// /// The function call ID. @@ -35,14 +35,14 @@ public sealed class FunctionCallContent : KernelContent public KernelArguments? Arguments { get; } /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// /// The function name. /// The plugin name. /// The function call ID. /// The function original arguments. [JsonConstructor] - public FunctionCallContent(string functionName, string? pluginName = null, string? id = null, KernelArguments? arguments = null) + public FunctionCallRequestContent(string functionName, string? pluginName = null, string? id = null, KernelArguments? arguments = null) { Verify.NotNull(functionName); diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs index 6e8e95871c8c..4af013ac9d3e 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs @@ -51,11 +51,11 @@ public FunctionResultContent(string functionName, string? pluginName = null, str } /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// /// The function call. /// The function result. - public FunctionResultContent(FunctionCallContent functionCall, object? result = null) + public FunctionResultContent(FunctionCallRequestContent functionCall, object? result = null) { this.Id = functionCall.Id; this.PluginName = functionCall.PluginName; @@ -64,11 +64,11 @@ public FunctionResultContent(FunctionCallContent functionCall, object? result = } /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// /// The function call content. /// The function result. - public FunctionResultContent(FunctionCallContent functionCallContent, FunctionResult result) : + public FunctionResultContent(FunctionCallRequestContent functionCallContent, FunctionResult result) : this(functionCallContent, result.Value) { this.InnerContent = result; diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/KernelContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/KernelContent.cs index b23109537762..782f68a3d6b2 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/KernelContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/KernelContent.cs @@ -16,7 +16,7 @@ namespace Microsoft.SemanticKernel; #pragma warning restore SKEXP0010 #pragma warning disable SKEXP0001 [JsonDerivedType(typeof(AudioContent), typeDiscriminator: nameof(AudioContent))] -[JsonDerivedType(typeof(FunctionCallContent), typeDiscriminator: nameof(FunctionCallContent))] +[JsonDerivedType(typeof(FunctionCallRequestContent), typeDiscriminator: nameof(FunctionCallRequestContent))] [JsonDerivedType(typeof(FunctionResultContent), typeDiscriminator: nameof(FunctionResultContent))] #pragma warning restore SKEXP0001 public abstract class KernelContent diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs index 22b1db2bae76..c79106418590 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs @@ -190,9 +190,9 @@ public void ItCanBeSerializeAndDeserialized() }) { MimeType = "mime-type-6" }); - items.Add(new FunctionCallContent("function-name", "plugin-name", "function-id", new KernelArguments { ["parameter"] = "argument" })); + items.Add(new FunctionCallRequestContent("function-name", "plugin-name", "function-id", new KernelArguments { ["parameter"] = "argument" })); - items.Add(new FunctionResultContent(new FunctionCallContent("function-name", "plugin-name", "function-id"), "function-result")); + items.Add(new FunctionResultContent(new FunctionCallRequestContent("function-name", "plugin-name", "function-id"), "function-result")); var sut = new ChatMessageContent(AuthorRole.User, items: items, "message-model", metadata: new Dictionary() { @@ -274,7 +274,7 @@ public void ItCanBeSerializeAndDeserialized() Assert.Single(textContent.Metadata); Assert.Equal("metadata-value-6", textContent.Metadata["metadata-key-6"]?.ToString()); - var functionCallContent = deserializedMessage.Items[6] as FunctionCallContent; + var functionCallContent = deserializedMessage.Items[6] as FunctionCallRequestContent; Assert.NotNull(functionCallContent); Assert.Equal("function-name", functionCallContent.FunctionName); Assert.Equal("plugin-name", functionCallContent.PluginName); diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallRequestContentTests.cs similarity index 82% rename from dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs rename to dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallRequestContentTests.cs index 7e0efd30e56e..76aa25735a4b 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallRequestContentTests.cs @@ -5,11 +5,11 @@ namespace Microsoft.SemanticKernel.Contents; -public class FunctionCallContentTests +public class FunctionCallRequestContentTests { private readonly KernelArguments _arguments; - public FunctionCallContentTests() + public FunctionCallRequestContentTests() { this._arguments = []; } @@ -18,7 +18,7 @@ public FunctionCallContentTests() public void ItShouldBeInitializedFromFunctionAndPluginName() { // Arrange & act - var sut = new FunctionCallContent("f1", "p1", "id", this._arguments); + var sut = new FunctionCallRequestContent("f1", "p1", "id", this._arguments); // Assert Assert.Equal("f1", sut.FunctionName); @@ -43,7 +43,7 @@ public async Task ItShouldFindKernelFunctionAndInvokeItAsync() kernel.Plugins.AddFromFunctions("p1", [function]); - var sut = new FunctionCallContent("f1", "p1", "id", this._arguments); + var sut = new FunctionCallRequestContent("f1", "p1", "id", this._arguments); // Act var resultContent = await sut.InvokeAsync(kernel); diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs index aaef8340be90..68c8808a5c57 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs @@ -7,11 +7,11 @@ namespace SemanticKernel.UnitTests.Contents; public class FunctionResultContentTests { - private readonly FunctionCallContent _callContent; + private readonly FunctionCallRequestContent _callContent; public FunctionResultContentTests() { - this._callContent = new FunctionCallContent("f1", "p1", "id", []); + this._callContent = new FunctionCallRequestContent("f1", "p1", "id", []); } [Fact] From d555b29876897242f1a48c93f41f81c626e67d15 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 9 Apr 2024 11:11:25 +0100 Subject: [PATCH 27/90] FunctionResultContent class is renamed to FunctionCallResultContent --- .../Example59_OpenAIFunctionCalling.cs | 4 ++-- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 4 ++-- .../AzureOpenAIChatCompletionServiceTests.cs | 8 ++++---- .../OpenAIChatCompletionServiceTests.cs | 8 ++++---- .../Connectors/OpenAI/OpenAIToolsTests.cs | 4 ++-- .../Contents/FunctionCallRequestContent.cs | 4 ++-- ...Content.cs => FunctionCallResultContent.cs} | 10 +++++----- .../Contents/KernelContent.cs | 2 +- .../Contents/ChatMessageContentTests.cs | 4 ++-- ...ts.cs => FunctionCallResultContentTests.cs} | 18 +++++++++--------- 10 files changed, 33 insertions(+), 33 deletions(-) rename dotnet/src/SemanticKernel.Abstractions/Contents/{FunctionResultContent.cs => FunctionCallResultContent.cs} (81%) rename dotnet/src/SemanticKernel.UnitTests/Contents/{FunctionResultContentTests.cs => FunctionCallResultContentTests.cs} (75%) diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index 2fe389b8d355..3a4a449b8ef5 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -94,13 +94,13 @@ public async Task RunAsync() { try { - FunctionResultContent functionResult = await functionCall.InvokeAsync(kernel); // Executing each function. + FunctionCallResultContent functionResult = await functionCall.InvokeAsync(kernel); // Executing each function. chatHistory.Add(new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { functionResult })); // Adding function result to chat history. } catch (Exception ex) { - chatHistory.Add(new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionResultContent(functionCall, ex) })); // Adding function result to chat history. + chatHistory.Add(new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionCallResultContent(functionCall, ex) })); // Adding function result to chat history. // Adding exception to chat history. // or //string message = "Error details that LLM can reason about."; diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 39436b75a9f2..e6b3749a3591 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -449,7 +449,7 @@ static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory c // 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, OpenAIFunction.NameSeparator); - message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, functionCall.Id, result)); + message.Items.Add(new FunctionCallResultContent(functionName.Name, functionName.PluginName, functionCall.Id, result)); } chat.Add(message); @@ -999,7 +999,7 @@ private static IEnumerable GetRequestMessages(ChatMessageCon // Handling function call results represented by the FunctionResultContent type. // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) - var resultContents = message.Items.OfType().ToArray(); + var resultContents = message.Items.OfType().ToArray(); if (resultContents.Length != 0) { var toolMessages = new List(); diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index 3e9a8ea45d5a..15fc239d48e6 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -825,11 +825,11 @@ public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsyn { new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { - new FunctionResultContent(new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + new FunctionCallResultContent(new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), }), new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { - new FunctionResultContent(new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + new FunctionCallResultContent(new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") }) }; @@ -873,8 +873,8 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage { new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { - new FunctionResultContent(new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), - new FunctionResultContent(new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + new FunctionCallResultContent(new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + new FunctionCallResultContent(new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") }) }; diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs index a6b5d0ee7e34..0e41aa37561d 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs @@ -448,11 +448,11 @@ public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsyn { new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { - new FunctionResultContent(new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + new FunctionCallResultContent(new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), }), new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { - new FunctionResultContent(new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + new FunctionCallResultContent(new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") }) }; @@ -496,8 +496,8 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage { new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { - new FunctionResultContent(new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), - new FunctionResultContent(new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + new FunctionCallResultContent(new FunctionCallRequestContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + new FunctionCallResultContent(new FunctionCallRequestContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") }) }; diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index 36df4b884f2d..a7b2cd1a532e 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -301,7 +301,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc // Simulating an exception var exception = new OperationCanceledException("The operation was canceled due to timeout."); - chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionResultContent(functionCall, exception) }); + chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionCallResultContent(functionCall, exception) }); } // Sending the functions execution results back to the LLM to get the final response @@ -353,7 +353,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu // Adding a simulated function result to chat history var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; - chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionResultContent(simulatedFunctionCall, simulatedFunctionResult) }); + chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionCallResultContent(simulatedFunctionCall, simulatedFunctionResult) }); // Sending the functions invocation results back to the LLM to get the final response messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs index 2e7d62e2bf84..70ca320d0a7a 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs @@ -58,7 +58,7 @@ public FunctionCallRequestContent(string functionName, string? pluginName = null /// The containing services, plugins, and other state for use throughout the operation. /// The to monitor for cancellation requests. The default is . /// The result of the function's execution. - public async Task InvokeAsync(Kernel kernel, CancellationToken cancellationToken = default) + public async Task InvokeAsync(Kernel kernel, CancellationToken cancellationToken = default) { Verify.NotNull(kernel, nameof(kernel)); @@ -66,7 +66,7 @@ public async Task InvokeAsync(Kernel kernel, Cancellation { var result = await function.InvokeAsync(kernel, this.Arguments, cancellationToken).ConfigureAwait(false); - return new FunctionResultContent(this, result); + return new FunctionCallResultContent(this, result); } throw new KeyNotFoundException($"The plugin collection does not contain a plugin and/or function with the specified names. Plugin name - '{this.PluginName}', function name - '{this.FunctionName}'."); diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs similarity index 81% rename from dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs rename to dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs index 4af013ac9d3e..c3e23bd5bc45 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs @@ -9,7 +9,7 @@ namespace Microsoft.SemanticKernel; /// Represents the result of a function call. /// [Experimental("SKEXP0001")] -public sealed class FunctionResultContent : KernelContent +public sealed class FunctionCallResultContent : KernelContent { /// /// The function call ID. @@ -35,14 +35,14 @@ public sealed class FunctionResultContent : KernelContent public object? Result { get; } /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// /// The function name. /// The plugin name. /// The function call ID. /// The function result. [JsonConstructor] - public FunctionResultContent(string functionName, string? pluginName = null, string? id = null, object? result = null) + public FunctionCallResultContent(string functionName, string? pluginName = null, string? id = null, object? result = null) { this.FunctionName = functionName; this.PluginName = pluginName; @@ -55,7 +55,7 @@ public FunctionResultContent(string functionName, string? pluginName = null, str /// /// The function call. /// The function result. - public FunctionResultContent(FunctionCallRequestContent functionCall, object? result = null) + public FunctionCallResultContent(FunctionCallRequestContent functionCall, object? result = null) { this.Id = functionCall.Id; this.PluginName = functionCall.PluginName; @@ -68,7 +68,7 @@ public FunctionResultContent(FunctionCallRequestContent functionCall, object? re /// /// The function call content. /// The function result. - public FunctionResultContent(FunctionCallRequestContent functionCallContent, FunctionResult result) : + public FunctionCallResultContent(FunctionCallRequestContent functionCallContent, FunctionResult result) : this(functionCallContent, result.Value) { this.InnerContent = result; diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/KernelContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/KernelContent.cs index 782f68a3d6b2..3338a4c5141b 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/KernelContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/KernelContent.cs @@ -17,7 +17,7 @@ namespace Microsoft.SemanticKernel; #pragma warning disable SKEXP0001 [JsonDerivedType(typeof(AudioContent), typeDiscriminator: nameof(AudioContent))] [JsonDerivedType(typeof(FunctionCallRequestContent), typeDiscriminator: nameof(FunctionCallRequestContent))] -[JsonDerivedType(typeof(FunctionResultContent), typeDiscriminator: nameof(FunctionResultContent))] +[JsonDerivedType(typeof(FunctionCallResultContent), typeDiscriminator: nameof(FunctionCallResultContent))] #pragma warning restore SKEXP0001 public abstract class KernelContent { diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs index c79106418590..cb599f389399 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs @@ -192,7 +192,7 @@ public void ItCanBeSerializeAndDeserialized() items.Add(new FunctionCallRequestContent("function-name", "plugin-name", "function-id", new KernelArguments { ["parameter"] = "argument" })); - items.Add(new FunctionResultContent(new FunctionCallRequestContent("function-name", "plugin-name", "function-id"), "function-result")); + items.Add(new FunctionCallResultContent(new FunctionCallRequestContent("function-name", "plugin-name", "function-id"), "function-result")); var sut = new ChatMessageContent(AuthorRole.User, items: items, "message-model", metadata: new Dictionary() { @@ -283,7 +283,7 @@ public void ItCanBeSerializeAndDeserialized() Assert.Single(functionCallContent.Arguments); Assert.Equal("argument", functionCallContent.Arguments["parameter"]?.ToString()); - var functionResultContent = deserializedMessage.Items[7] as FunctionResultContent; + var functionResultContent = deserializedMessage.Items[7] as FunctionCallResultContent; Assert.NotNull(functionResultContent); Assert.Equal("function-result", functionResultContent.Result?.ToString()); Assert.Equal("function-name", functionResultContent.FunctionName); diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallResultContentTests.cs similarity index 75% rename from dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs rename to dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallResultContentTests.cs index 68c8808a5c57..64f68aa476f6 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionResultContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallResultContentTests.cs @@ -5,11 +5,11 @@ using Xunit; namespace SemanticKernel.UnitTests.Contents; -public class FunctionResultContentTests +public class FunctionCallResultContentTests { private readonly FunctionCallRequestContent _callContent; - public FunctionResultContentTests() + public FunctionCallResultContentTests() { this._callContent = new FunctionCallRequestContent("f1", "p1", "id", []); } @@ -18,7 +18,7 @@ public FunctionResultContentTests() public void ItShouldHaveFunctionIdInitialized() { // Arrange & act - var sut = new FunctionResultContent(this._callContent, "result"); + var sut = new FunctionCallResultContent(this._callContent, "result"); // Assert Assert.Equal("id", sut.Id); @@ -28,7 +28,7 @@ public void ItShouldHaveFunctionIdInitialized() public void ItShouldHavePluginNameInitialized() { // Arrange & act - var sut = new FunctionResultContent(this._callContent, "result"); + var sut = new FunctionCallResultContent(this._callContent, "result"); // Assert Assert.Equal("p1", sut.PluginName); @@ -38,7 +38,7 @@ public void ItShouldHavePluginNameInitialized() public void ItShouldHaveFunctionNameInitialized() { // Arrange & act - var sut = new FunctionResultContent(this._callContent, "result"); + var sut = new FunctionCallResultContent(this._callContent, "result"); // Assert Assert.Equal("f1", sut.FunctionName); @@ -48,7 +48,7 @@ public void ItShouldHaveFunctionNameInitialized() public void ItShouldHaveFunctionResultInitialized() { // Arrange & act - var sut = new FunctionResultContent(this._callContent, "result"); + var sut = new FunctionCallResultContent(this._callContent, "result"); // Assert Assert.Same("result", sut.Result); @@ -62,7 +62,7 @@ public void ItShouldHaveValueFromFunctionResultAsResultInitialized() var functionResult = new FunctionResult(function, "result"); - var sut = new FunctionResultContent(this._callContent, functionResult); + var sut = new FunctionCallResultContent(this._callContent, functionResult); // Assert Assert.Equal("result", sut.Result); @@ -72,12 +72,12 @@ public void ItShouldHaveValueFromFunctionResultAsResultInitialized() public void ItShouldBeSerializableAndDeserializable() { // Arrange - var sut = new FunctionResultContent(this._callContent, "result"); + var sut = new FunctionCallResultContent(this._callContent, "result"); // Act var json = JsonSerializer.Serialize(sut); - var deserializedSut = JsonSerializer.Deserialize(json); + var deserializedSut = JsonSerializer.Deserialize(json); // Assert Assert.NotNull(deserializedSut); From 8e2ae55f67305656b24ecd440a064398910d3e86 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 9 Apr 2024 21:56:31 +0100 Subject: [PATCH 28/90] More efficient handling of the FunctionCallResultContent items --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 29 +++++---- .../Connectors/OpenAI/OpenAIToolsTests.cs | 63 +++++++++++++++++++ 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index e6b3749a3591..e77789ef655d 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -999,24 +999,29 @@ private static IEnumerable GetRequestMessages(ChatMessageCon // Handling function call results represented by the FunctionResultContent type. // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) - var resultContents = message.Items.OfType().ToArray(); - if (resultContents.Length != 0) + List? toolMessages = null; + foreach (var item in message.Items) { - var toolMessages = new List(); - - foreach (var resultContent in resultContents) + if (item is not FunctionCallResultContent resultContent) { - if (resultContent.Result is Exception ex) - { - toolMessages.Add(new ChatRequestToolMessage($"Error: Exception while invoking function. {ex.Message}", resultContent.Id)); - continue; - } + continue; + } - var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); + toolMessages ??= new(); - toolMessages.Add(new ChatRequestToolMessage(stringResult ?? string.Empty, resultContent.Id)); + if (resultContent.Result is Exception ex) + { + toolMessages.Add(new ChatRequestToolMessage($"Error: Exception while invoking function. {ex.Message}", resultContent.Id)); + continue; } + var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); + + toolMessages.Add(new ChatRequestToolMessage(stringResult ?? string.Empty, resultContent.Id)); + } + + if (toolMessages is not null) + { return toolMessages; } diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index a7b2cd1a532e..13e89a2a187f 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -388,6 +388,69 @@ public async Task ItFailsIfNoFunctionResultProvidedAsync() Assert.Contains("'tool_calls' must be followed by tool", exception.Message, StringComparison.InvariantCulture); } + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFunctionCallingAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Assert + Assert.Equal(5, chatHistory.Count); + + var userMessage = chatHistory[0]; + Assert.Equal(AuthorRole.User, userMessage.Role); + + // LLM requested the current time. + var getCurrentTimeFunctionCallRequestMessage = chatHistory[1]; + Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallRequestMessage.Role); + + var getCurrentTimeFunctionCallRequest = getCurrentTimeFunctionCallRequestMessage.Items.OfType().Single(); + Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallRequest.PluginName); + Assert.NotNull(getCurrentTimeFunctionCallRequest.Id); + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + var getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionCallResultMessage.Role); + Assert.Single(getCurrentTimeFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. + + var getCurrentTimeFunctionCallResult = getCurrentTimeFunctionCallResultMessage.Items.OfType().Single(); + Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallResult.FunctionName); + Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallResult.PluginName); + Assert.Equal(getCurrentTimeFunctionCallRequest.Id, getCurrentTimeFunctionCallResult.Id); + Assert.NotNull(getCurrentTimeFunctionCallResult.Result); + + // LLM requested the weather for Boston. + var getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; + Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallRequestMessage.Role); + + var getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); + Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + var getWeatherForCityFunctionCallResultMessage = chatHistory[4]; + Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionCallResultMessage.Role); + Assert.Single(getWeatherForCityFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. + + var getWeatherForCityFunctionCallResult = getWeatherForCityFunctionCallResultMessage.Items.OfType().Single(); + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallResult.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallResult.PluginName); + Assert.Equal(getWeatherForCityFunctionCallRequest.Id, getWeatherForCityFunctionCallResult.Id); + Assert.NotNull(getWeatherForCityFunctionCallResult.Result); + } + private Kernel InitializeKernel(bool importHelperPlugin = false) { OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("Planners:OpenAI").Get(); From b66874fad6957c03b5e5f8e0f6354278e563dfd3 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 10 Apr 2024 14:17:09 +0100 Subject: [PATCH 29/90] Preserving function call request argument deserialization exception to allow callers to handle it. --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 16 +++++++++------ .../AzureOpenAIChatCompletionServiceTests.cs | 3 +++ .../OpenAIChatCompletionServiceTests.cs | 3 +++ .../Contents/FunctionCallRequestContent.cs | 11 ++++++++++ .../FunctionCallRequestContentTests.cs | 20 +++++++++++++++++++ 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index e77789ef655d..ae3926e72f57 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1076,7 +1076,7 @@ private static IEnumerable GetRequestMessages(ChatMessageCon } } - // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallContent type. + // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallRequestContent type. var functionCallRequests = message.Items.OfType().ToArray(); if (functionCallRequests.Length != 0) { @@ -1141,10 +1141,11 @@ private OpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompl foreach (var toolCall in chatChoice.Message.ToolCalls) { - // Adding items of 'FunctionCallContent' type to the 'Items' collection even though the function calls are available via the 'ToolCalls' property. + // Adding items of 'FunctionCallRequestContent' type to the 'Items' collection even though the function calls are available via the 'ToolCalls' property. // This allows consumers to work with functions in an LLM-agnostic way. if (toolCall is ChatCompletionsFunctionToolCall functionToolCall) { + Exception? exception = null; KernelArguments? arguments = null; try { @@ -1152,12 +1153,12 @@ private OpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompl } catch (JsonException ex) { + exception = new KernelException("Error: Function call arguments were invalid JSON.", ex); + if (this.Logger.IsEnabled(LogLevel.Debug)) { this.Logger.LogDebug(ex, "Failed to deserialize function arguments ({FunctionName}/{FunctionId}).", functionToolCall.Name, functionToolCall.Id); } - // If the arguments are not valid JSON, we'll just leave them as null. - // The original arguments and function tool call will be available via the 'InnerContent' property for the connector caller to access. } var functionName = FunctionName.Parse(functionToolCall.Name, OpenAIFunction.NameSeparator); @@ -1166,8 +1167,11 @@ private OpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompl functionName: functionName.Name, pluginName: functionName.PluginName, id: functionToolCall.Id, - arguments: arguments); - functionCallRequestContent.InnerContent = functionToolCall; + arguments: arguments) + { + InnerContent = functionToolCall, + Exception = exception + }; message.Items.Add(functionCallRequestContent); } diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index 15fc239d48e6..1d0b94a6a151 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -750,6 +750,9 @@ public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfT Assert.Equal("MyPlugin", invalidArgumentsFunctionCall.PluginName); Assert.Equal("4", invalidArgumentsFunctionCall.Id); Assert.Null(invalidArgumentsFunctionCall.Arguments); + Assert.NotNull(invalidArgumentsFunctionCall.Exception); + Assert.Equal("Error: Function call arguments were invalid JSON.", invalidArgumentsFunctionCall.Exception.Message); + Assert.NotNull(invalidArgumentsFunctionCall.Exception.InnerException); } [Fact] diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs index 0e41aa37561d..0cb54d4fee72 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs @@ -373,6 +373,9 @@ public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfT Assert.Equal("MyPlugin", invalidArgumentsFunctionCall.PluginName); Assert.Equal("4", invalidArgumentsFunctionCall.Id); Assert.Null(invalidArgumentsFunctionCall.Arguments); + Assert.NotNull(invalidArgumentsFunctionCall.Exception); + Assert.Equal("Error: Function call arguments were invalid JSON.", invalidArgumentsFunctionCall.Exception.Message); + Assert.NotNull(invalidArgumentsFunctionCall.Exception.InnerException); } [Fact] diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs index 70ca320d0a7a..2ae15db2187b 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; @@ -34,6 +35,11 @@ public sealed class FunctionCallRequestContent : KernelContent /// public KernelArguments? Arguments { get; } + /// + /// The exception that occurred while mapping original LLM function call request to the model class. + /// + public Exception? Exception { get; init; } + /// /// Creates a new instance of the class. /// @@ -62,6 +68,11 @@ public async Task InvokeAsync(Kernel kernel, Cancella { Verify.NotNull(kernel, nameof(kernel)); + if (this.Exception is not null) + { + return new FunctionCallResultContent(this, this.Exception.Message); + } + if (kernel.Plugins.TryGetFunction(this.PluginName, this.FunctionName, out KernelFunction? function)) { var result = await function.InvokeAsync(kernel, this.Arguments, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallRequestContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallRequestContentTests.cs index 76aa25735a4b..98be7ae1bfcc 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallRequestContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallRequestContentTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; using System.Threading.Tasks; using Xunit; @@ -53,4 +54,23 @@ public async Task ItShouldFindKernelFunctionAndInvokeItAsync() Assert.Equal("result", resultContent.Result); Assert.Same(this._arguments, actualArguments); } + + [Fact] + public async Task ItShouldHandleFunctionCallRequestExceptionAsync() + { + // Arrange + var kernel = new Kernel(); + + var sut = new FunctionCallRequestContent("f1", "p1", "id") + { + Exception = new JsonException("Error: Function call arguments were invalid JSON.") + }; + + // Act + var resultContent = await sut.InvokeAsync(kernel); + + // Assert + Assert.NotNull(resultContent); + Assert.Equal("Error: Function call arguments were invalid JSON.", resultContent.Result); + } } From d015196fc4b9317d79e8b432975487365ccdeac8 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 10 Apr 2024 16:29:30 +0100 Subject: [PATCH 30/90] ClientCore refactored not to add funciton calls and function results into chat messages metadata. --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 12 ++--- .../AzureSdk/OpenAIChatMessageContent.cs | 44 ++----------------- .../Contents/FunctionCallResultContent.cs | 4 +- 3 files changed, 12 insertions(+), 48 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index ae3926e72f57..1ba05a81c1f8 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -442,15 +442,17 @@ static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory c chatOptions.Messages.Add(new ChatRequestToolMessage(result, toolCall.Id)); // Add the tool response message to the chat history. - var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); + var message = new ChatMessageContent(role: AuthorRole.Tool, content: null); if (toolCall is ChatCompletionsFunctionToolCall functionCall) { - // 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, OpenAIFunction.NameSeparator); message.Items.Add(new FunctionCallResultContent(functionName.Name, functionName.PluginName, functionCall.Id, result)); } + else + { + message.Items.Add(new FunctionCallResultContent(id: toolCall.Id, result: result)); + } chat.Add(message); } @@ -997,8 +999,8 @@ private static IEnumerable GetRequestMessages(ChatMessageCon return new[] { new ChatRequestToolMessage(message.Content, toolIdString) }; } - // Handling function call results represented by the FunctionResultContent type. - // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) + // Handling function call results represented by the FunctionCallResultContent type. + // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionCallResultContent(functionCall, result) }) List? toolMessages = null; foreach (var item in message.Items) { diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIChatMessageContent.cs index bad2f3ae2a9f..902d7b7b2e18 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIChatMessageContent.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Azure.AI.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; @@ -27,7 +26,7 @@ public sealed class OpenAIChatMessageContent : ChatMessageContent /// Initializes a new instance of the class. /// internal OpenAIChatMessageContent(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)) + : base(new AuthorRole(chatMessage.Role.ToString()), chatMessage.Content, modelId, chatMessage, System.Text.Encoding.UTF8, metadata) { this.ToolCalls = chatMessage.ToolCalls; } @@ -36,7 +35,7 @@ internal OpenAIChatMessageContent(ChatResponseMessage chatMessage, string modelI /// Initializes a new instance of the class. /// internal OpenAIChatMessageContent(ChatRole 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)) + : base(new AuthorRole(role.ToString()), content, modelId, content, System.Text.Encoding.UTF8, metadata) { this.ToolCalls = toolCalls; } @@ -45,7 +44,7 @@ internal OpenAIChatMessageContent(ChatRole role, string? content, string modelId /// Initializes a new instance of the class. /// internal OpenAIChatMessageContent(AuthorRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) - : base(role, content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) + : base(role, content, modelId, content, System.Text.Encoding.UTF8, metadata) { this.ToolCalls = toolCalls; } @@ -78,41 +77,4 @@ public IReadOnlyList GetOpenAIFunctionToolCalls() return Array.Empty(); } - - private static IReadOnlyDictionary? CreateMetadataDictionary( - IReadOnlyList toolCalls, - IReadOnlyDictionary? original) - { - // We only need to augment the metadata if there are any tool calls. - if (toolCalls.Count > 0) - { - Dictionary newDictionary; - if (original is null) - { - // There's no existing metadata to clone; just allocate a new dictionary. - newDictionary = new Dictionary(1); - } - else if (original is IDictionary origIDictionary) - { - // Efficiently clone the old dictionary to a new one. - newDictionary = new Dictionary(origIDictionary); - } - else - { - // There's metadata to clone but we have to do so one item at a time. - newDictionary = new Dictionary(original.Count + 1); - foreach (var kvp in original) - { - newDictionary[kvp.Key] = kvp.Value; - } - } - - // Add the additional entry. - newDictionary.Add(FunctionToolCallsProperty, toolCalls.OfType().ToList()); - - return newDictionary; - } - - return original; - } } diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs index c3e23bd5bc45..284671a43259 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs @@ -26,7 +26,7 @@ public sealed class FunctionCallResultContent : KernelContent /// /// The function name. /// - public string FunctionName { get; } + public string? FunctionName { get; } /// /// The result of the function call, the function invocation exception or the custom error message. @@ -42,7 +42,7 @@ public sealed class FunctionCallResultContent : KernelContent /// The function call ID. /// The function result. [JsonConstructor] - public FunctionCallResultContent(string functionName, string? pluginName = null, string? id = null, object? result = null) + public FunctionCallResultContent(string? functionName = null, string? pluginName = null, string? id = null, object? result = null) { this.FunctionName = functionName; this.PluginName = pluginName; From f9aed8dab61415fba09fe12da56a802a834deec3 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 10 Apr 2024 17:06:56 +0100 Subject: [PATCH 31/90] Changes of the OpenAIChatMessageContent class are rolled back because the code is used by streaming api that does not support the new function calling model yet. --- .../AzureSdk/OpenAIChatMessageContent.cs | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIChatMessageContent.cs index 902d7b7b2e18..bad2f3ae2a9f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIChatMessageContent.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Azure.AI.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; @@ -26,7 +27,7 @@ public sealed class OpenAIChatMessageContent : ChatMessageContent /// Initializes a new instance of the class. /// internal OpenAIChatMessageContent(ChatResponseMessage chatMessage, string modelId, IReadOnlyDictionary? metadata = null) - : base(new AuthorRole(chatMessage.Role.ToString()), chatMessage.Content, modelId, chatMessage, System.Text.Encoding.UTF8, metadata) + : base(new AuthorRole(chatMessage.Role.ToString()), chatMessage.Content, modelId, chatMessage, System.Text.Encoding.UTF8, CreateMetadataDictionary(chatMessage.ToolCalls, metadata)) { this.ToolCalls = chatMessage.ToolCalls; } @@ -35,7 +36,7 @@ internal OpenAIChatMessageContent(ChatResponseMessage chatMessage, string modelI /// Initializes a new instance of the class. /// internal OpenAIChatMessageContent(ChatRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) - : base(new AuthorRole(role.ToString()), content, modelId, content, System.Text.Encoding.UTF8, metadata) + : base(new AuthorRole(role.ToString()), content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) { this.ToolCalls = toolCalls; } @@ -44,7 +45,7 @@ internal OpenAIChatMessageContent(ChatRole role, string? content, string modelId /// Initializes a new instance of the class. /// internal OpenAIChatMessageContent(AuthorRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) - : base(role, content, modelId, content, System.Text.Encoding.UTF8, metadata) + : base(role, content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) { this.ToolCalls = toolCalls; } @@ -77,4 +78,41 @@ public IReadOnlyList GetOpenAIFunctionToolCalls() return Array.Empty(); } + + private static IReadOnlyDictionary? CreateMetadataDictionary( + IReadOnlyList toolCalls, + IReadOnlyDictionary? original) + { + // We only need to augment the metadata if there are any tool calls. + if (toolCalls.Count > 0) + { + Dictionary newDictionary; + if (original is null) + { + // There's no existing metadata to clone; just allocate a new dictionary. + newDictionary = new Dictionary(1); + } + else if (original is IDictionary origIDictionary) + { + // Efficiently clone the old dictionary to a new one. + newDictionary = new Dictionary(origIDictionary); + } + else + { + // There's metadata to clone but we have to do so one item at a time. + newDictionary = new Dictionary(original.Count + 1); + foreach (var kvp in original) + { + newDictionary[kvp.Key] = kvp.Value; + } + } + + // Add the additional entry. + newDictionary.Add(FunctionToolCallsProperty, toolCalls.OfType().ToList()); + + return newDictionary; + } + + return original; + } } From 4bdf39524b2b093a5b4135bb46e844a0c173975e Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 10 Apr 2024 19:18:20 +0100 Subject: [PATCH 32/90] 1. More optimal deduplication mechanism for function call requests added. 2. Rolled back functionality that adds tool id metadata to chat message for auto funciton calling to avoid potential breaking changes. --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 1ba05a81c1f8..1b3bdac6902f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -442,17 +442,15 @@ static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory c chatOptions.Messages.Add(new ChatRequestToolMessage(result, toolCall.Id)); // Add the tool response message to the chat history. - var message = new ChatMessageContent(role: AuthorRole.Tool, content: null); + var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); if (toolCall is ChatCompletionsFunctionToolCall functionCall) { + // 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, OpenAIFunction.NameSeparator); message.Items.Add(new FunctionCallResultContent(functionName.Name, functionName.PluginName, functionCall.Id, result)); } - else - { - message.Items.Add(new FunctionCallResultContent(id: toolCall.Id, result: result)); - } chat.Add(message); } @@ -1078,28 +1076,30 @@ private static IEnumerable GetRequestMessages(ChatMessageCon } } - // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallRequestContent type. - var functionCallRequests = message.Items.OfType().ToArray(); - if (functionCallRequests.Length != 0) + if (tools is not null) { - var ftcs = new List(tools ?? Enumerable.Empty()); + asstMessage.ToolCalls.AddRange(tools); + } - foreach (var fcRequest in functionCallRequests) + // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallRequestContent type. + HashSet? functionCallIds = null; + foreach (var item in message.Items) + { + if (item is not FunctionCallRequestContent callRequest) { - if (!ftcs.Any(ftc => ftc.Id == fcRequest.Id)) - { - var argument = JsonSerializer.Serialize(fcRequest.Arguments); + continue; + } - ftcs.Add(new ChatCompletionsFunctionToolCall(fcRequest.Id, FunctionName.ToFullyQualifiedName(fcRequest.FunctionName, fcRequest.PluginName, OpenAIFunction.NameSeparator), argument ?? string.Empty)); - } + functionCallIds ??= new HashSet(asstMessage.ToolCalls.Select(t => t.Id)); + + if (callRequest.Id is null || functionCallIds.Contains(callRequest.Id)) + { + continue; } - tools = ftcs; - } + var argument = JsonSerializer.Serialize(callRequest.Arguments); - if (tools is not null) - { - asstMessage.ToolCalls.AddRange(tools); + asstMessage.ToolCalls.Add(new ChatCompletionsFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, OpenAIFunction.NameSeparator), argument ?? string.Empty)); } return new[] { asstMessage }; From e53dd2910a511451613382648847661b8b26e55a Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 10 Apr 2024 20:55:09 +0100 Subject: [PATCH 33/90] To convenience methods `FunctionCallResultContent.ToChatMessage` and `FunctionCallRequestContent.GetFunctionCalls` added --- .../Example59_OpenAIFunctionCalling.cs | 10 +++---- .../Contents/FunctionCallRequestContent.cs | 11 ++++++++ .../Contents/FunctionCallResultContent.cs | 10 +++++++ .../FunctionCallRequestContentTests.cs | 26 +++++++++++++++++++ .../FunctionCallResultContentTests.cs | 15 +++++++++++ 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index 3a4a449b8ef5..5c14f6bd863e 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -82,7 +82,7 @@ public async Task RunAsync() Write(result.Content); } - IEnumerable functionCalls = result.Items.OfType(); // Getting list of function calls. + IEnumerable functionCalls = FunctionCallRequestContent.GetFunctionCalls(result); if (!functionCalls.Any()) { break; @@ -94,17 +94,17 @@ public async Task RunAsync() { try { - FunctionCallResultContent functionResult = await functionCall.InvokeAsync(kernel); // Executing each function. + FunctionCallResultContent resultContent = await functionCall.InvokeAsync(kernel); // Executing each function. - chatHistory.Add(new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { functionResult })); // Adding function result to chat history. + chatHistory.Add(resultContent.ToChatMessage()); } catch (Exception ex) { - chatHistory.Add(new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionCallResultContent(functionCall, ex) })); // Adding function result to chat history. + chatHistory.Add(new FunctionCallResultContent(functionCall, ex).ToChatMessage()); // Adding function result to chat history. // Adding exception to chat history. // or //string message = "Error details that LLM can reason about."; - //chatHistory.Add(new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionResultContent(functionCall, message) })); + //chatHistory.Add(new FunctionCallResultContent(functionCall, message).ToChatMessageContent()); // Adding function result to chat history. } } diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs index 2ae15db2187b..38315750e23d 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -82,4 +83,14 @@ public async Task InvokeAsync(Kernel kernel, Cancella throw new KeyNotFoundException($"The plugin collection does not contain a plugin and/or function with the specified names. Plugin name - '{this.PluginName}', function name - '{this.FunctionName}'."); } + + /// + /// Returns list of function call requests provided via collection. + /// + /// The . + /// + public static IEnumerable GetFunctionCalls(ChatMessageContent messageContent) + { + return messageContent.Items.OfType(); + } } diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs index 284671a43259..8b544bb19631 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel; @@ -73,4 +74,13 @@ public FunctionCallResultContent(FunctionCallRequestContent functionCallContent, { this.InnerContent = result; } + + /// + /// Creates and adds the current instance of the class to the collection. + /// + /// The instance. + public ChatMessageContent ToChatMessage() + { + return new ChatMessageContent(AuthorRole.Tool, new ChatMessageContentItemCollection() { this }); + } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallRequestContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallRequestContentTests.cs index 98be7ae1bfcc..76a4f18bc154 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallRequestContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallRequestContentTests.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using Microsoft.SemanticKernel.ChatCompletion; using Xunit; namespace Microsoft.SemanticKernel.Contents; @@ -73,4 +75,28 @@ public async Task ItShouldHandleFunctionCallRequestExceptionAsync() Assert.NotNull(resultContent); Assert.Equal("Error: Function call arguments were invalid JSON.", resultContent.Result); } + + [Fact] + public void ItShouldReturnListOfFunctionCallRequests() + { + // Arrange + var functionCallRequestContents = new ChatMessageContentItemCollection + { + new FunctionCallRequestContent("f1", "p1", "id1", this._arguments), + new FunctionCallRequestContent("f2", "p2", "id2", this._arguments), + new FunctionCallRequestContent("f3", "p3", "id3", this._arguments) + }; + + var chatMessage = new ChatMessageContent(AuthorRole.Tool, functionCallRequestContents); + + // Act + var result = FunctionCallRequestContent.GetFunctionCalls(chatMessage).ToArray(); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Length); + Assert.Equal("id1", result.ElementAt(0).Id); + Assert.Equal("id2", result.ElementAt(1).Id); + Assert.Equal("id3", result.ElementAt(2).Id); + } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallResultContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallResultContentTests.cs index 64f68aa476f6..a6260be02ab9 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallResultContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallResultContentTests.cs @@ -86,4 +86,19 @@ public void ItShouldBeSerializableAndDeserializable() Assert.Equal(sut.FunctionName, deserializedSut.FunctionName); Assert.Equal(sut.Result, deserializedSut.Result?.ToString()); } + + [Fact] + public void ItShouldCreateChatMessageContent() + { + // Arrange + var sut = new FunctionCallResultContent(this._callContent, "result"); + + // Act + var chatMessageContent = sut.ToChatMessage(); + + // Assert + Assert.NotNull(chatMessageContent); + Assert.Single(chatMessageContent.Items); + Assert.Same(sut, chatMessageContent.Items[0]); + } } From b3d35182dd371db095f3488b579549c5b7afffc4 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 10 Apr 2024 22:16:10 +0100 Subject: [PATCH 34/90] Fix for test coverage and ignoring serialization of null properties --- dotnet/src/InternalUtilities/src/Functions/FunctionName.cs | 2 ++ .../Contents/FunctionCallRequestContent.cs | 4 ++++ .../Contents/FunctionCallResultContent.cs | 1 + 3 files changed, 7 insertions(+) diff --git a/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs b/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs index 5884cf179860..76f54de92a56 100644 --- a/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs +++ b/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs @@ -1,12 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.SemanticKernel; /// /// Represents a function name. /// +[ExcludeFromCodeCoverage] internal sealed class FunctionName { /// diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs index 38315750e23d..67c57ab59436 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallRequestContent.cs @@ -19,11 +19,13 @@ public sealed class FunctionCallRequestContent : KernelContent /// /// The function call ID. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Id { get; } /// /// The plugin name. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? PluginName { get; } /// @@ -34,11 +36,13 @@ public sealed class FunctionCallRequestContent : KernelContent /// /// The kernel arguments. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public KernelArguments? Arguments { get; } /// /// The exception that occurred while mapping original LLM function call request to the model class. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Exception? Exception { get; init; } /// diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs index 8b544bb19631..c118430a4e0f 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallResultContent.cs @@ -27,6 +27,7 @@ public sealed class FunctionCallResultContent : KernelContent /// /// The function name. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? FunctionName { get; } /// From c179a6f54f4c16e577ea163e03b29e83fc8aa3fb Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 12 Apr 2024 19:52:46 +0100 Subject: [PATCH 35/90] Simulated function example added + integration tests updated to use new convenience methods. --- .../Example59_OpenAIFunctionCalling.cs | 46 ++++++++++++++++++- .../Connectors/OpenAI/OpenAIToolsTests.cs | 18 ++++---- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index 5c14f6bd863e..8e832d0017e3 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -112,8 +112,52 @@ public async Task RunAsync() } } + WriteLine("======== Example 4: Simulated function calling with a non-streaming prompt ========"); + { + var chat = kernel.GetRequiredService(); + + OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + while (true) + { + ChatMessageContent result = await chat.GetChatMessageContentAsync(chatHistory, settings, kernel); + if (result.Content is not null) + { + Write(result.Content); + } + + chatHistory.Add(result); // Adding LLM response containing function calls(requests) to chat history as it's required by LLMs. + + IEnumerable functionCalls = FunctionCallRequestContent.GetFunctionCalls(result); + if (!functionCalls.Any()) + { + break; + } + + foreach (var functionCall in functionCalls) + { + FunctionCallResultContent resultContent = await functionCall.InvokeAsync(kernel); // Executing each function. + + chatHistory.Add(resultContent.ToChatMessage()); + } + + // Adding a simulated function call request to the connector response message + var simulatedFunctionCall = new FunctionCallRequestContent("weather-alert", id: "call_123"); + result.Items.Add(simulatedFunctionCall); + + // Adding a simulated function result to chat history + var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; + chatHistory.Add(new FunctionCallResultContent(simulatedFunctionCall, simulatedFunctionResult).ToChatMessage()); + + WriteLine(); + } + } + /* Uncomment this to try in a console chat loop. - Console.WriteLine("======== Example 4: Use automated function calling with a streaming chat ========"); + Console.WriteLine("======== Example 5: Use automated function calling with a streaming chat ========"); { OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; var chat = kernel.GetRequiredService(); diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index 13e89a2a187f..47e87d8a8abb 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -248,7 +248,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManual // Act var messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - var functionCalls = messageContent.Items.OfType().ToArray(); + var functionCalls = FunctionCallRequestContent.GetFunctionCalls(messageContent).ToArray(); while (functionCalls.Length != 0) { @@ -260,12 +260,12 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManual { var result = await functionCall.InvokeAsync(kernel); - chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { result }); + chatHistory.Add(result.ToChatMessage()); } // Sending the functions invocation results to the LLM to get the final response messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = messageContent.Items.OfType().ToArray(); + functionCalls = FunctionCallRequestContent.GetFunctionCalls(messageContent).ToArray(); } // Assert @@ -288,7 +288,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc // Act var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - var functionCalls = messageContent.Items.OfType().ToArray(); + var functionCalls = FunctionCallRequestContent.GetFunctionCalls(messageContent).ToArray(); while (functionCalls.Length != 0) { @@ -301,12 +301,12 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc // Simulating an exception var exception = new OperationCanceledException("The operation was canceled due to timeout."); - chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionCallResultContent(functionCall, exception) }); + chatHistory.Add(new FunctionCallResultContent(functionCall, exception).ToChatMessage()); } // Sending the functions execution results back to the LLM to get the final response messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = messageContent.Items.OfType().ToArray(); + functionCalls = FunctionCallRequestContent.GetFunctionCalls(messageContent).ToArray(); } // Assert @@ -332,7 +332,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu // Act var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - var functionCalls = messageContent.Items.OfType().ToArray(); + var functionCalls = FunctionCallRequestContent.GetFunctionCalls(messageContent).ToArray(); while (functionCalls.Length > 0) { @@ -353,11 +353,11 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu // Adding a simulated function result to chat history var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; - chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { new FunctionCallResultContent(simulatedFunctionCall, simulatedFunctionResult) }); + chatHistory.Add(new FunctionCallResultContent(simulatedFunctionCall, simulatedFunctionResult).ToChatMessage()); // Sending the functions invocation results back to the LLM to get the final response messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = messageContent.Items.OfType().ToArray(); + functionCalls = FunctionCallRequestContent.GetFunctionCalls(messageContent).ToArray(); } // Assert From 2e9efa9246e7e66c38316fc2fc36722cda220e56 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 15 Apr 2024 09:18:07 +0100 Subject: [PATCH 36/90] Update dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- .../src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs index d9882cae8328..9cac9d1384d7 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/FunctionNameTests.cs @@ -4,6 +4,7 @@ using Xunit; namespace SemanticKernel.UnitTests.Utilities; + public class FunctionNameTests { [Fact] From cd607bc3ac4a3bdc738336bd3c3cb03b1c279a78 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 15 Apr 2024 09:18:24 +0100 Subject: [PATCH 37/90] Update dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallResultContentTests.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- .../Contents/FunctionCallResultContentTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallResultContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallResultContentTests.cs index a6260be02ab9..4986c0e31d54 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallResultContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallResultContentTests.cs @@ -5,6 +5,7 @@ using Xunit; namespace SemanticKernel.UnitTests.Contents; + public class FunctionCallResultContentTests { private readonly FunctionCallRequestContent _callContent; From db1b3536eec4752bec80380a34b23fcdd54a2ef7 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 15 Apr 2024 09:19:20 +0100 Subject: [PATCH 38/90] Update dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- .../OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs index 78d9fdcf0e79..a6d68954c270 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs @@ -324,7 +324,7 @@ public async Task GetChatMessageContentsWithChatMessageContentItemCollectionAndS } [Fact] - public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfTypeFunctionCallContentAsync() + public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfTypeFunctionCallRequestContentAsync() { // Arrange this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) From 2c4e4825d74d5b4b1db8a74c9a717f268356d78b Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:54:10 +0100 Subject: [PATCH 39/90] Update dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index d26dbf6e88c5..083d7218be9d 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1007,7 +1007,7 @@ private static IEnumerable GetRequestMessages(ChatMessageCon continue; } - toolMessages ??= new(); + toolMessages ??= []; if (resultContent.Result is Exception ex) { From 29b8d41b313c8d690dd05d61d11524f2d6f45ca4 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:54:25 +0100 Subject: [PATCH 40/90] Update dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- .../ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index d1c015c7be76..e4d051d2d2e7 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -701,7 +701,7 @@ public async Task GetChatMessageContentsWithChatMessageContentItemCollectionAndS } [Fact] - public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfTypeFunctionCallContentAsync() + public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfTypeFunctionCallRequestContentAsync() { // Arrange this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) From 69a8437a289c2482f2564b2b2c29f0a90122edbe Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 15 Apr 2024 19:15:48 +0100 Subject: [PATCH 41/90] Fix to support non-string funciton arguments in additiojn to string ones. --- .../Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs | 7 +++++++ .../AzureOpenAIChatCompletionServiceTests.cs | 9 ++++++++- .../ChatCompletion/OpenAIChatCompletionServiceTests.cs | 9 ++++++++- ...completion_multiple_function_calls_test_response.json | 8 ++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 083d7218be9d..2c0e1c888944 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1152,6 +1152,13 @@ private OpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompl try { arguments = JsonSerializer.Deserialize(functionToolCall.Arguments); + if (arguments is not null) + { + foreach (var argument in arguments) + { + arguments[argument.Key] = argument.Value?.ToString(); + } + } } catch (JsonException ex) { diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index d0c4672611de..d80f31dc2caf 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -721,7 +721,7 @@ public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfT // Assert Assert.NotNull(result); - Assert.Equal(4, result.Items.Count); + Assert.Equal(5, result.Items.Count); var getCurrentWeatherFunctionCall = result.Items[0] as FunctionCallRequestContent; Assert.NotNull(getCurrentWeatherFunctionCall); @@ -753,6 +753,13 @@ public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfT Assert.NotNull(invalidArgumentsFunctionCall.Exception); Assert.Equal("Error: Function call arguments were invalid JSON.", invalidArgumentsFunctionCall.Exception.Message); Assert.NotNull(invalidArgumentsFunctionCall.Exception.InnerException); + + var intArgumentsFunctionCall = result.Items[4] as FunctionCallRequestContent; + Assert.NotNull(intArgumentsFunctionCall); + Assert.Equal("IntArguments", intArgumentsFunctionCall.FunctionName); + Assert.Equal("MyPlugin", intArgumentsFunctionCall.PluginName); + Assert.Equal("5", intArgumentsFunctionCall.Id); + Assert.Equal("36", intArgumentsFunctionCall.Arguments?["age"]?.ToString()); } [Fact] diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs index 8f096792edf2..d0a858538292 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs @@ -344,7 +344,7 @@ public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfT // Assert Assert.NotNull(result); - Assert.Equal(4, result.Items.Count); + Assert.Equal(5, result.Items.Count); var getCurrentWeatherFunctionCall = result.Items[0] as FunctionCallRequestContent; Assert.NotNull(getCurrentWeatherFunctionCall); @@ -376,6 +376,13 @@ public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfT Assert.NotNull(invalidArgumentsFunctionCall.Exception); Assert.Equal("Error: Function call arguments were invalid JSON.", invalidArgumentsFunctionCall.Exception.Message); Assert.NotNull(invalidArgumentsFunctionCall.Exception.InnerException); + + var intArgumentsFunctionCall = result.Items[4] as FunctionCallRequestContent; + Assert.NotNull(intArgumentsFunctionCall); + Assert.Equal("IntArguments", intArgumentsFunctionCall.FunctionName); + Assert.Equal("MyPlugin", intArgumentsFunctionCall.PluginName); + Assert.Equal("5", intArgumentsFunctionCall.Id); + Assert.Equal("36", intArgumentsFunctionCall.Arguments?["age"]?.ToString()); } [Fact] diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_multiple_function_calls_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_multiple_function_calls_test_response.json index d339ae99b6ab..737b972309ba 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_multiple_function_calls_test_response.json +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_multiple_function_calls_test_response.json @@ -41,6 +41,14 @@ "name": "MyPlugin-InvalidArguments", "arguments": "invalid_arguments_format" } + }, + { + "id": "5", + "type": "function", + "function": { + "name": "MyPlugin-IntArguments", + "arguments": "{\n\"age\": 36\n}" + } } ] }, From f5ab032cd34844c56a0e25153ffb0a4973625d56 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 1 May 2024 10:02:08 +0100 Subject: [PATCH 42/90] POC with polymorphic deserialization of tool behaviors and function call behavior --- dotnet/SK-dotnet.sln | 6 + ...xample100_ToolCallBehaviorsInYamlPrompt.cs | 72 +++ .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 193 ++++-- .../OpenAIPromptExecutionSettings.cs | 6 +- .../KernelFunctionMarkdown.cs | 4 +- .../Functions/KernelFunctionMarkdownTests.cs | 95 ++- .../Yaml/Functions/KernelFunctionYamlTests.cs | 50 +- ...mptExecutionSettingsTypeConverterTests.cs} | 6 +- .../FunctionCallBehaviorTypeConverter.cs | 36 ++ .../Functions.Yaml/KernelFunctionYaml.cs | 2 +- ...PromptExecutionSettingsNodeDeserializer.cs | 43 -- .../PromptExecutionSettingsTypeConverter.cs | 57 ++ .../Connectors/OpenAI/OpenAIFunctionsTests.cs | 588 ++++++++++++++++++ .../Connectors/OpenAI/OpenAIToolsTests.cs | 48 ++ .../ToolBehaviorsResolver.cs | 46 ++ .../AI/PromptExecutionSettings.cs | 17 +- .../Choices/AutoFunctionCallChoice.cs | 87 +++ .../Choices/FunctionCallBehavior.cs | 10 + .../FunctionCallChoiceConfiguration.cs | 18 + .../Choices/FunctionCallChoiceContext.cs | 10 + .../Choices/NoneFunctionCallChoice.cs | 16 + .../Choices/RequiredFunctionCallChoice.cs | 81 +++ .../AI/ToolBehaviors/FunctionCallBehavior.cs | 36 ++ .../AI/ToolBehaviors/ToolBehavior.cs | 7 + .../PromptTemplate/PromptTemplateConfig.cs | 4 +- .../Functions/FunctionCallBehaviorTests.cs | 306 +++++++++ .../PromptTemplateConfigTests.cs | 139 +++++ 27 files changed, 1875 insertions(+), 108 deletions(-) create mode 100644 dotnet/samples/KernelSyntaxExamples/Example100_ToolCallBehaviorsInYamlPrompt.cs rename dotnet/src/Functions/Functions.UnitTests/Yaml/{PromptExecutionSettingsNodeDeserializerTests.cs => PromptExecutionSettingsTypeConverterTests.cs} (91%) create mode 100644 dotnet/src/Functions/Functions.Yaml/FunctionCallBehaviorTypeConverter.cs delete mode 100644 dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsNodeDeserializer.cs create mode 100644 dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs create mode 100644 dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorsResolver.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/AutoFunctionCallChoice.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallBehavior.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceConfiguration.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceContext.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/NoneFunctionCallChoice.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/RequiredFunctionCallChoice.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/FunctionCallBehavior.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/ToolBehavior.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallBehaviorTests.cs diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index e4ee2a25b19c..0c5555a89bcf 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -110,6 +110,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Diagnostics", "Diagnostics" src\InternalUtilities\src\Diagnostics\Verify.cs = src\InternalUtilities\src\Diagnostics\Verify.cs EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PromptSerialization", "PromptSerialization", "{6A4C2EAA-E1B4-4F33-A4E6-21ED36413919}" + ProjectSection(SolutionItems) = preProject + src\InternalUtilities\src\PromptSerialization\ToolBehaviorsResolver.cs = src\InternalUtilities\src\PromptSerialization\ToolBehaviorsResolver.cs + EndProjectSection +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linq", "Linq", "{B00AD427-0047-4850-BEF9-BA8237EA9D8B}" ProjectSection(SolutionItems) = preProject src\InternalUtilities\src\Linq\AsyncEnumerable.cs = src\InternalUtilities\src\Linq\AsyncEnumerable.cs @@ -650,6 +655,7 @@ Global {5C246969-D794-4EC3-8E8F-F90D4D166420} = {4D3DAE63-41C6-4E1C-A35A-E77BDFC40675} {958AD708-F048-4FAF-94ED-D2F2B92748B9} = {4D3DAE63-41C6-4E1C-A35A-E77BDFC40675} {29E7D971-1308-4171-9872-E8E4669A1134} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} + {6A4C2EAA-E1B4-4F33-A4E6-21ED36413919} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} {B00AD427-0047-4850-BEF9-BA8237EA9D8B} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} {1C19D805-3573-4477-BF07-40180FCDE1BD} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} {3CDE10B2-AE8F-4FC4-8D55-92D4AD32E144} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} diff --git a/dotnet/samples/KernelSyntaxExamples/Example100_ToolCallBehaviorsInYamlPrompt.cs b/dotnet/samples/KernelSyntaxExamples/Example100_ToolCallBehaviorsInYamlPrompt.cs new file mode 100644 index 000000000000..f571ffd4f79c --- /dev/null +++ b/dotnet/samples/KernelSyntaxExamples/Example100_ToolCallBehaviorsInYamlPrompt.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Examples; +using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; + +namespace GettingStarted; + +/// +/// This example shows how to create a prompt from a YAML resource. +/// +public sealed class Example100_ToolCallBehaviorsInYamlPrompt(ITestOutputHelper output) : BaseTest(output) +{ + /// + /// Show how to create a prompt from a YAML resource. + /// + [Fact] + public async Task RunAsync() + { + // Create a kernel with OpenAI chat completion + Kernel kernel = Kernel.CreateBuilder() + .AddOpenAIChatCompletion( + modelId: TestConfiguration.OpenAI.ChatModelId, + apiKey: TestConfiguration.OpenAI.ApiKey) + .Build(); + + // Add a plugin with some helper functions we want to allow the model to utilize. + kernel.ImportPluginFromFunctions("HelperFunctions", + [ + kernel.CreateFunctionFromMethod(() => DateTime.UtcNow.ToString("R"), "GetCurrentUtcTime", "Retrieves the current time in UTC."), + kernel.CreateFunctionFromMethod((string cityName) => + cityName switch + { + "Boston" => "61 and rainy", + "London" => "55 and cloudy", + "Miami" => "80 and sunny", + "Paris" => "60 and rainy", + "Tokyo" => "50 and sunny", + "Sydney" => "75 and sunny", + "Tel Aviv" => "80 and sunny", + _ => "31 and snowing", + }, "Get_Weather_For_City", "Gets the current weather for the specified city"), + ]); + + var generateStoryYaml = + """ + name: GenerateStory + template: | + Given the current time of day and weather, what is the likely color of the sky in Boston? + template_format: semantic-kernel + description: A function that returns sky color in a city. + output_variable: + description: The sky color. + execution_settings: + default: + temperature: 0.4 + tool_behaviors: + - !function_call_behavior + choice: !auto + functions: + - HelperFunctions.Get_Weather_For_City + """; + var function = kernel.CreateFunctionFromPromptYaml(generateStoryYaml); + + var result = await kernel.InvokeAsync(function); + + WriteLine(result); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 2c0e1c888944..cc1532942656 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -17,6 +17,7 @@ using Azure.Core.Pipeline; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.AI.ToolBehaviors; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Http; @@ -306,13 +307,16 @@ internal async Task> GetChatMessageContentsAsy // Convert the incoming execution settings to OpenAI settings. OpenAIPromptExecutionSettings chatExecutionSettings = OpenAIPromptExecutionSettings.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 = CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); + var functionCallConfiguration = this.ConfigureFunctionCallingOptions(kernel, chatExecutionSettings, chatOptions, 0); + + bool autoInvoke = kernel is not null && functionCallConfiguration?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; + ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); + for (int iteration = 1; ; iteration++) { // Make the request. @@ -388,8 +392,7 @@ internal async Task> GetChatMessageContentsAsy // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, // 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)) + if ((!functionCallConfiguration?.AllowAnyRequestedKernelFunction ?? false) && !IsRequestableTool(chatOptions, openAIFunctionToolCall)) { AddResponseMessage(chatOptions, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); continue; @@ -424,7 +427,7 @@ internal async Task> GetChatMessageContentsAsy s_inflightAutoInvokes.Value--; } - var stringResult = ProcessFunctionResult(functionResult, chatExecutionSettings.ToolCallBehavior); + var stringResult = ProcessFunctionResult(functionResult, chatExecutionSettings); AddResponseMessage(chatOptions, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); @@ -457,26 +460,13 @@ static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory c } // Update tool use information for the next go-around based on having completed another iteration. - Debug.Assert(chatExecutionSettings.ToolCallBehavior is not null); + Debug.Assert(functionCallConfiguration 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 (iteration >= 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); - } + this.ConfigureFunctionCallingOptions(kernel, chatExecutionSettings, chatOptions, iteration); // 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. @@ -487,12 +477,12 @@ static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory c } // Disable auto invocation if we've exceeded the allowed limit. - if (iteration >= chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts) + if (iteration >= functionCallConfiguration?.MaximumAutoInvokeAttempts) { autoInvoke = false; if (this.Logger.IsEnabled(LogLevel.Debug)) { - this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); + this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", functionCallConfiguration?.MaximumAutoInvokeAttempts); } } } @@ -507,14 +497,15 @@ internal async IAsyncEnumerable GetStreamingC Verify.NotNull(chat); OpenAIPromptExecutionSettings chatExecutionSettings = OpenAIPromptExecutionSettings.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 = CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); + var functionCallConfiguration = this.ConfigureFunctionCallingOptions(kernel, chatExecutionSettings, chatOptions, 0); + + bool autoInvoke = kernel is not null && functionCallConfiguration?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; + ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); + StringBuilder? contentBuilder = null; Dictionary? toolCallIdsByIndex = null; Dictionary? functionNamesByIndex = null; @@ -613,8 +604,7 @@ internal async IAsyncEnumerable GetStreamingC // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, // 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)) + if ((!functionCallConfiguration?.AllowAnyRequestedKernelFunction ?? false) && !IsRequestableTool(chatOptions, openAIFunctionToolCall)) { AddResponseMessage(chatOptions, chat, streamedRole, toolCall, metadata, result: null, "Error: Function call request for a function that wasn't defined.", this.Logger); continue; @@ -649,7 +639,7 @@ internal async IAsyncEnumerable GetStreamingC s_inflightAutoInvokes.Value--; } - var stringResult = ProcessFunctionResult(functionResult, chatExecutionSettings.ToolCallBehavior); + var stringResult = ProcessFunctionResult(functionResult, chatExecutionSettings); AddResponseMessage(chatOptions, chat, streamedRole, toolCall, metadata, stringResult, errorMessage: null, this.Logger); @@ -671,45 +661,143 @@ static void AddResponseMessage( } // Update tool use information for the next go-around based on having completed another iteration. - Debug.Assert(chatExecutionSettings.ToolCallBehavior is not null); + Debug.Assert(functionCallConfiguration 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 (iteration >= chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts) + this.ConfigureFunctionCallingOptions(kernel, chatExecutionSettings, chatOptions, iteration); + + // 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 (iteration >= functionCallConfiguration?.MaximumAutoInvokeAttempts) + { + autoInvoke = false; + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", functionCallConfiguration?.MaximumAutoInvokeAttempts); + } + } + } + } + + private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts, int? MaximumUseAttempts)? ConfigureFunctionCallingOptions(Kernel? kernel, OpenAIPromptExecutionSettings executionSettings, ChatCompletionsOptions chatOptions, int iteration) + { + if (executionSettings.ToolBehaviors is not null && executionSettings.ToolCallBehavior is not null) + { + throw new ArgumentException("ToolBehaviors and ToolCallBehavior cannot be used together."); + } + + // Handling old-style tool call behavior represented by `OpenAIPromptExecutionSettings.ToolCallBehavior` property. + if (executionSettings.ToolCallBehavior is { } toolCallBehavior) + { + if (iteration >= 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); + this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tools.", 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); + 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) + return new() { - Debug.Assert(chatOptions.Tools.Count == 0); - chatOptions.Tools.Add(s_nonInvocableFunctionTool); + AllowAnyRequestedKernelFunction = toolCallBehavior.AllowAnyRequestedKernelFunction, + MaximumAutoInvokeAttempts = toolCallBehavior.MaximumAutoInvokeAttempts, + MaximumUseAttempts = toolCallBehavior.MaximumUseAttempts + }; + } + + // Handling new tool behavior represented by `PromptExecutionSettings.ToolBehaviors` property. + if (executionSettings.ToolBehaviors?.OfType() is { } functionCallBehaviors && functionCallBehaviors.Any()) + { + if (functionCallBehaviors.Count() > 1) + { + throw new KernelException("Only one function call behavior is allowed."); } - // Disable auto invocation if we've exceeded the allowed limit. - if (iteration >= chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts) + var functionCallBehavior = functionCallBehaviors.Single(); + + // Regenerate the tool list as necessary and getting other call behavior properties. The invocation of the function(s) could have augmented + // what functions are available in the kernel. + var config = functionCallBehavior.Choice.Configure(new() { Kernel = kernel, Model = chatOptions }); + if (config is null) { - autoInvoke = false; + return null; + } + + (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts, int? MaximumUseAttempts) result = new() + { + AllowAnyRequestedKernelFunction = config.AllowAnyRequestedKernelFunction, + MaximumAutoInvokeAttempts = config.MaximumAutoInvokeAttempts, + MaximumUseAttempts = config.MaximumUseAttempts + }; + + if (iteration >= config.MaximumUseAttempts) + { + // Don't add any tools as we've reached the maximum attempts limit. if (this.Logger.IsEnabled(LogLevel.Debug)) { - this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); + this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the functions.", config.MaximumUseAttempts); + } + + return result; + } + + // If we have a required function, it means we want to force LLM to invoke that function. + if (config.RequiredFunctions is { } requiredFunctions && requiredFunctions.Any()) + { + if (requiredFunctions.Count() > 1) + { + throw new KernelException("Only one required function is allowed."); + } + + var functionDefinition = requiredFunctions.First().ToOpenAIFunction().ToFunctionDefinition(); + + chatOptions.ToolChoice = new ChatCompletionsToolChoice(functionDefinition); + chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); + + return result; + } + + // If we have available functions, we want LLM to choose which function(s) to call. + if (config.AvailableFunctions is { } availableFunctions && availableFunctions.Any()) + { + chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; + + foreach (var function in availableFunctions) + { + var functionDefinition = function.ToOpenAIFunction().ToFunctionDefinition(); + chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); } + + return result; } + + // If we have neither required nor available functions, we don't want LLM to call any functions. + chatOptions.ToolChoice = ChatCompletionsToolChoice.None; + + return result; } + + // If no function call behavior is specified, we don't want LLM to call any functions. + chatOptions.ToolChoice = ChatCompletionsToolChoice.None; + + return null; } /// Checks if a tool call is for a function that was defined. @@ -922,7 +1010,6 @@ private static ChatCompletionsOptions CreateChatCompletionsOptions( break; } - executionSettings.ToolCallBehavior?.ConfigureOptions(kernel, options); if (executionSettings.TokenSelectionBiases is not null) { foreach (var keyValue in executionSettings.TokenSelectionBiases) @@ -941,12 +1028,12 @@ private static ChatCompletionsOptions CreateChatCompletionsOptions( if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) { - options.Messages.AddRange(GetRequestMessages(new ChatMessageContent(AuthorRole.System, executionSettings!.ChatSystemPrompt), executionSettings.ToolCallBehavior)); + options.Messages.AddRange(GetRequestMessages(new ChatMessageContent(AuthorRole.System, executionSettings!.ChatSystemPrompt), executionSettings)); } foreach (var message in chatHistory) { - options.Messages.AddRange(GetRequestMessages(message, executionSettings.ToolCallBehavior)); + options.Messages.AddRange(GetRequestMessages(message, executionSettings)); } return options; @@ -980,7 +1067,7 @@ private static ChatRequestMessage GetRequestMessage(ChatRole chatRole, string co throw new NotImplementedException($"Role {chatRole} is not implemented"); } - private static IEnumerable GetRequestMessages(ChatMessageContent message, ToolCallBehavior? toolCallBehavior) + private static IEnumerable GetRequestMessages(ChatMessageContent message, OpenAIPromptExecutionSettings executionSettings) { if (message.Role == AuthorRole.System) { @@ -1015,7 +1102,7 @@ private static IEnumerable GetRequestMessages(ChatMessageCon continue; } - var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); + var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, executionSettings); toolMessages.Add(new ChatRequestToolMessage(stringResult ?? string.Empty, resultContent.Id)); } @@ -1251,9 +1338,9 @@ private void CaptureUsageDetails(CompletionsUsage usage) /// Processes the function result. /// /// The result of the function call. - /// The ToolCallBehavior object containing optional settings like JsonSerializerOptions.TypeInfoResolver. + /// The prompt execution settings. /// A string representation of the function result. - private static string? ProcessFunctionResult(object functionResult, ToolCallBehavior? toolCallBehavior) + private static string? ProcessFunctionResult(object functionResult, OpenAIPromptExecutionSettings promptExecutionSettings) { if (functionResult is string stringResult) { @@ -1267,12 +1354,14 @@ private void CaptureUsageDetails(CompletionsUsage usage) return chatMessageContent.ToString(); } +#pragma warning disable CS0618 // Type or member is obsolete + var serializerOptions = promptExecutionSettings?.ToolCallBehavior?.ToolCallResultSerializerOptions; +#pragma warning restore CS0618 // Type or member is obsolete + // For polymorphic serialization of unknown in advance child classes of the KernelContent class, // a corresponding JsonTypeInfoResolver should be provided via the JsonSerializerOptions.TypeInfoResolver property. // For more details about the polymorphic serialization, see the article at: // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0 -#pragma warning disable CS0618 // Type or member is obsolete - return JsonSerializer.Serialize(functionResult, toolCallBehavior?.ToolCallResultSerializerOptions); -#pragma warning restore CS0618 // Type or member is obsolete + return JsonSerializer.Serialize(functionResult, serializerOptions); } } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs index b731db727149..de353a5dfb30 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs @@ -18,6 +18,8 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings { + private readonly static JsonSerializerOptions s_serializerOptions = new(JsonOptionsCache.ReadPermissive) { TypeInfoResolver = ToolBehaviorsResolver.Instance }; + /// /// Temperature controls the randomness of the completion. /// The higher the temperature, the more random the completion. @@ -325,9 +327,9 @@ public static OpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutio return settings; } - var json = JsonSerializer.Serialize(executionSettings); + var json = JsonSerializer.Serialize(executionSettings, s_serializerOptions); - var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); + var openAIExecutionSettings = JsonSerializer.Deserialize(json, s_serializerOptions); if (openAIExecutionSettings is not null) { return openAIExecutionSettings; diff --git a/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs b/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs index 9753051aea4e..56d7b6de6592 100644 --- a/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs +++ b/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs @@ -13,6 +13,8 @@ namespace Microsoft.SemanticKernel; /// public static class KernelFunctionMarkdown { + private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() { TypeInfoResolver = ToolBehaviorsResolver.Instance }; + /// /// Creates a instance for a prompt function using the specified markdown text. /// @@ -56,7 +58,7 @@ internal static PromptTemplateConfig CreateFromPromptMarkdown(string text, strin case "sk.execution_settings": var modelSettings = codeBlock.Lines.ToString(); - var settingsDictionary = JsonSerializer.Deserialize>(modelSettings); + var settingsDictionary = JsonSerializer.Deserialize>(modelSettings, s_jsonSerializerOptions); if (settingsDictionary is not null) { foreach (var keyValue in settingsDictionary) diff --git a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs index a277284f3ccc..5bb940e72fc4 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI.ToolBehaviors; using Xunit; namespace SemanticKernel.Functions.UnitTests.Markdown.Functions; @@ -18,9 +20,60 @@ public void ItShouldCreatePromptFunctionConfigFromMarkdown() Assert.NotNull(model); Assert.Equal("TellMeAbout", model.Name); Assert.Equal("Hello AI, tell me about {{$input}}", model.Template); - Assert.Equal(2, model.ExecutionSettings.Count); + Assert.Equal(3, model.ExecutionSettings.Count); Assert.Equal("gpt4", model.ExecutionSettings["service1"].ModelId); Assert.Equal("gpt3.5", model.ExecutionSettings["service2"].ModelId); + Assert.Equal("gpt3.5-turbo", model.ExecutionSettings["service3"].ModelId); + } + + [Fact] + public void ItShouldInitializeFunctionCallChoicesFromMarkdown() + { + // Arrange + var kernel = new Kernel(); + + // Act + var function = KernelFunctionMarkdown.CreateFromPromptMarkdown(Markdown, "TellMeAbout"); + + // Assert + Assert.NotNull(function); + Assert.NotEmpty(function.ExecutionSettings); + + Assert.Equal(3, function.ExecutionSettings.Count); + + // AutoFunctionCallChoice for service1 + var service1ExecutionSettings = function.ExecutionSettings["service1"]; + Assert.NotNull(service1ExecutionSettings?.ToolBehaviors); + Assert.Single(service1ExecutionSettings.ToolBehaviors); + + var service1FunctionCallBehavior = service1ExecutionSettings.ToolBehaviors.Single() as FunctionCallBehavior; + Assert.NotNull(service1FunctionCallBehavior?.Choice); + + var service1AutoFunctionCallChoice = service1FunctionCallBehavior?.Choice as AutoFunctionCallChoice; + Assert.NotNull(service1AutoFunctionCallChoice); + Assert.True(service1AutoFunctionCallChoice.AllowAnyRequestedKernelFunction); + Assert.NotNull(service1AutoFunctionCallChoice.Functions); + Assert.Single(service1AutoFunctionCallChoice.Functions); + Assert.Equal("p1.f1", service1AutoFunctionCallChoice.Functions.First()); + + // RequiredFunctionCallChoice for service2 + var service2ExecutionSettings = function.ExecutionSettings["service2"]; + Assert.NotNull(service2ExecutionSettings?.ToolBehaviors); + Assert.Single(service2ExecutionSettings.ToolBehaviors); + + var service2FunctionCallBehavior = service2ExecutionSettings.ToolBehaviors.Single() as FunctionCallBehavior; + Assert.NotNull(service2FunctionCallBehavior?.Choice); + + var service2RequiredFunctionCallChoice = service2FunctionCallBehavior?.Choice as RequiredFunctionCallChoice; + Assert.NotNull(service2RequiredFunctionCallChoice); + Assert.NotNull(service2RequiredFunctionCallChoice.Functions); + Assert.Single(service2RequiredFunctionCallChoice.Functions); + Assert.Equal("p1.f1", service2RequiredFunctionCallChoice.Functions.First()); + + // NoneFunctionCallChoice for service3 + var service3ExecutionSettings = function.ExecutionSettings["service3"]; + Assert.NotNull(service3ExecutionSettings?.ToolBehaviors); + Assert.Single(service3ExecutionSettings.ToolBehaviors); } [Fact] @@ -47,7 +100,17 @@ These are AI execution settings { "service1" : { "model_id": "gpt4", - "temperature": 0.7 + "temperature": 0.7, + "tool_behaviors": [ + { + "type": "function_call_behavior", + "choice": { + "type": "auto", + "allowAnyRequestedKernelFunction" : true, + "functions": ["p1.f1"] + } + } + ] } } ``` @@ -56,7 +119,33 @@ These are more AI execution settings { "service2" : { "model_id": "gpt3.5", - "temperature": 0.8 + "temperature": 0.8, + "tool_behaviors": [ + { + "type": "function_call_behavior", + "choice": { + "type": "required", + "functions": ["p1.f1"] + } + } + ] + } + } + ``` + These are AI execution settings as well + ```sk.execution_settings + { + "service3" : { + "model_id": "gpt3.5-turbo", + "temperature": 0.8, + "tool_behaviors": [ + { + "type": "function_call_behavior", + "choice": { + "type": "none" + } + } + ] } } ``` diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs index 30bce2a3fac2..f353f2fcecc7 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs @@ -1,8 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Linq; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI.ToolBehaviors; using Microsoft.SemanticKernel.Connectors.OpenAI; + using Xunit; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -68,7 +71,7 @@ public void ItShouldSupportCreatingOpenAIExecutionSettings() // Arrange var deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithNodeDeserializer(new PromptExecutionSettingsNodeDeserializer()) + .WithTypeConverter(new PromptExecutionSettingsTypeConverter()) .Build(); var promptFunctionModel = deserializer.Deserialize(this._yaml); @@ -82,6 +85,41 @@ public void ItShouldSupportCreatingOpenAIExecutionSettings() Assert.Equal(0.0, executionSettings.TopP); } + [Fact] + public void ItShouldDeserializeFunctionCallChoices() + { + // Act + var promptTemplateConfig = KernelFunctionYaml.ToPromptTemplateConfig(this._yaml); + + // Assert + Assert.NotNull(promptTemplateConfig?.ExecutionSettings); + Assert.Equal(2, promptTemplateConfig.ExecutionSettings.Count); + + // Service with auto function call choice + var service1ExecutionSettings = promptTemplateConfig.ExecutionSettings["service1"]; + Assert.NotNull(service1ExecutionSettings?.ToolBehaviors); + Assert.Single(service1ExecutionSettings.ToolBehaviors); + + var service1FunctionCallBehavior = service1ExecutionSettings.ToolBehaviors.Single() as FunctionCallBehavior; + Assert.NotNull(service1FunctionCallBehavior?.Choice); + + var autoFunctionCallChoice = service1FunctionCallBehavior.Choice as AutoFunctionCallChoice; + Assert.NotNull(autoFunctionCallChoice?.Functions); + Assert.Equal("p1.f1", autoFunctionCallChoice.Functions.Single()); + + // Service with required function call choice + var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; + Assert.NotNull(service2ExecutionSettings?.ToolBehaviors); + Assert.Single(service2ExecutionSettings.ToolBehaviors); + + var service2FunctionCallBehavior = service2ExecutionSettings.ToolBehaviors.Single() as FunctionCallBehavior; + Assert.NotNull(service2FunctionCallBehavior?.Choice); + + var requiredFunctionCallChoice = service2FunctionCallBehavior.Choice as RequiredFunctionCallChoice; + Assert.NotNull(requiredFunctionCallChoice?.Functions); + Assert.Equal("p2.f2", requiredFunctionCallChoice.Functions.Single()); + } + [Fact] public void ItShouldCreateFunctionWithDefaultValueOfStringType() { @@ -157,6 +195,11 @@ string CreateYaml(object defaultValue) frequency_penalty: 0.0 max_tokens: 256 stop_sequences: [] + tool_behaviors: + - !function_call_behavior + choice: !auto + functions: + - p1.f1 service2: model_id: gpt-3.5 temperature: 1.0 @@ -165,6 +208,11 @@ string CreateYaml(object defaultValue) frequency_penalty: 0.0 max_tokens: 256 stop_sequences: [ "foo", "bar", "baz" ] + tool_behaviors: + - !function_call_behavior + choice: !required + functions: + - p2.f2 """; private readonly string _yamlWithCustomSettings = """ diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsNodeDeserializerTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs similarity index 91% rename from dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsNodeDeserializerTests.cs rename to dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs index 140de66fdaa8..1c4c1d9cfe03 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsNodeDeserializerTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs @@ -8,9 +8,9 @@ namespace SemanticKernel.Functions.UnitTests.Yaml; /// -/// Tests for . +/// Tests for . /// -public sealed class PromptExecutionSettingsNodeDeserializerTests +public sealed class PromptExecutionSettingsTypeConverterTests { [Fact] public void ItShouldCreatePromptFunctionFromYamlWithCustomModelSettings() @@ -18,7 +18,7 @@ public void ItShouldCreatePromptFunctionFromYamlWithCustomModelSettings() // Arrange var deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithNodeDeserializer(new PromptExecutionSettingsNodeDeserializer()) + .WithTypeConverter(new PromptExecutionSettingsTypeConverter()) .Build(); // Act diff --git a/dotnet/src/Functions/Functions.Yaml/FunctionCallBehaviorTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/FunctionCallBehaviorTypeConverter.cs new file mode 100644 index 000000000000..4e32454dbe06 --- /dev/null +++ b/dotnet/src/Functions/Functions.Yaml/FunctionCallBehaviorTypeConverter.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.AI.ToolBehaviors; +using YamlDotNet.Core; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Microsoft.SemanticKernel; + +internal sealed class FunctionCallBehaviorTypeConverter : IYamlTypeConverter +{ + private static IDeserializer? s_deserializer; + + public bool Accepts(Type type) + { + return typeof(FunctionCallBehavior) == type; + } + + public object? ReadYaml(IParser parser, Type type) + { + s_deserializer ??= new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTagMapping("!function_call_behavior", typeof(FunctionCallBehavior)) + .WithTagMapping("!auto", typeof(AutoFunctionCallChoice)) + .WithTagMapping("!required", typeof(RequiredFunctionCallChoice)) + .Build(); + + return s_deserializer.Deserialize(parser, type); + } + + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + throw new NotImplementedException(); + } +} diff --git a/dotnet/src/Functions/Functions.Yaml/KernelFunctionYaml.cs b/dotnet/src/Functions/Functions.Yaml/KernelFunctionYaml.cs index ec2a26fc2b61..863d991bb207 100644 --- a/dotnet/src/Functions/Functions.Yaml/KernelFunctionYaml.cs +++ b/dotnet/src/Functions/Functions.Yaml/KernelFunctionYaml.cs @@ -57,7 +57,7 @@ public static PromptTemplateConfig ToPromptTemplateConfig(string text) { var deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithNodeDeserializer(new PromptExecutionSettingsNodeDeserializer()) + .WithTypeConverter(new PromptExecutionSettingsTypeConverter()) .Build(); return deserializer.Deserialize(text); diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsNodeDeserializer.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsNodeDeserializer.cs deleted file mode 100644 index 5bd7b839b068..000000000000 --- a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsNodeDeserializer.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using YamlDotNet.Core; -using YamlDotNet.Serialization; - -namespace Microsoft.SemanticKernel; - -/// -/// Deserializer for . -/// -internal sealed class PromptExecutionSettingsNodeDeserializer : INodeDeserializer -{ - /// - public bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value) - { - if (expectedType != typeof(PromptExecutionSettings)) - { - value = null; - return false; - } - - var dictionary = nestedObjectDeserializer.Invoke(reader, typeof(Dictionary)); - var modelSettings = new PromptExecutionSettings(); - foreach (var kv in (Dictionary)dictionary!) - { - switch (kv.Key) - { - case "model_id": - modelSettings.ModelId = (string)kv.Value; - break; - - default: - (modelSettings.ExtensionData ??= new Dictionary()).Add(kv.Key, kv.Value); - break; - } - } - - value = modelSettings; - return true; - } -} diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs new file mode 100644 index 000000000000..f9be1cee27d9 --- /dev/null +++ b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.SemanticKernel.AI.ToolBehaviors; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Microsoft.SemanticKernel; + +internal sealed class PromptExecutionSettingsTypeConverter : IYamlTypeConverter +{ + private static IDeserializer? s_deserializer; + + public bool Accepts(Type type) + { + return type == typeof(PromptExecutionSettings); + } + + public object? ReadYaml(IParser parser, Type type) + { + s_deserializer ??= new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeConverter(new FunctionCallBehaviorTypeConverter()) + .WithTagMapping("!function_call_behavior", typeof(FunctionCallBehavior)) + .Build(); + + parser.MoveNext(); // Move to the first property + + var executionSettings = new PromptExecutionSettings(); + while (parser.Current is not MappingEnd) + { + var propertyName = parser.Consume().Value; + switch (propertyName) + { + case "model_id": + executionSettings.ModelId = s_deserializer.Deserialize(parser); + break; + case "tool_behaviors": + executionSettings.ToolBehaviors = s_deserializer.Deserialize>(parser); + break; + default: + (executionSettings.ExtensionData ??= new Dictionary()).Add(propertyName, s_deserializer.Deserialize(parser)); + break; + } + } + parser.MoveNext(); // Move past the MappingEnd event + return executionSettings; + } + + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + throw new NotImplementedException(); + } +} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs new file mode 100644 index 000000000000..fe01d53d24a2 --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs @@ -0,0 +1,588 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI.ToolBehaviors; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using SemanticKernel.IntegrationTests.Planners.Stepwise; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; + +public sealed class OpenAIFunctionsTests : BaseIntegrationTest +{ + [Fact] + public async Task CanAutoInvokeKernelFunctionsAsync() + { + // Arrange + Kernel kernel = this.InitializeKernel(); + kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + +#pragma warning disable CS0618 // Events are deprecated + void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e) + { + invokedFunctions.Add(e.Function.Name); + } + + kernel.FunctionInvoking += MyInvokingHandler; +#pragma warning restore CS0618 // Events are deprecated + + // Act + OpenAIPromptExecutionSettings settings = new() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + var result = await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.", new(settings)); + + // Assert + Assert.NotNull(result); + Assert.Contains("GetCurrentUtcTime", invokedFunctions); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionsStreamingAsync() + { + // Arrange + Kernel kernel = this.InitializeKernel(); + kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + +#pragma warning disable CS0618 // Events are deprecated + void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e) + { + invokedFunctions.Add($"{e.Function.Name}({string.Join(", ", e.Arguments)})"); + } + + kernel.FunctionInvoking += MyInvokingHandler; +#pragma warning restore CS0618 // Events are deprecated + + // Act + OpenAIPromptExecutionSettings settings = new() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + string result = ""; + await foreach (string c in kernel.InvokePromptStreamingAsync( + $"How much older is John than Jim? Compute that value and pass it to the {nameof(TimeInformation)}.{nameof(TimeInformation.InterpretValue)} function, then respond only with its result.", + new(settings))) + { + result += c; + } + + // Assert + Assert.Contains("6", result, StringComparison.InvariantCulture); + Assert.Contains("GetAge([personName, John])", invokedFunctions); + Assert.Contains("GetAge([personName, Jim])", invokedFunctions); + Assert.Contains("InterpretValue([value, 3])", invokedFunctions); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionsWithComplexTypeParametersAsync() + { + // Arrange + Kernel kernel = this.InitializeKernel(); + kernel.ImportPluginFromType(); + + // Act + OpenAIPromptExecutionSettings settings = new() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + var result = await kernel.InvokePromptAsync("What is the current temperature in Dublin, Ireland, in Fahrenheit?", new(settings)); + + // Assert + Assert.NotNull(result); + Assert.Contains("42.8", result.GetValue(), StringComparison.InvariantCulture); // The WeatherPlugin always returns 42.8 for Dublin, Ireland. + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionsWithPrimitiveTypeParametersAsync() + { + // Arrange + Kernel kernel = this.InitializeKernel(); + kernel.ImportPluginFromType(); + + // Act + OpenAIPromptExecutionSettings settings = new() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + + + var result = await kernel.InvokePromptAsync("Convert 50 degrees Fahrenheit to Celsius.", new(settings)); + + // Assert + Assert.NotNull(result); + Assert.Contains("10", result.GetValue(), StringComparison.InvariantCulture); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionFromPromptAsync() + { + // Arrange + Kernel kernel = this.InitializeKernel(); + + var promptFunction = KernelFunctionFactory.CreateFromPrompt( + "Your role is always to return this text - 'A Game-Changer for the Transportation Industry'. Don't ask for more details or context.", + functionName: "FindLatestNews", + description: "Searches for the latest news."); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions( + "NewsProvider", + "Delivers up-to-date news content.", + [promptFunction])); + + // Act + OpenAIPromptExecutionSettings settings = new() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + var result = await kernel.InvokePromptAsync("Show me the latest news as they are.", new(settings)); + + // Assert + Assert.NotNull(result); + Assert.Contains("Transportation", result.GetValue(), StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionFromPromptStreamingAsync() + { + // Arrange + Kernel kernel = this.InitializeKernel(); + + var promptFunction = KernelFunctionFactory.CreateFromPrompt( + "Your role is always to return this text - 'A Game-Changer for the Transportation Industry'. Don't ask for more details or context.", + functionName: "FindLatestNews", + description: "Searches for the latest news."); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions( + "NewsProvider", + "Delivers up-to-date news content.", + [promptFunction])); + + // Act + OpenAIPromptExecutionSettings settings = new() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + var streamingResult = kernel.InvokePromptStreamingAsync("Show me the latest news as they are.", new(settings)); + + var builder = new StringBuilder(); + + await foreach (var update in streamingResult) + { + builder.Append(update.ToString()); + } + + var result = builder.ToString(); + + // Assert + Assert.NotNull(result); + Assert.Contains("Transportation", result, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorSpecificChatMessageContentClassesCanBeUsedForManualFunctionCallingAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false)] }; + + var sut = kernel.GetRequiredService(); + + // Act + var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Current way of handling function calls manually using connector specific chat message content class. + var toolCalls = ((OpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); + + while (toolCalls.Count > 0) + { + // Adding LLM function call request to chat history + chatHistory.Add(result); + + // Iterating over the requested function calls and invoking them + foreach (var toolCall in toolCalls) + { + string content = kernel.Plugins.TryGetFunctionAndArguments(toolCall, out KernelFunction? function, out KernelArguments? arguments) ? + JsonSerializer.Serialize((await function.InvokeAsync(kernel, arguments)).GetValue()) : + "Unable to find function. Please try again!"; + + // Adding the result of the function call to the chat history + chatHistory.Add(new ChatMessageContent( + AuthorRole.Tool, + content, + metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } })); + } + + // Sending the functions invocation results back to the LLM to get the final response + result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + toolCalls = ((OpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); + } + + // Assert + Assert.Contains("rain", result.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManualFunctionCallingAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false)] }; + + var sut = kernel.GetRequiredService(); + + // Act + var messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + var functionCalls = FunctionCallRequestContent.GetFunctionCalls(messageContent).ToArray(); + + while (functionCalls.Length != 0) + { + // Adding function call request from LLM to chat history + chatHistory.Add(messageContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + var result = await functionCall.InvokeAsync(kernel); + + chatHistory.Add(result.ToChatMessage()); + } + + // Sending the functions invocation results to the LLM to get the final response + messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + functionCalls = FunctionCallRequestContent.GetFunctionCalls(messageContent).ToArray(); + } + + // Assert + Assert.Contains("rain", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExceptionToConnectorAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("If you are unable to answer the question for whatever reason, please add the 'error' keyword to the response."); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false)] }; + + var completionService = kernel.GetRequiredService(); + + // Act + var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + + var functionCalls = FunctionCallRequestContent.GetFunctionCalls(messageContent).ToArray(); + + while (functionCalls.Length != 0) + { + // Adding function call request from LLM to chat history + chatHistory.Add(messageContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + // Simulating an exception + var exception = new OperationCanceledException("The operation was canceled due to timeout."); + + chatHistory.Add(new FunctionCallResultContent(functionCall, exception).ToChatMessage()); + } + + // Sending the functions execution results back to the LLM to get the final response + messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + functionCalls = FunctionCallRequestContent.GetFunctionCalls(messageContent).ToArray(); + } + + // Assert + Assert.NotNull(messageContent.Content); + + Assert.Contains("error", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFunctionCallsAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("if there's a tornado warning, please add the 'tornado' keyword to the response."); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false)] }; + + var completionService = kernel.GetRequiredService(); + + // Act + var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + + var functionCalls = FunctionCallRequestContent.GetFunctionCalls(messageContent).ToArray(); + + while (functionCalls.Length > 0) + { + // Adding function call request from LLM to chat history + chatHistory.Add(messageContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + var result = await functionCall.InvokeAsync(kernel); + + chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { result }); + } + + // Adding a simulated function call to the connector response message + var simulatedFunctionCall = new FunctionCallRequestContent("weather-alert", id: "call_123"); + messageContent.Items.Add(simulatedFunctionCall); + + // Adding a simulated function result to chat history + var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; + chatHistory.Add(new FunctionCallResultContent(simulatedFunctionCall, simulatedFunctionResult).ToChatMessage()); + + // Sending the functions invocation results back to the LLM to get the final response + messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + functionCalls = FunctionCallRequestContent.GetFunctionCalls(messageContent).ToArray(); + } + + // Assert + Assert.Contains("tornado", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ItFailsIfNoFunctionResultProvidedAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false)] }; + + var completionService = kernel.GetRequiredService(); + + // Act + var result = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + + chatHistory.Add(result); + + var exception = await Assert.ThrowsAsync(() => completionService.GetChatMessageContentAsync(chatHistory, settings, kernel)); + + // Assert + Assert.Contains("'tool_calls' must be followed by tool", exception.Message, StringComparison.InvariantCulture); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFunctionCallingAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + + var sut = kernel.GetRequiredService(); + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Assert + Assert.Equal(5, chatHistory.Count); + + var userMessage = chatHistory[0]; + Assert.Equal(AuthorRole.User, userMessage.Role); + + // LLM requested the current time. + var getCurrentTimeFunctionCallRequestMessage = chatHistory[1]; + Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallRequestMessage.Role); + + var getCurrentTimeFunctionCallRequest = getCurrentTimeFunctionCallRequestMessage.Items.OfType().Single(); + Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallRequest.PluginName); + Assert.NotNull(getCurrentTimeFunctionCallRequest.Id); + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + var getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionCallResultMessage.Role); + Assert.Single(getCurrentTimeFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. + + var getCurrentTimeFunctionCallResult = getCurrentTimeFunctionCallResultMessage.Items.OfType().Single(); + Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallResult.FunctionName); + Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallResult.PluginName); + Assert.Equal(getCurrentTimeFunctionCallRequest.Id, getCurrentTimeFunctionCallResult.Id); + Assert.NotNull(getCurrentTimeFunctionCallResult.Result); + + // LLM requested the weather for Boston. + var getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; + Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallRequestMessage.Role); + + var getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); + Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + var getWeatherForCityFunctionCallResultMessage = chatHistory[4]; + Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionCallResultMessage.Role); + Assert.Single(getWeatherForCityFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. + + var getWeatherForCityFunctionCallResult = getWeatherForCityFunctionCallResultMessage.Items.OfType().Single(); + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallResult.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallResult.PluginName); + Assert.Equal(getWeatherForCityFunctionCallRequest.Id, getWeatherForCityFunctionCallResult.Id); + Assert.NotNull(getWeatherForCityFunctionCallResult.Result); + } + + [Fact] + public async Task SubsetOfFunctionsCanBeUsedForFunctionCallingAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: false); + + var function = kernel.CreateFunctionFromMethod(() => DayOfWeek.Friday, "GetDayOfWeek", "Retrieves the current day of the week."); + kernel.ImportPluginFromFunctions("HelperFunctions", [function]); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("What day is today?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: true)] }; + + var sut = kernel.GetRequiredService(); + + // Act + var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Assert + Assert.NotNull(result); + Assert.Contains("Friday", result.Content, StringComparison.InvariantCulture); + } + + [Fact] + public async Task RequiredFunctionShouldBeCalledByAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: false); + + var function = kernel.CreateFunctionFromMethod(() => DayOfWeek.Friday, "GetDayOfWeek", "Retrieves the current day of the week."); + kernel.ImportPluginFromFunctions("HelperFunctions", [function]); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("What day is today?"); + + var settings = new PromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.RequiredFunctionChoice([function], true)] }; + + var sut = kernel.GetRequiredService(); + + // Act + var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Assert + Assert.NotNull(result); + Assert.Contains("Friday", result.Content, StringComparison.InvariantCulture); + } + + private Kernel InitializeKernel(bool importHelperPlugin = false) + { + OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("Planners:OpenAI").Get(); + Assert.NotNull(openAIConfiguration); + + IKernelBuilder builder = this.CreateKernelBuilder() + .AddOpenAIChatCompletion( + modelId: openAIConfiguration.ModelId, + apiKey: openAIConfiguration.ApiKey); + + var kernel = builder.Build(); + + if (importHelperPlugin) + { + kernel.ImportPluginFromFunctions("HelperFunctions", new[] + { + kernel.CreateFunctionFromMethod(() => DateTime.UtcNow.ToString("R"), "GetCurrentUtcTime", "Retrieves the current time in UTC."), + kernel.CreateFunctionFromMethod((string cityName) => + cityName switch + { + "Boston" => "61 and rainy", + _ => "31 and snowing", + }, "Get_Weather_For_City", "Gets the current weather for the specified city"), + }); + } + + return kernel; + } + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + /// + /// A plugin that returns the current time. + /// + public class TimeInformation + { + [KernelFunction] + [Description("Retrieves the current time in UTC.")] + public string GetCurrentUtcTime() => DateTime.UtcNow.ToString("R"); + + [KernelFunction] + [Description("Gets the age of the specified person.")] + public int GetAge(string personName) + { + if ("John".Equals(personName, StringComparison.OrdinalIgnoreCase)) + { + return 33; + } + + if ("Jim".Equals(personName, StringComparison.OrdinalIgnoreCase)) + { + return 30; + } + + return -1; + } + + [KernelFunction] + public int InterpretValue(int value) => value * 2; + } + + public class WeatherPlugin + { + [KernelFunction, Description("Get current temperature.")] + public Task GetCurrentTemperatureAsync(WeatherParameters parameters) + { + if (parameters.City.Name == "Dublin" && (parameters.City.Country == "Ireland" || parameters.City.Country == "IE")) + { + return Task.FromResult(42.8); // 42.8 Fahrenheit. + } + + throw new NotSupportedException($"Weather in {parameters.City.Name} ({parameters.City.Country}) is not supported."); + } + + [KernelFunction, Description("Convert temperature from Fahrenheit to Celsius.")] + public Task ConvertTemperatureAsync(double temperatureInFahrenheit) + { + double temperatureInCelsius = (temperatureInFahrenheit - 32) * 5 / 9; + return Task.FromResult(temperatureInCelsius); + } + } + + public record WeatherParameters(City City); + + public class City + { + public string Name { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + } +} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index f27ad7789091..82b743ab0cff 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -439,6 +439,54 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFu Assert.NotNull(getWeatherForCityFunctionCallResult.Result); } + [Fact] + public async Task SubsetOfFunctionsCanBeUsedForFunctionCallingAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: false); + + var function = kernel.CreateFunctionFromMethod(() => DayOfWeek.Friday, "GetDayOfWeek", "Retrieves the current day of the week."); + kernel.ImportPluginFromFunctions("HelperFunctions", [function]); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("What day is today?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableFunctions([function.Metadata.ToOpenAIFunction()], true) }; + + var sut = kernel.GetRequiredService(); + + // Act + var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Assert + Assert.NotNull(result); + Assert.Contains("Friday", result.Content, StringComparison.InvariantCulture); + } + + [Fact] + public async Task RequiredFunctionShouldBeCalledAsync() + { + // Arrange + var kernel = this.InitializeKernel(importHelperPlugin: false); + + var function = kernel.CreateFunctionFromMethod(() => DayOfWeek.Friday, "GetDayOfWeek", "Retrieves the current day of the week."); + kernel.ImportPluginFromFunctions("HelperFunctions", [function]); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("What day is today?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.RequireFunction(function.Metadata.ToOpenAIFunction(), true) }; + + var sut = kernel.GetRequiredService(); + + // Act + var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Assert + Assert.NotNull(result); + Assert.Contains("Friday", result.Content, StringComparison.InvariantCulture); + } + private Kernel InitializeKernel(bool importHelperPlugin = false) { OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("Planners:OpenAI").Get(); diff --git a/dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorsResolver.cs b/dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorsResolver.cs new file mode 100644 index 000000000000..3fa8a0225b34 --- /dev/null +++ b/dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorsResolver.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using Microsoft.SemanticKernel.AI.ToolBehaviors; + +namespace Microsoft.SemanticKernel; + +[ExcludeFromCodeCoverage] +internal sealed class ToolBehaviorsResolver : DefaultJsonTypeInfoResolver +{ + public static ToolBehaviorsResolver Instance { get; } = new ToolBehaviorsResolver(); + + public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + var jsonTypeInfo = base.GetTypeInfo(type, options); + + if (jsonTypeInfo.Type == typeof(ToolBehavior)) + { + jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions(); + + jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(FunctionCallBehavior), "function_call_behavior")); + + jsonTypeInfo.PolymorphismOptions.TypeDiscriminatorPropertyName = "type"; + + return jsonTypeInfo; + } + + if (jsonTypeInfo.Type == typeof(FunctionCallChoice)) + { + jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions(); + + jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(RequiredFunctionCallChoice), "required")); + jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(AutoFunctionCallChoice), "auto")); + jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(NoneFunctionCallChoice), "none")); + + jsonTypeInfo.PolymorphismOptions.TypeDiscriminatorPropertyName = "type"; + + return jsonTypeInfo; + } + + return jsonTypeInfo; + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs index 14b0d553aa58..886c5952ece4 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.AI.ToolBehaviors; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.TextGeneration; @@ -43,6 +44,18 @@ public string? ModelId } } + [JsonPropertyName("tool_behaviors")] + public IEnumerable? ToolBehaviors + { + get => this._toolBehaviors; + + set + { + this.ThrowIfFrozen(); + this._toolBehaviors = value; + } + } + /// /// Extra properties that may be included in the serialized execution settings. /// @@ -92,7 +105,8 @@ public virtual PromptExecutionSettings Clone() return new() { ModelId = this.ModelId, - ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null + ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, + ToolBehaviors = this.ToolBehaviors }; } @@ -112,6 +126,7 @@ protected void ThrowIfFrozen() private string? _modelId; private IDictionary? _extensionData; + private IEnumerable? _toolBehaviors; #endregion } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/AutoFunctionCallChoice.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/AutoFunctionCallChoice.cs new file mode 100644 index 000000000000..7c0bec197a8d --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/AutoFunctionCallChoice.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.AI.ToolBehaviors; + +public sealed class AutoFunctionCallChoice : FunctionCallChoice +{ + internal const int DefaultMaximumAutoInvokeAttempts = 5; + + [JsonConstructor] + public AutoFunctionCallChoice() + { + } + + public AutoFunctionCallChoice(IEnumerable functions) + { + this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)); + + this.AllowAnyRequestedKernelFunction = !functions.Any(); + } + + [JsonPropertyName("maximumAutoInvokeAttempts")] + public int MaximumAutoInvokeAttempts { get; init; } = DefaultMaximumAutoInvokeAttempts; + + [JsonPropertyName("functions")] + public IEnumerable? Functions { get; init; } + + [JsonPropertyName("allowAnyRequestedKernelFunction")] + public bool AllowAnyRequestedKernelFunction { get; init; } + + public override FunctionCallChoiceConfiguration Configure(FunctionCallChoiceContext context) + { + bool autoInvoke = this.MaximumAutoInvokeAttempts > 0; + + // If auto-invocation is specified, we need a kernel to be able to invoke the functions. + // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions + // and then fail to do so, so we fail before we get to that point. This is an error + // on the consumers behalf: if they specify auto-invocation with any functions, they must + // specify the kernel and the kernel must contain those functions. + if (autoInvoke && context.Kernel is null) + { + throw new KernelException("Auto-invocation in Auto mode is not supported when no kernel is provided."); + } + + IList? availableFunctions = null; + + if (context.Kernel is not null) + { + if (this.Functions is { } functionFQNs && functionFQNs.Any()) + { + foreach (var functionFQN in functionFQNs) + { + availableFunctions ??= new List(); + + // Make sure that every enabled function can be found in the kernel. + Debug.Assert(context.Kernel is not null); + + var name = FunctionName.Parse(functionFQN, FunctionNameSeparator); + + if (!context.Kernel!.Plugins.TryGetFunction(name.PluginName, name.Name, out var function)) + { + throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); + } + + availableFunctions.Add(function.Metadata); + } + } + else + { + // Provide all functions from the kernel. + var kernelFunctions = context.Kernel.Plugins.GetFunctionsMetadata(); + availableFunctions = kernelFunctions.Any() ? kernelFunctions : null; + } + } + + return new FunctionCallChoiceConfiguration() + { + AvailableFunctions = availableFunctions, + MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts, + AllowAnyRequestedKernelFunction = this.AllowAnyRequestedKernelFunction + }; + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallBehavior.cs new file mode 100644 index 000000000000..23b55ba8f51c --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallBehavior.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.AI.ToolBehaviors; + +public abstract class FunctionCallChoice +{ + protected const string FunctionNameSeparator = "."; + + public abstract FunctionCallChoiceConfiguration Configure(FunctionCallChoiceContext context); +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceConfiguration.cs new file mode 100644 index 000000000000..d29f53235214 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceConfiguration.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel.AI.ToolBehaviors; + +public class FunctionCallChoiceConfiguration +{ + public IEnumerable? AvailableFunctions { get; init; } + + public IEnumerable? RequiredFunctions { get; init; } + + public bool? AllowAnyRequestedKernelFunction { get; init; } + + public int? MaximumAutoInvokeAttempts { get; init; } + + public int? MaximumUseAttempts { get; init; } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceContext.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceContext.cs new file mode 100644 index 000000000000..192c9c8f2e49 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceContext.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.AI.ToolBehaviors; + +public class FunctionCallChoiceContext +{ + public Kernel? Kernel { get; init; } + + public object? Model { get; init; } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/NoneFunctionCallChoice.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/NoneFunctionCallChoice.cs new file mode 100644 index 000000000000..2c9c6f91dde1 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/NoneFunctionCallChoice.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.AI.ToolBehaviors; + +public sealed class NoneFunctionCallChoice : FunctionCallChoice +{ + public override FunctionCallChoiceConfiguration Configure(FunctionCallChoiceContext context) + { + return new FunctionCallChoiceConfiguration() + { + MaximumAutoInvokeAttempts = 0, + MaximumUseAttempts = 0, + AllowAnyRequestedKernelFunction = false + }; + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/RequiredFunctionCallChoice.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/RequiredFunctionCallChoice.cs new file mode 100644 index 000000000000..17c1c1e1c325 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/RequiredFunctionCallChoice.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.AI.ToolBehaviors; + +public sealed class RequiredFunctionCallChoice : FunctionCallChoice +{ + internal const int DefaultMaximumAutoInvokeAttempts = 5; + + internal const int DefaultMaximumUseAttempts = 1; + + [JsonConstructor] + public RequiredFunctionCallChoice() + { + } + + public RequiredFunctionCallChoice(IEnumerable functions) + { + this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)); + } + + [JsonPropertyName("functions")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IEnumerable? Functions { get; init; } + + [JsonPropertyName("maximumAutoInvokeAttempts")] + public int MaximumAutoInvokeAttempts { get; init; } = DefaultMaximumAutoInvokeAttempts; + + [JsonPropertyName("maximumUseAttempts")] + public int MaximumUseAttempts { get; init; } = DefaultMaximumUseAttempts; + + public override FunctionCallChoiceConfiguration Configure(FunctionCallChoiceContext context) + { + List? requiredFunctions = null; + + if (this.Functions is { } functionFQNs && functionFQNs.Any()) + { + bool autoInvoke = this.MaximumAutoInvokeAttempts > 0; + + // If auto-invocation is specified, we need a kernel to be able to invoke the functions. + // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions + // and then fail to do so, so we fail before we get to that point. This is an error + // on the consumers behalf: if they specify auto-invocation with any functions, they must + // specify the kernel and the kernel must contain those functions. + if (autoInvoke && context.Kernel is null) + { + throw new KernelException("Auto-invocation in Any mode is not supported when no kernel is provided."); + } + + foreach (var functionFQN in functionFQNs) + { + requiredFunctions ??= []; + + // Make sure that every enabled function can be found in the kernel. + Debug.Assert(context.Kernel is not null); + + var name = FunctionName.Parse(functionFQN, FunctionNameSeparator); + + // Make sure that the required functions can be found in the kernel. + if (!context.Kernel!.Plugins.TryGetFunction(name.PluginName, name.Name, out var function)) + { + throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); + } + + requiredFunctions.Add(function.Metadata); + } + } + + return new FunctionCallChoiceConfiguration() + { + RequiredFunctions = requiredFunctions, + MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts, + MaximumUseAttempts = this.MaximumUseAttempts, + AllowAnyRequestedKernelFunction = false + }; + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/FunctionCallBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/FunctionCallBehavior.cs new file mode 100644 index 000000000000..d83e43f69c9a --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/FunctionCallBehavior.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.AI.ToolBehaviors; + +public class FunctionCallBehavior : ToolBehavior +{ + public static FunctionCallBehavior AutoFunctionChoice(IEnumerable? enabledFunctions = null, bool autoInvoke = true) + { + return new FunctionCallBehavior + { + Choice = new AutoFunctionCallChoice(enabledFunctions ?? []) + { + MaximumAutoInvokeAttempts = autoInvoke ? AutoFunctionCallChoice.DefaultMaximumAutoInvokeAttempts : 0 + } + }; + } + + public static FunctionCallBehavior RequiredFunctionChoice(IEnumerable functions, bool autoInvoke = true) + { + return new FunctionCallBehavior + { + Choice = new RequiredFunctionCallChoice(functions) + { + MaximumAutoInvokeAttempts = autoInvoke ? AutoFunctionCallChoice.DefaultMaximumAutoInvokeAttempts : 0 + } + }; + } + + public static FunctionCallBehavior None { get; } = new FunctionCallBehavior() { Choice = new NoneFunctionCallChoice() }; + + [JsonPropertyName("choice")] + public FunctionCallChoice Choice { get; init; } = new NoneFunctionCallChoice(); +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/ToolBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/ToolBehavior.cs new file mode 100644 index 000000000000..fe403daf6db6 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/ToolBehavior.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.AI.ToolBehaviors; + +public abstract class ToolBehavior +{ +} diff --git a/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs b/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs index 11d0aab28f7d..3372015ec1ff 100644 --- a/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs +++ b/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs @@ -25,6 +25,8 @@ namespace Microsoft.SemanticKernel; /// public sealed class PromptTemplateConfig { + private readonly static JsonSerializerOptions s_serializerOptions = new(JsonOptionsCache.ReadPermissive) { TypeInfoResolver = ToolBehaviorsResolver.Instance }; + /// The format of the prompt template. private string? _templateFormat; /// The prompt template string. @@ -66,7 +68,7 @@ public static PromptTemplateConfig FromJson(string json) PromptTemplateConfig? config = null; try { - config = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); + config = JsonSerializer.Deserialize(json, s_serializerOptions); if (config is null) { throw new ArgumentException($"Unable to deserialize {nameof(PromptTemplateConfig)} from the specified JSON.", nameof(json)); diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallBehaviorTests.cs new file mode 100644 index 000000000000..aa2047f6a5cd --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallBehaviorTests.cs @@ -0,0 +1,306 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI.ToolBehaviors; +using Xunit; + +namespace SemanticKernel.UnitTests.Functions; + +/// +/// Unit tests for +/// +public sealed class FunctionCallBehaviorTests +{ + private readonly static JsonSerializerOptions s_serializerOptions = new() { TypeInfoResolver = new ToolBehaviorsResolver() }; + + [Fact] + public void EnableKernelFunctionsAreNotAutoInvoked() + { + // Arrange + var kernel = new Kernel(); + var behavior = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false); + + // Act + var config = behavior.Choice.Configure(new() { Kernel = kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(0, config.MaximumAutoInvokeAttempts); + } + + [Fact] + public void AutoInvokeKernelFunctionsShouldSpecifyNumberOfAutoInvokeAttempts() + { + // Arrange + var kernel = new Kernel(); + var behavior = FunctionCallBehavior.AutoFunctionChoice(); + + // Act + var config = behavior.Choice.Configure(new() { Kernel = kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(5, config.MaximumAutoInvokeAttempts); + } + + [Fact] + public void KernelFunctionsConfigureWithNullKernelDoesNotAddTools() + { + // Arrange + var kernelFunctions = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false); + + // Act + var config = kernelFunctions.Choice.Configure(new() { }); + + // Assert + Assert.Null(config.AvailableFunctions); + Assert.Null(config.RequiredFunctions); + } + + [Fact] + public void KernelFunctionsConfigureWithoutFunctionsDoesNotAddTools() + { + // Arrange + var kernelFunctions = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false); + + var kernel = Kernel.CreateBuilder().Build(); + + // Act + var config = kernelFunctions.Choice.Configure(new() { Kernel = kernel }); + + // Assert + Assert.Null(config.AvailableFunctions); + Assert.Null(config.RequiredFunctions); + } + + [Fact] + public void KernelFunctionsConfigureWithFunctionsAddsTools() + { + // Arrange + var kernelFunctions = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false); + var kernel = Kernel.CreateBuilder().Build(); + + var plugin = this.GetTestPlugin(); + + kernel.Plugins.Add(plugin); + + // Act + var config = kernelFunctions.Choice.Configure(new() { Kernel = kernel }); + + // Assert + Assert.Null(config.RequiredFunctions); + + this.AssertFunctions(config.AvailableFunctions); + } + + [Fact] + public void EnabledFunctionsConfigureWithoutFunctionsDoesNotAddTools() + { + // Arrange + var enabledFunctions = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); + + // Act + var config = enabledFunctions.Choice.Configure(new() { }); + + // Assert + Assert.Null(chatCompletionsOptions.ToolChoice); + Assert.Empty(chatCompletionsOptions.Tools); + } + + [Fact] + public void EnabledFunctionsConfigureWithAutoInvokeAndNullKernelThrowsException() + { + // Arrange + var kernel = new Kernel(); + + var function = this.GetTestPlugin().Single(); + var enabledFunctions = FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: true); + + // Act & Assert + var exception = Assert.Throws(() => enabledFunctions.Choice.Configure(new() { })); ; + Assert.Equal("Auto-invocation in Auto mode is not supported when no kernel is provided.", exception.Message); + } + + [Fact] + public void EnabledFunctionsConfigureWithAutoInvokeAndEmptyKernelThrowsException() + { + // Arrange + var function = this.GetTestPlugin().Single(); + var enabledFunctions = FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: true); + var kernel = Kernel.CreateBuilder().Build(); + + // Act & Assert + var exception = Assert.Throws(() => enabledFunctions.Choice.Configure(new() { Kernel = kernel })); + Assert.Equal("The specified function MyPlugin.MyFunction is not available in the kernel.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EnabledFunctionsConfigureWithKernelAndPluginsAddsTools(bool autoInvoke) + { + // Arrange + var plugin = this.GetTestPlugin(); + var function = plugin.Single(); + var enabledFunctions = FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: autoInvoke); + var kernel = Kernel.CreateBuilder().Build(); + + kernel.Plugins.Add(plugin); + + // Act + var config = enabledFunctions.Choice.Configure(new() { Kernel = kernel }); + + // Assert + this.AssertFunctions(config.AvailableFunctions); + } + + [Fact] + public void RequiredFunctionsConfigureWithAutoInvokeAndNullKernelThrowsException() + { + // Arrange + var kernel = new Kernel(); + + var function = this.GetTestPlugin().Single(); + var requiredFunction = FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: true); + + // Act & Assert + var exception = Assert.Throws(() => requiredFunction.Choice.Configure(new() { })); + Assert.Equal("Auto-invocation in Auto mode is not supported when no kernel is provided.", exception.Message); + } + + [Fact] + public void RequiredFunctionsConfigureWithAutoInvokeAndEmptyKernelThrowsException() + { + // Arrange + var function = this.GetTestPlugin().Single(); + var requiredFunction = FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: true); + var kernel = Kernel.CreateBuilder().Build(); + + // Act & Assert + var exception = Assert.Throws(() => requiredFunction.Choice.Configure(new() { Kernel = kernel })); + Assert.Equal("The specified function MyPlugin.MyFunction is not available in the kernel.", exception.Message); + } + + [Fact] + public void RequiredFunctionConfigureAddsTools() + { + // Arrange + var plugin = this.GetTestPlugin(); + var function = plugin.Single(); + var requiredFunction = FunctionCallBehavior.RequiredFunctionChoice([function], autoInvoke: true); + var kernel = new Kernel(); + kernel.Plugins.Add(plugin); + + // Act + var config = requiredFunction.Choice.Configure(new() { Kernel = kernel }); + + // Assert + this.AssertFunctions(config.RequiredFunctions); + } + + [Fact] + public void ItShouldBePossibleToDeserializeAutoFunctionCallChoice() + { + // Arrange + var json = + """ + { + "type":"auto", + "allowAnyRequestedKernelFunction":true, + "maximumAutoInvokeAttempts":12, + "functions":[ + "MyPlugin.MyFunction" + ] + } + """; + + // Act + var deserializedFunction = JsonSerializer.Deserialize(json, s_serializerOptions) as AutoFunctionCallChoice; + + // Assert + Assert.NotNull(deserializedFunction); + Assert.True(deserializedFunction.AllowAnyRequestedKernelFunction); + Assert.Equal(12, deserializedFunction.MaximumAutoInvokeAttempts); + Assert.NotNull(deserializedFunction.Functions); + Assert.Single(deserializedFunction.Functions); + Assert.Equal("MyPlugin.MyFunction", deserializedFunction.Functions.ElementAt(0)); + } + + [Fact] + public void ItShouldBePossibleToDeserializeForcedFunctionCallChoice() + { + // Arrange + var json = + """ + { + "type": "required", + "maximumAutoInvokeAttempts": 12, + "maximumUseAttempts": 10, + "functions":[ + "MyPlugin.MyFunction" + ] + } + """; + + // Act + var deserializedFunction = JsonSerializer.Deserialize(json, s_serializerOptions) as RequiredFunctionCallChoice; + + // Assert + Assert.NotNull(deserializedFunction); + Assert.Equal(10, deserializedFunction.MaximumUseAttempts); + Assert.Equal(12, deserializedFunction.MaximumAutoInvokeAttempts); + Assert.NotNull(deserializedFunction.Functions); + Assert.Single(deserializedFunction.Functions); + Assert.Equal("MyPlugin.MyFunction", deserializedFunction.Functions.ElementAt(0)); + } + + [Fact] + public void ItShouldBePossibleToDeserializeNoneFunctionCallBehavior() + { + // Arrange + var json = + """ + { + "type": "none" + } + """; + + // Act + var deserializedFunction = JsonSerializer.Deserialize(json, s_serializerOptions) as NoneFunctionCallChoice; + + // Assert + Assert.NotNull(deserializedFunction); + } + + private KernelPlugin GetTestPlugin() + { + var function = KernelFunctionFactory.CreateFromMethod( + (string parameter1, string parameter2) => "Result1", + "MyFunction", + "Test Function", + [new KernelParameterMetadata("parameter1"), new KernelParameterMetadata("parameter2")], + new KernelReturnParameterMetadata { ParameterType = typeof(string), Description = "Function Result" }); + + return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + } + + private void AssertFunctions(IEnumerable? kernelFunctionsMetadata) + { + Assert.NotNull(kernelFunctionsMetadata); + Assert.Single(kernelFunctionsMetadata); + + var functionMetadata = kernelFunctionsMetadata.ElementAt(0); + + Assert.NotNull(functionMetadata); + + Assert.Equal("MyPlugin", functionMetadata.PluginName); + Assert.Equal("MyFunction", functionMetadata.Name); + Assert.Equal("Test Function", functionMetadata.Description); + Assert.Equal(2, functionMetadata.Parameters.Count); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs index 3285ed6b819f..eb55444666c8 100644 --- a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Linq; using System.Text.Json; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI.ToolBehaviors; using Microsoft.SemanticKernel.Connectors.OpenAI; using Xunit; @@ -141,6 +143,143 @@ public void DeserializingExpectCompletion() Assert.Equal("gpt-4", promptTemplateConfig.DefaultExecutionSettings?.ModelId); } + [Fact] + public void DeserializingAutoFunctionCallingChoice() + { + // Arrange + string configPayload = """ + { + "schema": 1, + "execution_settings": { + "default": { + "model_id": "gpt-4", + "tool_behaviors": [ + { + "type": "function_call_behavior", + "choice":{ + "type": "auto", + "allowAnyRequestedKernelFunction" : true, + "maximumAutoInvokeAttempts": 12, + "functions":[ + "p1.f1" + ] + } + } + ] + } + } + } + """; + + // Act + var promptTemplateConfig = PromptTemplateConfig.FromJson(configPayload); + + // Assert + Assert.NotNull(promptTemplateConfig); + Assert.Single(promptTemplateConfig.ExecutionSettings); + + var executionSettings = promptTemplateConfig.ExecutionSettings.Single(); + Assert.NotNull(executionSettings.Value.ToolBehaviors); + Assert.Single(executionSettings.Value.ToolBehaviors); + + var functionCallBehavior = executionSettings.Value.ToolBehaviors.Single() as FunctionCallBehavior; + Assert.NotNull(functionCallBehavior); + + var autoFunctionCallChoice = functionCallBehavior.Choice as AutoFunctionCallChoice; + Assert.NotNull(autoFunctionCallChoice?.Functions); + Assert.Equal("p1.f1", autoFunctionCallChoice.Functions.Single()); + + Assert.True(autoFunctionCallChoice.AllowAnyRequestedKernelFunction); + } + + [Fact] + public void DeserializingRequiredFunctionCallingChoice() + { + // Arrange + string configPayload = """ + { + "schema": 1, + "execution_settings": { + "default": { + "model_id": "gpt-4", + "tool_behaviors": [ + { + "type": "function_call_behavior", + "choice":{ + "type": "required", + "maximumAutoInvokeAttempts": 11, + "functions":[ + "p1.f1" + ] + } + } + ] + } + } + } + """; + + // Act + var promptTemplateConfig = PromptTemplateConfig.FromJson(configPayload); + + // Assert + Assert.NotNull(promptTemplateConfig); + Assert.Single(promptTemplateConfig.ExecutionSettings); + + var executionSettings = promptTemplateConfig.ExecutionSettings.Single(); + Assert.NotNull(executionSettings.Value.ToolBehaviors); + Assert.Single(executionSettings.Value.ToolBehaviors); + + var functionCallBehavior = executionSettings.Value.ToolBehaviors.Single() as FunctionCallBehavior; + Assert.NotNull(functionCallBehavior); + + var requiredFunctionCallChoice = functionCallBehavior.Choice as RequiredFunctionCallChoice; + Assert.NotNull(requiredFunctionCallChoice?.Functions); + Assert.Equal("p1.f1", requiredFunctionCallChoice.Functions.Single()); + + Assert.Equal(11, requiredFunctionCallChoice.MaximumAutoInvokeAttempts); + Assert.Equal(1, requiredFunctionCallChoice.MaximumUseAttempts); + } + + [Fact] + public void DeserializingNoneFunctionCallingChoice() + { + // Arrange + string configPayload = """ + { + "schema": 1, + "execution_settings": { + "default": { + "model_id": "gpt-4", + "tool_behaviors": [ + { + "type": "function_call_behavior", + "choice":{ + "type": "none" + } + } + ] + } + } + } + """; + + // Act + var promptTemplateConfig = PromptTemplateConfig.FromJson(configPayload); + + // Assert + Assert.NotNull(promptTemplateConfig); + Assert.Single(promptTemplateConfig.ExecutionSettings); + + var executionSettings = promptTemplateConfig.ExecutionSettings.Single(); + Assert.NotNull(executionSettings.Value.ToolBehaviors); + Assert.Single(executionSettings.Value.ToolBehaviors); + + var functionCallBehavior = executionSettings.Value.ToolBehaviors.Single() as FunctionCallBehavior; + Assert.NotNull(functionCallBehavior); + Assert.IsType(functionCallBehavior.Choice); + } + [Fact] public void DeserializingExpectInputVariables() { From 0c088d8c3f2ae670351aed42cb864c4cc6e189e0 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 1 May 2024 16:13:28 +0100 Subject: [PATCH 43/90] Old example removed --- .../Example59_OpenAIFunctionCalling.cs | 190 ------------------ 1 file changed, 190 deletions(-) delete mode 100644 dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs deleted file mode 100644 index c9cb34988bdf..000000000000 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; -using Xunit.Abstractions; - -namespace Examples; - -// This example shows how to use OpenAI's tool calling capability via the chat completions interface. -public class Example59_OpenAIFunctionCalling(ITestOutputHelper output) : BaseTest(output) -{ - [Fact] - public async Task RunAsync() - { - // Create kernel. - IKernelBuilder builder = Kernel.CreateBuilder(); - - // We recommend the usage of OpenAI latest models for the best experience with tool calling. - // i.e. gpt-3.5-turbo-1106 or gpt-4-1106-preview - builder.AddOpenAIChatCompletion("gpt-3.5-turbo-1106", TestConfiguration.OpenAI.ApiKey); - - builder.Services.AddLogging(services => services.AddConsole().SetMinimumLevel(LogLevel.Trace)); - Kernel kernel = builder.Build(); - - // Add a plugin with some helper functions we want to allow the model to utilize. - kernel.ImportPluginFromFunctions("HelperFunctions", - [ - kernel.CreateFunctionFromMethod(() => DateTime.UtcNow.ToString("R"), "GetCurrentUtcTime", "Retrieves the current time in UTC."), - kernel.CreateFunctionFromMethod((string cityName) => - cityName switch - { - "Boston" => "61 and rainy", - "London" => "55 and cloudy", - "Miami" => "80 and sunny", - "Paris" => "60 and rainy", - "Tokyo" => "50 and sunny", - "Sydney" => "75 and sunny", - "Tel Aviv" => "80 and sunny", - _ => "31 and snowing", - }, "Get_Weather_For_City", "Gets the current weather for the specified city"), - ]); - - WriteLine("======== Example 1: Use automated function calling with a non-streaming prompt ========"); - { - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - WriteLine(await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings))); - WriteLine(); - } - - WriteLine("======== Example 2: Use automated function calling with a streaming prompt ========"); - { - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - await foreach (var update in kernel.InvokePromptStreamingAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings))) - { - Write(update); - } - WriteLine(); - } - - WriteLine("======== Example 3: Use manual function calling with a non-streaming prompt ========"); - { - var chat = kernel.GetRequiredService(); - - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - while (true) - { - ChatMessageContent result = await chat.GetChatMessageContentAsync(chatHistory, settings, kernel); - if (result.Content is not null) - { - Write(result.Content); - } - - IEnumerable functionCalls = FunctionCallRequestContent.GetFunctionCalls(result); - if (!functionCalls.Any()) - { - break; - } - - chatHistory.Add(result); // Adding LLM response containing function calls(requests) to chat history as it's required by LLMs. - - foreach (var functionCall in functionCalls) - { - try - { - FunctionCallResultContent resultContent = await functionCall.InvokeAsync(kernel); // Executing each function. - - chatHistory.Add(resultContent.ToChatMessage()); - } - catch (Exception ex) - { - chatHistory.Add(new FunctionCallResultContent(functionCall, ex).ToChatMessage()); // Adding function result to chat history. - // Adding exception to chat history. - // or - //string message = "Error details that LLM can reason about."; - //chatHistory.Add(new FunctionCallResultContent(functionCall, message).ToChatMessageContent()); // Adding function result to chat history. - } - } - - WriteLine(); - } - } - - WriteLine("======== Example 4: Simulated function calling with a non-streaming prompt ========"); - { - var chat = kernel.GetRequiredService(); - - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - while (true) - { - ChatMessageContent result = await chat.GetChatMessageContentAsync(chatHistory, settings, kernel); - if (result.Content is not null) - { - Write(result.Content); - } - - chatHistory.Add(result); // Adding LLM response containing function calls(requests) to chat history as it's required by LLMs. - - IEnumerable functionCalls = FunctionCallRequestContent.GetFunctionCalls(result); - if (!functionCalls.Any()) - { - break; - } - - foreach (var functionCall in functionCalls) - { - FunctionCallResultContent resultContent = await functionCall.InvokeAsync(kernel); // Executing each function. - - chatHistory.Add(resultContent.ToChatMessage()); - } - - // Adding a simulated function call request to the connector response message - var simulatedFunctionCall = new FunctionCallRequestContent("weather-alert", id: "call_123"); - result.Items.Add(simulatedFunctionCall); - - // Adding a simulated function result to chat history - var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; - chatHistory.Add(new FunctionCallResultContent(simulatedFunctionCall, simulatedFunctionResult).ToChatMessage()); - - WriteLine(); - } - } - - /* Uncomment this to try in a console chat loop. - Console.WriteLine("======== Example 5: Use automated function calling with a streaming chat ========"); - { - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - var chat = kernel.GetRequiredService(); - var chatHistory = new ChatHistory(); - - while (true) - { - Console.Write("Question (Type \"quit\" to leave): "); - string question = Console.ReadLine() ?? string.Empty; - if (question == "quit") - { - break; - } - - chatHistory.AddUserMessage(question); - StringBuilder sb = new(); - await foreach (var update in chat.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) - { - if (update.Content is not null) - { - Console.Write(update.Content); - sb.Append(update.Content); - } - } - chatHistory.AddAssistantMessage(sb.ToString()); - Console.WriteLine(); - } - }*/ - } -} From 09feb8da76c6eb0aaac0564a8116c91f9b0cd5a0 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 1 May 2024 16:18:48 +0100 Subject: [PATCH 44/90] ConfigureFunctionCallingOptions method moved down to simplify review --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 216 +++++++++--------- 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index de18af44a59d..ec02175bdd84 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -758,114 +758,6 @@ static void AddResponseMessage( } } - private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts, int? MaximumUseAttempts)? ConfigureFunctionCallingOptions(Kernel? kernel, OpenAIPromptExecutionSettings executionSettings, ChatCompletionsOptions chatOptions, int iteration) - { - if (executionSettings.ToolBehaviors is not null && executionSettings.ToolCallBehavior is not null) - { - throw new ArgumentException("ToolBehaviors and ToolCallBehavior cannot be used together."); - } - - // Handling old-style tool call behavior represented by `OpenAIPromptExecutionSettings.ToolCallBehavior` property. - if (executionSettings.ToolCallBehavior is { } toolCallBehavior) - { - if (iteration >= 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 tools.", 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. - toolCallBehavior.ConfigureOptions(kernel, chatOptions); - } - - return new() - { - AllowAnyRequestedKernelFunction = toolCallBehavior.AllowAnyRequestedKernelFunction, - MaximumAutoInvokeAttempts = toolCallBehavior.MaximumAutoInvokeAttempts, - MaximumUseAttempts = toolCallBehavior.MaximumUseAttempts - }; - } - - // Handling new tool behavior represented by `PromptExecutionSettings.ToolBehaviors` property. - if (executionSettings.ToolBehaviors?.OfType() is { } functionCallBehaviors && functionCallBehaviors.Any()) - { - if (functionCallBehaviors.Count() > 1) - { - throw new KernelException("Only one function call behavior is allowed."); - } - - var functionCallBehavior = functionCallBehaviors.Single(); - - // Regenerate the tool list as necessary and getting other call behavior properties. The invocation of the function(s) could have augmented - // what functions are available in the kernel. - var config = functionCallBehavior.Choice.Configure(new() { Kernel = kernel, Model = chatOptions }); - if (config is null) - { - return null; - } - - (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts, int? MaximumUseAttempts) result = new() - { - AllowAnyRequestedKernelFunction = config.AllowAnyRequestedKernelFunction, - MaximumAutoInvokeAttempts = config.MaximumAutoInvokeAttempts, - MaximumUseAttempts = config.MaximumUseAttempts - }; - - if (iteration >= config.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 functions.", config.MaximumUseAttempts); - } - - return result; - } - - // If we have a required function, it means we want to force LLM to invoke that function. - if (config.RequiredFunctions is { } requiredFunctions && requiredFunctions.Any()) - { - if (requiredFunctions.Count() > 1) - { - throw new KernelException("Only one required function is allowed."); - } - - var functionDefinition = requiredFunctions.First().ToOpenAIFunction().ToFunctionDefinition(); - - chatOptions.ToolChoice = new ChatCompletionsToolChoice(functionDefinition); - chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); - - return result; - } - - // If we have available functions, we want LLM to choose which function(s) to call. - if (config.AvailableFunctions is { } availableFunctions && availableFunctions.Any()) - { - chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; - - foreach (var function in availableFunctions) - { - var functionDefinition = function.ToOpenAIFunction().ToFunctionDefinition(); - chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); - } - - return result; - } - - // If we have neither required nor available functions, we don't want LLM to call any functions. - chatOptions.ToolChoice = ChatCompletionsToolChoice.None; - - return result; - } - - return null; - } - /// Checks if a tool call is for a function that was defined. private static bool IsRequestableTool(ChatCompletionsOptions options, OpenAIFunctionToolCall ftc) { @@ -1470,4 +1362,112 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context await functionCallCallback(context).ConfigureAwait(false); } } + + private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts, int? MaximumUseAttempts)? ConfigureFunctionCallingOptions(Kernel? kernel, OpenAIPromptExecutionSettings executionSettings, ChatCompletionsOptions chatOptions, int iteration) + { + if (executionSettings.ToolBehaviors is not null && executionSettings.ToolCallBehavior is not null) + { + throw new ArgumentException("ToolBehaviors and ToolCallBehavior cannot be used together."); + } + + // Handling old-style tool call behavior represented by `OpenAIPromptExecutionSettings.ToolCallBehavior` property. + if (executionSettings.ToolCallBehavior is { } toolCallBehavior) + { + if (iteration >= 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 tools.", 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. + toolCallBehavior.ConfigureOptions(kernel, chatOptions); + } + + return new() + { + AllowAnyRequestedKernelFunction = toolCallBehavior.AllowAnyRequestedKernelFunction, + MaximumAutoInvokeAttempts = toolCallBehavior.MaximumAutoInvokeAttempts, + MaximumUseAttempts = toolCallBehavior.MaximumUseAttempts + }; + } + + // Handling new tool behavior represented by `PromptExecutionSettings.ToolBehaviors` property. + if (executionSettings.ToolBehaviors?.OfType() is { } functionCallBehaviors && functionCallBehaviors.Any()) + { + if (functionCallBehaviors.Count() > 1) + { + throw new KernelException("Only one function call behavior is allowed."); + } + + var functionCallBehavior = functionCallBehaviors.Single(); + + // Regenerate the tool list as necessary and getting other call behavior properties. The invocation of the function(s) could have augmented + // what functions are available in the kernel. + var config = functionCallBehavior.Choice.Configure(new() { Kernel = kernel, Model = chatOptions }); + if (config is null) + { + return null; + } + + (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts, int? MaximumUseAttempts) result = new() + { + AllowAnyRequestedKernelFunction = config.AllowAnyRequestedKernelFunction, + MaximumAutoInvokeAttempts = config.MaximumAutoInvokeAttempts, + MaximumUseAttempts = config.MaximumUseAttempts + }; + + if (iteration >= config.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 functions.", config.MaximumUseAttempts); + } + + return result; + } + + // If we have a required function, it means we want to force LLM to invoke that function. + if (config.RequiredFunctions is { } requiredFunctions && requiredFunctions.Any()) + { + if (requiredFunctions.Count() > 1) + { + throw new KernelException("Only one required function is allowed."); + } + + var functionDefinition = requiredFunctions.First().ToOpenAIFunction().ToFunctionDefinition(); + + chatOptions.ToolChoice = new ChatCompletionsToolChoice(functionDefinition); + chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); + + return result; + } + + // If we have available functions, we want LLM to choose which function(s) to call. + if (config.AvailableFunctions is { } availableFunctions && availableFunctions.Any()) + { + chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; + + foreach (var function in availableFunctions) + { + var functionDefinition = function.ToOpenAIFunction().ToFunctionDefinition(); + chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); + } + + return result; + } + + // If we have neither required nor available functions, we don't want LLM to call any functions. + chatOptions.ToolChoice = ChatCompletionsToolChoice.None; + + return result; + } + + return null; + } } From ebabb7e01e4551edb933a033f5c37952b76d2d5a Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 1 May 2024 16:24:57 +0100 Subject: [PATCH 45/90] renaming --- .../Connectors.OpenAI/OpenAIPromptExecutionSettings.cs | 2 +- .../Functions/Functions.Markdown/KernelFunctionMarkdown.cs | 2 +- .../Connectors/OpenAI/OpenAIFunctionsTests.cs | 2 +- .../IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs | 2 +- .../{ToolBehaviorsResolver.cs => ToolBehaviorResolver.cs} | 4 ++-- .../PromptTemplate/PromptTemplateConfig.cs | 2 +- .../Functions/FunctionCallBehaviorTests.cs | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) rename dotnet/src/InternalUtilities/src/PromptSerialization/{ToolBehaviorsResolver.cs => ToolBehaviorResolver.cs} (90%) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs index de353a5dfb30..572398d27da4 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs @@ -18,7 +18,7 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings { - private readonly static JsonSerializerOptions s_serializerOptions = new(JsonOptionsCache.ReadPermissive) { TypeInfoResolver = ToolBehaviorsResolver.Instance }; + private readonly static JsonSerializerOptions s_serializerOptions = new(JsonOptionsCache.ReadPermissive) { TypeInfoResolver = ToolBehaviorResolver.Instance }; /// /// Temperature controls the randomness of the completion. diff --git a/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs b/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs index 56d7b6de6592..ab0230041a11 100644 --- a/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs +++ b/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs @@ -13,7 +13,7 @@ namespace Microsoft.SemanticKernel; /// public static class KernelFunctionMarkdown { - private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() { TypeInfoResolver = ToolBehaviorsResolver.Instance }; + private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() { TypeInfoResolver = ToolBehaviorResolver.Instance }; /// /// Creates a instance for a prompt function using the specified markdown text. diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs index 0f88ed744198..e4eb93b08837 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs @@ -468,7 +468,7 @@ public async Task SubsetOfFunctionsCanBeUsedForFunctionCallingAsync() } [Fact] - public async Task RequiredFunctionShouldBeCalledByAsync() + public async Task RequiredFunctionShouldBeCalledAsync() { // Arrange var kernel = this.InitializeKernel(importHelperPlugin: false); diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index 7fc46ef75156..dcb376d712c3 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -463,7 +463,7 @@ public async Task SubsetOfFunctionsCanBeUsedForFunctionCallingAsync() } [Fact] - public async Task RequiredFunctionShouldBeCalledByAsync() + public async Task RequiredFunctionShouldBeCalledAsync() { // Arrange var kernel = this.InitializeKernel(importHelperPlugin: false); diff --git a/dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorsResolver.cs b/dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorResolver.cs similarity index 90% rename from dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorsResolver.cs rename to dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorResolver.cs index 3fa8a0225b34..75d46ee69c6b 100644 --- a/dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorsResolver.cs +++ b/dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorResolver.cs @@ -9,9 +9,9 @@ namespace Microsoft.SemanticKernel; [ExcludeFromCodeCoverage] -internal sealed class ToolBehaviorsResolver : DefaultJsonTypeInfoResolver +internal sealed class ToolBehaviorResolver : DefaultJsonTypeInfoResolver { - public static ToolBehaviorsResolver Instance { get; } = new ToolBehaviorsResolver(); + public static ToolBehaviorResolver Instance { get; } = new ToolBehaviorResolver(); public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) { diff --git a/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs b/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs index 864f7098d401..af1424227538 100644 --- a/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs +++ b/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs @@ -25,7 +25,7 @@ namespace Microsoft.SemanticKernel; /// public sealed class PromptTemplateConfig { - private readonly static JsonSerializerOptions s_serializerOptions = new(JsonOptionsCache.ReadPermissive) { TypeInfoResolver = ToolBehaviorsResolver.Instance }; + private readonly static JsonSerializerOptions s_serializerOptions = new(JsonOptionsCache.ReadPermissive) { TypeInfoResolver = ToolBehaviorResolver.Instance }; /// The format of the prompt template. private string? _templateFormat; diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallBehaviorTests.cs index aa2047f6a5cd..cb4457838c5f 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallBehaviorTests.cs @@ -15,7 +15,7 @@ namespace SemanticKernel.UnitTests.Functions; /// public sealed class FunctionCallBehaviorTests { - private readonly static JsonSerializerOptions s_serializerOptions = new() { TypeInfoResolver = new ToolBehaviorsResolver() }; + private readonly static JsonSerializerOptions s_serializerOptions = new() { TypeInfoResolver = new ToolBehaviorResolver() }; [Fact] public void EnableKernelFunctionsAreNotAutoInvoked() From 15e98e3fd452c99720a875dd683a1614256b17ac Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 1 May 2024 16:33:24 +0100 Subject: [PATCH 46/90] Solution item name change --- dotnet/SK-dotnet.sln | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 9ad78a8689ec..333d33f5d458 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -113,7 +113,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Diagnostics", "Diagnostics" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PromptSerialization", "PromptSerialization", "{6A4C2EAA-E1B4-4F33-A4E6-21ED36413919}" ProjectSection(SolutionItems) = preProject - src\InternalUtilities\src\PromptSerialization\ToolBehaviorsResolver.cs = src\InternalUtilities\src\PromptSerialization\ToolBehaviorsResolver.cs + src\InternalUtilities\src\PromptSerialization\ToolBehaviorResolver.cs = src\InternalUtilities\src\PromptSerialization\ToolBehaviorResolver.cs EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linq", "Linq", "{B00AD427-0047-4850-BEF9-BA8237EA9D8B}" From 78c59fffe180461cf3c0ffbbbb57ad295950be39 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 2 May 2024 20:46:05 +0100 Subject: [PATCH 47/90] Breaking glass scenario is desabled --- dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs | 2 +- .../Choices/{FunctionCallBehavior.cs => FunctionCallChoice.cs} | 0 .../AI/ToolBehaviors/Choices/FunctionCallChoiceContext.cs | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) rename dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/{FunctionCallBehavior.cs => FunctionCallChoice.cs} (100%) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index ec02175bdd84..e87d00f120fb 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1408,7 +1408,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context // Regenerate the tool list as necessary and getting other call behavior properties. The invocation of the function(s) could have augmented // what functions are available in the kernel. - var config = functionCallBehavior.Choice.Configure(new() { Kernel = kernel, Model = chatOptions }); + var config = functionCallBehavior.Choice.Configure(new() { Kernel = kernel }); if (config is null) { return null; diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoice.cs similarity index 100% rename from dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallBehavior.cs rename to dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoice.cs diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceContext.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceContext.cs index 192c9c8f2e49..b38d63cac410 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceContext.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceContext.cs @@ -5,6 +5,4 @@ namespace Microsoft.SemanticKernel.AI.ToolBehaviors; public class FunctionCallChoiceContext { public Kernel? Kernel { get; init; } - - public object? Model { get; init; } } From fb39b4422669a3f38f03ee8a8388756e5d0efc3e Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 3 May 2024 12:38:00 +0100 Subject: [PATCH 48/90] The PromptExecutionSettings.ToolBehaviors property has been renamed to ToolBehavior, and its type has changed from IEnumerable to ToolBehavior in the spirit of reducing "complexity" and simplifying dev experience. --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 11 +-- .../Functions/KernelFunctionMarkdownTests.cs | 55 +++++++-------- .../Yaml/Functions/KernelFunctionYamlTests.cs | 30 ++++----- .../PromptExecutionSettingsTypeConverter.cs | 4 +- .../Connectors/OpenAI/OpenAIFunctionsTests.cs | 28 ++++---- .../AI/PromptExecutionSettings.cs | 12 ++-- .../PromptTemplateConfigTests.cs | 67 ++++++++----------- 7 files changed, 89 insertions(+), 118 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index e87d00f120fb..e1173b8ed66e 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1365,7 +1365,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts, int? MaximumUseAttempts)? ConfigureFunctionCallingOptions(Kernel? kernel, OpenAIPromptExecutionSettings executionSettings, ChatCompletionsOptions chatOptions, int iteration) { - if (executionSettings.ToolBehaviors is not null && executionSettings.ToolCallBehavior is not null) + if (executionSettings.ToolBehavior is not null && executionSettings.ToolCallBehavior is not null) { throw new ArgumentException("ToolBehaviors and ToolCallBehavior cannot be used together."); } @@ -1397,15 +1397,8 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context } // Handling new tool behavior represented by `PromptExecutionSettings.ToolBehaviors` property. - if (executionSettings.ToolBehaviors?.OfType() is { } functionCallBehaviors && functionCallBehaviors.Any()) + if (executionSettings.ToolBehavior is FunctionCallBehavior functionCallBehavior) { - if (functionCallBehaviors.Count() > 1) - { - throw new KernelException("Only one function call behavior is allowed."); - } - - var functionCallBehavior = functionCallBehaviors.Single(); - // Regenerate the tool list as necessary and getting other call behavior properties. The invocation of the function(s) could have augmented // what functions are available in the kernel. var config = functionCallBehavior.Choice.Configure(new() { Kernel = kernel }); diff --git a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs index 5bb940e72fc4..77e30827f451 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs @@ -43,10 +43,9 @@ public void ItShouldInitializeFunctionCallChoicesFromMarkdown() // AutoFunctionCallChoice for service1 var service1ExecutionSettings = function.ExecutionSettings["service1"]; - Assert.NotNull(service1ExecutionSettings?.ToolBehaviors); - Assert.Single(service1ExecutionSettings.ToolBehaviors); + Assert.NotNull(service1ExecutionSettings?.ToolBehavior); - var service1FunctionCallBehavior = service1ExecutionSettings.ToolBehaviors.Single() as FunctionCallBehavior; + var service1FunctionCallBehavior = service1ExecutionSettings.ToolBehavior as FunctionCallBehavior; Assert.NotNull(service1FunctionCallBehavior?.Choice); var service1AutoFunctionCallChoice = service1FunctionCallBehavior?.Choice as AutoFunctionCallChoice; @@ -58,10 +57,9 @@ public void ItShouldInitializeFunctionCallChoicesFromMarkdown() // RequiredFunctionCallChoice for service2 var service2ExecutionSettings = function.ExecutionSettings["service2"]; - Assert.NotNull(service2ExecutionSettings?.ToolBehaviors); - Assert.Single(service2ExecutionSettings.ToolBehaviors); + Assert.NotNull(service2ExecutionSettings?.ToolBehavior); - var service2FunctionCallBehavior = service2ExecutionSettings.ToolBehaviors.Single() as FunctionCallBehavior; + var service2FunctionCallBehavior = service2ExecutionSettings.ToolBehavior as FunctionCallBehavior; Assert.NotNull(service2FunctionCallBehavior?.Choice); var service2RequiredFunctionCallChoice = service2FunctionCallBehavior?.Choice as RequiredFunctionCallChoice; @@ -72,8 +70,7 @@ public void ItShouldInitializeFunctionCallChoicesFromMarkdown() // NoneFunctionCallChoice for service3 var service3ExecutionSettings = function.ExecutionSettings["service3"]; - Assert.NotNull(service3ExecutionSettings?.ToolBehaviors); - Assert.Single(service3ExecutionSettings.ToolBehaviors); + Assert.NotNull(service3ExecutionSettings?.ToolBehavior); } [Fact] @@ -101,16 +98,14 @@ These are AI execution settings "service1" : { "model_id": "gpt4", "temperature": 0.7, - "tool_behaviors": [ - { - "type": "function_call_behavior", - "choice": { - "type": "auto", - "allowAnyRequestedKernelFunction" : true, - "functions": ["p1.f1"] - } + "tool_behavior": { + "type": "function_call_behavior", + "choice": { + "type": "auto", + "allowAnyRequestedKernelFunction" : true, + "functions": ["p1.f1"] } - ] + } } } ``` @@ -120,15 +115,13 @@ These are more AI execution settings "service2" : { "model_id": "gpt3.5", "temperature": 0.8, - "tool_behaviors": [ - { - "type": "function_call_behavior", - "choice": { - "type": "required", - "functions": ["p1.f1"] - } + "tool_behavior": { + "type": "function_call_behavior", + "choice": { + "type": "required", + "functions": ["p1.f1"] } - ] + } } } ``` @@ -138,14 +131,12 @@ These are AI execution settings as well "service3" : { "model_id": "gpt3.5-turbo", "temperature": 0.8, - "tool_behaviors": [ - { - "type": "function_call_behavior", - "choice": { - "type": "none" - } + "tool_behavior": { + "type": "function_call_behavior", + "choice": { + "type": "none" } - ] + } } } ``` diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs index f353f2fcecc7..5705da4f0328 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs @@ -97,10 +97,9 @@ public void ItShouldDeserializeFunctionCallChoices() // Service with auto function call choice var service1ExecutionSettings = promptTemplateConfig.ExecutionSettings["service1"]; - Assert.NotNull(service1ExecutionSettings?.ToolBehaviors); - Assert.Single(service1ExecutionSettings.ToolBehaviors); + Assert.NotNull(service1ExecutionSettings?.ToolBehavior); - var service1FunctionCallBehavior = service1ExecutionSettings.ToolBehaviors.Single() as FunctionCallBehavior; + var service1FunctionCallBehavior = service1ExecutionSettings.ToolBehavior as FunctionCallBehavior; Assert.NotNull(service1FunctionCallBehavior?.Choice); var autoFunctionCallChoice = service1FunctionCallBehavior.Choice as AutoFunctionCallChoice; @@ -109,10 +108,9 @@ public void ItShouldDeserializeFunctionCallChoices() // Service with required function call choice var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; - Assert.NotNull(service2ExecutionSettings?.ToolBehaviors); - Assert.Single(service2ExecutionSettings.ToolBehaviors); + Assert.NotNull(service2ExecutionSettings?.ToolBehavior); - var service2FunctionCallBehavior = service2ExecutionSettings.ToolBehaviors.Single() as FunctionCallBehavior; + var service2FunctionCallBehavior = service2ExecutionSettings.ToolBehavior as FunctionCallBehavior; Assert.NotNull(service2FunctionCallBehavior?.Choice); var requiredFunctionCallChoice = service2FunctionCallBehavior.Choice as RequiredFunctionCallChoice; @@ -195,11 +193,11 @@ string CreateYaml(object defaultValue) frequency_penalty: 0.0 max_tokens: 256 stop_sequences: [] - tool_behaviors: - - !function_call_behavior - choice: !auto - functions: - - p1.f1 + tool_behavior: + !function_call_behavior + choice: !auto + functions: + - p1.f1 service2: model_id: gpt-3.5 temperature: 1.0 @@ -208,11 +206,11 @@ string CreateYaml(object defaultValue) frequency_penalty: 0.0 max_tokens: 256 stop_sequences: [ "foo", "bar", "baz" ] - tool_behaviors: - - !function_call_behavior - choice: !required - functions: - - p2.f2 + tool_behavior: + !function_call_behavior + choice: !required + functions: + - p2.f2 """; private readonly string _yamlWithCustomSettings = """ diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs index f9be1cee27d9..ca1a2e892b43 100644 --- a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs @@ -38,8 +38,8 @@ public bool Accepts(Type type) case "model_id": executionSettings.ModelId = s_deserializer.Deserialize(parser); break; - case "tool_behaviors": - executionSettings.ToolBehaviors = s_deserializer.Deserialize>(parser); + case "tool_behavior": + executionSettings.ToolBehavior = s_deserializer.Deserialize(parser); break; default: (executionSettings.ExtensionData ??= new Dictionary()).Add(propertyName, s_deserializer.Deserialize(parser)); diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs index e4eb93b08837..0c62ace921f9 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs @@ -40,7 +40,7 @@ void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e) #pragma warning restore CS0618 // Events are deprecated // Act - OpenAIPromptExecutionSettings settings = new() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + OpenAIPromptExecutionSettings settings = new() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; var result = await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.", new(settings)); // Assert @@ -67,7 +67,7 @@ void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e) #pragma warning restore CS0618 // Events are deprecated // Act - OpenAIPromptExecutionSettings settings = new() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + OpenAIPromptExecutionSettings settings = new() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; string result = ""; await foreach (string c in kernel.InvokePromptStreamingAsync( $"How much older is John than Jim? Compute that value and pass it to the {nameof(TimeInformation)}.{nameof(TimeInformation.InterpretValue)} function, then respond only with its result.", @@ -91,7 +91,7 @@ public async Task CanAutoInvokeKernelFunctionsWithComplexTypeParametersAsync() kernel.ImportPluginFromType(); // Act - OpenAIPromptExecutionSettings settings = new() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + OpenAIPromptExecutionSettings settings = new() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; var result = await kernel.InvokePromptAsync("What is the current temperature in Dublin, Ireland, in Fahrenheit?", new(settings)); // Assert @@ -107,7 +107,7 @@ public async Task CanAutoInvokeKernelFunctionsWithPrimitiveTypeParametersAsync() kernel.ImportPluginFromType(); // Act - OpenAIPromptExecutionSettings settings = new() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + OpenAIPromptExecutionSettings settings = new() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; var result = await kernel.InvokePromptAsync("Convert 50 degrees Fahrenheit to Celsius.", new(settings)); @@ -134,7 +134,7 @@ public async Task CanAutoInvokeKernelFunctionFromPromptAsync() [promptFunction])); // Act - OpenAIPromptExecutionSettings settings = new() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + OpenAIPromptExecutionSettings settings = new() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; var result = await kernel.InvokePromptAsync("Show me the latest news as they are.", new(settings)); // Assert @@ -159,7 +159,7 @@ public async Task CanAutoInvokeKernelFunctionFromPromptStreamingAsync() [promptFunction])); // Act - OpenAIPromptExecutionSettings settings = new() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + OpenAIPromptExecutionSettings settings = new() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; var streamingResult = kernel.InvokePromptStreamingAsync("Show me the latest news as they are.", new(settings)); var builder = new StringBuilder(); @@ -185,7 +185,7 @@ public async Task ConnectorSpecificChatMessageContentClassesCanBeUsedForManualFu var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false)] }; + var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false) }; var sut = kernel.GetRequiredService(); @@ -232,7 +232,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManual var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false)] }; + var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false) }; var sut = kernel.GetRequiredService(); @@ -273,7 +273,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc chatHistory.AddSystemMessage("If you are unable to answer the question for whatever reason, please add the 'error' keyword to the response."); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false)] }; + var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false) }; var completionService = kernel.GetRequiredService(); @@ -317,7 +317,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu chatHistory.AddSystemMessage("if there's a tornado warning, please add the 'tornado' keyword to the response."); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false)] }; + var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false) }; var completionService = kernel.GetRequiredService(); @@ -365,7 +365,7 @@ public async Task ItFailsIfNoFunctionResultProvidedAsync() var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false)] }; + var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false) }; var completionService = kernel.GetRequiredService(); @@ -389,7 +389,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFu var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice()] }; + var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; var sut = kernel.GetRequiredService(); @@ -455,7 +455,7 @@ public async Task SubsetOfFunctionsCanBeUsedForFunctionCallingAsync() var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("What day is today?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: true)] }; + var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: true) }; var sut = kernel.GetRequiredService(); @@ -479,7 +479,7 @@ public async Task RequiredFunctionShouldBeCalledAsync() var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("What day is today?"); - var settings = new PromptExecutionSettings() { ToolBehaviors = [FunctionCallBehavior.RequiredFunctionChoice([function], true)] }; + var settings = new PromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.RequiredFunctionChoice([function], true) }; var sut = kernel.GetRequiredService(); diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs index 886c5952ece4..50d035381068 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs @@ -44,15 +44,15 @@ public string? ModelId } } - [JsonPropertyName("tool_behaviors")] - public IEnumerable? ToolBehaviors + [JsonPropertyName("tool_behavior")] + public ToolBehavior? ToolBehavior { - get => this._toolBehaviors; + get => this._toolBehavior; set { this.ThrowIfFrozen(); - this._toolBehaviors = value; + this._toolBehavior = value; } } @@ -106,7 +106,7 @@ public virtual PromptExecutionSettings Clone() { ModelId = this.ModelId, ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, - ToolBehaviors = this.ToolBehaviors + ToolBehavior = this.ToolBehavior }; } @@ -126,7 +126,7 @@ protected void ThrowIfFrozen() private string? _modelId; private IDictionary? _extensionData; - private IEnumerable? _toolBehaviors; + private ToolBehavior? _toolBehavior; #endregion } diff --git a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs index eb55444666c8..8909091bd025 100644 --- a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs @@ -153,19 +153,15 @@ public void DeserializingAutoFunctionCallingChoice() "execution_settings": { "default": { "model_id": "gpt-4", - "tool_behaviors": [ - { - "type": "function_call_behavior", - "choice":{ - "type": "auto", - "allowAnyRequestedKernelFunction" : true, - "maximumAutoInvokeAttempts": 12, - "functions":[ - "p1.f1" - ] - } + "tool_behavior": { + "type": "function_call_behavior", + "choice":{ + "type": "auto", + "allowAnyRequestedKernelFunction" : true, + "maximumAutoInvokeAttempts": 12, + "functions":["p1.f1"] } - ] + } } } } @@ -179,10 +175,9 @@ public void DeserializingAutoFunctionCallingChoice() Assert.Single(promptTemplateConfig.ExecutionSettings); var executionSettings = promptTemplateConfig.ExecutionSettings.Single(); - Assert.NotNull(executionSettings.Value.ToolBehaviors); - Assert.Single(executionSettings.Value.ToolBehaviors); + Assert.NotNull(executionSettings.Value.ToolBehavior); - var functionCallBehavior = executionSettings.Value.ToolBehaviors.Single() as FunctionCallBehavior; + var functionCallBehavior = executionSettings.Value.ToolBehavior as FunctionCallBehavior; Assert.NotNull(functionCallBehavior); var autoFunctionCallChoice = functionCallBehavior.Choice as AutoFunctionCallChoice; @@ -202,18 +197,16 @@ public void DeserializingRequiredFunctionCallingChoice() "execution_settings": { "default": { "model_id": "gpt-4", - "tool_behaviors": [ - { - "type": "function_call_behavior", - "choice":{ - "type": "required", - "maximumAutoInvokeAttempts": 11, - "functions":[ - "p1.f1" - ] - } + "tool_behavior": { + "type": "function_call_behavior", + "choice":{ + "type": "required", + "maximumAutoInvokeAttempts": 11, + "functions":[ + "p1.f1" + ] } - ] + } } } } @@ -227,10 +220,9 @@ public void DeserializingRequiredFunctionCallingChoice() Assert.Single(promptTemplateConfig.ExecutionSettings); var executionSettings = promptTemplateConfig.ExecutionSettings.Single(); - Assert.NotNull(executionSettings.Value.ToolBehaviors); - Assert.Single(executionSettings.Value.ToolBehaviors); + Assert.NotNull(executionSettings.Value.ToolBehavior); - var functionCallBehavior = executionSettings.Value.ToolBehaviors.Single() as FunctionCallBehavior; + var functionCallBehavior = executionSettings.Value.ToolBehavior as FunctionCallBehavior; Assert.NotNull(functionCallBehavior); var requiredFunctionCallChoice = functionCallBehavior.Choice as RequiredFunctionCallChoice; @@ -251,14 +243,12 @@ public void DeserializingNoneFunctionCallingChoice() "execution_settings": { "default": { "model_id": "gpt-4", - "tool_behaviors": [ - { - "type": "function_call_behavior", - "choice":{ - "type": "none" - } + "tool_behavior": { + "type": "function_call_behavior", + "choice":{ + "type": "none" } - ] + } } } } @@ -272,10 +262,9 @@ public void DeserializingNoneFunctionCallingChoice() Assert.Single(promptTemplateConfig.ExecutionSettings); var executionSettings = promptTemplateConfig.ExecutionSettings.Single(); - Assert.NotNull(executionSettings.Value.ToolBehaviors); - Assert.Single(executionSettings.Value.ToolBehaviors); + Assert.NotNull(executionSettings.Value.ToolBehavior); - var functionCallBehavior = executionSettings.Value.ToolBehaviors.Single() as FunctionCallBehavior; + var functionCallBehavior = executionSettings.Value.ToolBehavior as FunctionCallBehavior; Assert.NotNull(functionCallBehavior); Assert.IsType(functionCallBehavior.Choice); } From 7cbaaf58c2035af40cd8aa5e017b9d84f7fbc91e Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 8 May 2024 11:38:02 +0100 Subject: [PATCH 49/90] fix: remove function call behavior and use function call choice instead --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 9 ++- .../OpenAIPromptExecutionSettings.cs | 2 +- .../KernelFunctionMarkdown.cs | 2 +- .../Functions/KernelFunctionMarkdownTests.cs | 63 ++++++++----------- .../Yaml/Functions/KernelFunctionYamlTests.cs | 61 +++++++++--------- .../FunctionCallBehaviorTypeConverter.cs | 36 ----------- .../PromptExecutionSettingsTypeConverter.cs | 10 +-- .../Connectors/OpenAI/OpenAIFunctionsTests.cs | 30 ++++----- .../FunctionChoiceBehaviorResolver.cs | 34 ++++++++++ .../ToolBehaviorResolver.cs | 46 -------------- .../AutoFunctionChoiceBehavior.cs} | 20 +++--- .../FunctionChoiceBehavior.cs | 30 +++++++++ .../FunctionChoiceBehaviorConfiguration.cs} | 6 +- .../FunctionChoiceBehaviorContext.cs} | 4 +- .../NoneFunctionChoiceBehavior.cs | 15 +++++ .../RequiredFunctionChoiceBehavior.cs} | 15 +++-- .../AI/PromptExecutionSettings.cs | 13 ++-- .../Choices/FunctionCallChoice.cs | 10 --- .../Choices/NoneFunctionCallChoice.cs | 16 ----- .../AI/ToolBehaviors/FunctionCallBehavior.cs | 36 ----------- .../AI/ToolBehaviors/ToolBehavior.cs | 7 --- .../PromptTemplate/PromptTemplateConfig.cs | 2 +- ...iorTests.cs => FunctionCallChoiceTests.cs} | 62 +++++++++--------- .../PromptTemplateConfigTests.cs | 53 +++++----------- 24 files changed, 232 insertions(+), 350 deletions(-) delete mode 100644 dotnet/src/Functions/Functions.Yaml/FunctionCallBehaviorTypeConverter.cs create mode 100644 dotnet/src/InternalUtilities/src/PromptSerialization/FunctionChoiceBehaviorResolver.cs delete mode 100644 dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorResolver.cs rename dotnet/src/SemanticKernel.Abstractions/AI/{ToolBehaviors/Choices/AutoFunctionCallChoice.cs => FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs} (80%) create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs rename dotnet/src/SemanticKernel.Abstractions/AI/{ToolBehaviors/Choices/FunctionCallChoiceConfiguration.cs => FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs} (69%) rename dotnet/src/SemanticKernel.Abstractions/AI/{ToolBehaviors/Choices/FunctionCallChoiceContext.cs => FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs} (51%) create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs rename dotnet/src/SemanticKernel.Abstractions/AI/{ToolBehaviors/Choices/RequiredFunctionCallChoice.cs => FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs} (84%) delete mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoice.cs delete mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/NoneFunctionCallChoice.cs delete mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/FunctionCallBehavior.cs delete mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/ToolBehavior.cs rename dotnet/src/SemanticKernel.UnitTests/Functions/{FunctionCallBehaviorTests.cs => FunctionCallChoiceTests.cs} (73%) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index e1173b8ed66e..e4ca5c15ae02 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -17,7 +17,6 @@ using Azure.Core.Pipeline; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.AI.ToolBehaviors; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Http; @@ -1365,7 +1364,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts, int? MaximumUseAttempts)? ConfigureFunctionCallingOptions(Kernel? kernel, OpenAIPromptExecutionSettings executionSettings, ChatCompletionsOptions chatOptions, int iteration) { - if (executionSettings.ToolBehavior is not null && executionSettings.ToolCallBehavior is not null) + if (executionSettings.FunctionChoiceBehavior is not null && executionSettings.ToolCallBehavior is not null) { throw new ArgumentException("ToolBehaviors and ToolCallBehavior cannot be used together."); } @@ -1397,11 +1396,11 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context } // Handling new tool behavior represented by `PromptExecutionSettings.ToolBehaviors` property. - if (executionSettings.ToolBehavior is FunctionCallBehavior functionCallBehavior) + if (executionSettings.FunctionChoiceBehavior is FunctionChoiceBehavior functionChoiceBehavior) { // Regenerate the tool list as necessary and getting other call behavior properties. The invocation of the function(s) could have augmented // what functions are available in the kernel. - var config = functionCallBehavior.Choice.Configure(new() { Kernel = kernel }); + var config = functionChoiceBehavior.Configure(new() { Kernel = kernel }); if (config is null) { return null; @@ -1409,7 +1408,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts, int? MaximumUseAttempts) result = new() { - AllowAnyRequestedKernelFunction = config.AllowAnyRequestedKernelFunction, + AllowAnyRequestedKernelFunction = false, MaximumAutoInvokeAttempts = config.MaximumAutoInvokeAttempts, MaximumUseAttempts = config.MaximumUseAttempts }; diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs index 572398d27da4..48497f522fa0 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs @@ -18,7 +18,7 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings { - private readonly static JsonSerializerOptions s_serializerOptions = new(JsonOptionsCache.ReadPermissive) { TypeInfoResolver = ToolBehaviorResolver.Instance }; + private readonly static JsonSerializerOptions s_serializerOptions = new(JsonOptionsCache.ReadPermissive) { TypeInfoResolver = FunctionChoiceBehaviorResolver.Instance }; /// /// Temperature controls the randomness of the completion. diff --git a/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs b/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs index ab0230041a11..93a288164fd9 100644 --- a/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs +++ b/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs @@ -13,7 +13,7 @@ namespace Microsoft.SemanticKernel; /// public static class KernelFunctionMarkdown { - private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() { TypeInfoResolver = ToolBehaviorResolver.Instance }; + private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() { TypeInfoResolver = FunctionChoiceBehaviorResolver.Instance }; /// /// Creates a instance for a prompt function using the specified markdown text. diff --git a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs index 77e30827f451..0753083f0612 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs @@ -2,7 +2,6 @@ using System.Linq; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI.ToolBehaviors; using Xunit; namespace SemanticKernel.Functions.UnitTests.Markdown.Functions; @@ -43,34 +42,31 @@ public void ItShouldInitializeFunctionCallChoicesFromMarkdown() // AutoFunctionCallChoice for service1 var service1ExecutionSettings = function.ExecutionSettings["service1"]; - Assert.NotNull(service1ExecutionSettings?.ToolBehavior); + Assert.NotNull(service1ExecutionSettings); - var service1FunctionCallBehavior = service1ExecutionSettings.ToolBehavior as FunctionCallBehavior; - Assert.NotNull(service1FunctionCallBehavior?.Choice); + var service1AutoFunctionChoiceBehavior = service1ExecutionSettings?.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; + Assert.NotNull(service1AutoFunctionChoiceBehavior); - var service1AutoFunctionCallChoice = service1FunctionCallBehavior?.Choice as AutoFunctionCallChoice; - Assert.NotNull(service1AutoFunctionCallChoice); - Assert.True(service1AutoFunctionCallChoice.AllowAnyRequestedKernelFunction); - Assert.NotNull(service1AutoFunctionCallChoice.Functions); - Assert.Single(service1AutoFunctionCallChoice.Functions); - Assert.Equal("p1.f1", service1AutoFunctionCallChoice.Functions.First()); + Assert.NotNull(service1AutoFunctionChoiceBehavior.Functions); + Assert.Single(service1AutoFunctionChoiceBehavior.Functions); + Assert.Equal("p1.f1", service1AutoFunctionChoiceBehavior.Functions.First()); // RequiredFunctionCallChoice for service2 var service2ExecutionSettings = function.ExecutionSettings["service2"]; - Assert.NotNull(service2ExecutionSettings?.ToolBehavior); + Assert.NotNull(service2ExecutionSettings); - var service2FunctionCallBehavior = service2ExecutionSettings.ToolBehavior as FunctionCallBehavior; - Assert.NotNull(service2FunctionCallBehavior?.Choice); - - var service2RequiredFunctionCallChoice = service2FunctionCallBehavior?.Choice as RequiredFunctionCallChoice; - Assert.NotNull(service2RequiredFunctionCallChoice); - Assert.NotNull(service2RequiredFunctionCallChoice.Functions); - Assert.Single(service2RequiredFunctionCallChoice.Functions); - Assert.Equal("p1.f1", service2RequiredFunctionCallChoice.Functions.First()); + var service2RequiredFunctionChoiceBehavior = service2ExecutionSettings?.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; + Assert.NotNull(service2RequiredFunctionChoiceBehavior); + Assert.NotNull(service2RequiredFunctionChoiceBehavior.Functions); + Assert.Single(service2RequiredFunctionChoiceBehavior.Functions); + Assert.Equal("p1.f1", service2RequiredFunctionChoiceBehavior.Functions.First()); // NoneFunctionCallChoice for service3 var service3ExecutionSettings = function.ExecutionSettings["service3"]; - Assert.NotNull(service3ExecutionSettings?.ToolBehavior); + Assert.NotNull(service3ExecutionSettings); + + var service3NoneFunctionChoiceBehavior = service3ExecutionSettings?.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; + Assert.NotNull(service3NoneFunctionChoiceBehavior); } [Fact] @@ -98,13 +94,10 @@ These are AI execution settings "service1" : { "model_id": "gpt4", "temperature": 0.7, - "tool_behavior": { - "type": "function_call_behavior", - "choice": { - "type": "auto", - "allowAnyRequestedKernelFunction" : true, - "functions": ["p1.f1"] - } + "function_choice_behavior": { + "type": "auto", + "allowAnyRequestedKernelFunction" : true, + "functions": ["p1.f1"] } } } @@ -115,12 +108,9 @@ These are more AI execution settings "service2" : { "model_id": "gpt3.5", "temperature": 0.8, - "tool_behavior": { - "type": "function_call_behavior", - "choice": { - "type": "required", - "functions": ["p1.f1"] - } + "function_choice_behavior": { + "type": "required", + "functions": ["p1.f1"] } } } @@ -131,11 +121,8 @@ These are AI execution settings as well "service3" : { "model_id": "gpt3.5-turbo", "temperature": 0.8, - "tool_behavior": { - "type": "function_call_behavior", - "choice": { - "type": "none" - } + "function_choice_behavior": { + "type": "none" } } } diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs index 5705da4f0328..19403d1861d7 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs @@ -3,7 +3,6 @@ using System; using System.Linq; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI.ToolBehaviors; using Microsoft.SemanticKernel.Connectors.OpenAI; using Xunit; @@ -86,36 +85,34 @@ public void ItShouldSupportCreatingOpenAIExecutionSettings() } [Fact] - public void ItShouldDeserializeFunctionCallChoices() + public void ItShouldDeserializeFunctionChoiceBehaviors() { // Act var promptTemplateConfig = KernelFunctionYaml.ToPromptTemplateConfig(this._yaml); // Assert Assert.NotNull(promptTemplateConfig?.ExecutionSettings); - Assert.Equal(2, promptTemplateConfig.ExecutionSettings.Count); + Assert.Equal(3, promptTemplateConfig.ExecutionSettings.Count); - // Service with auto function call choice + // Service with auto function choice behavior var service1ExecutionSettings = promptTemplateConfig.ExecutionSettings["service1"]; - Assert.NotNull(service1ExecutionSettings?.ToolBehavior); - var service1FunctionCallBehavior = service1ExecutionSettings.ToolBehavior as FunctionCallBehavior; - Assert.NotNull(service1FunctionCallBehavior?.Choice); + var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; + Assert.NotNull(autoFunctionChoiceBehavior?.Functions); + Assert.Equal("p1.f1", autoFunctionChoiceBehavior.Functions.Single()); - var autoFunctionCallChoice = service1FunctionCallBehavior.Choice as AutoFunctionCallChoice; - Assert.NotNull(autoFunctionCallChoice?.Functions); - Assert.Equal("p1.f1", autoFunctionCallChoice.Functions.Single()); - - // Service with required function call choice + // Service with required function choice behavior var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; - Assert.NotNull(service2ExecutionSettings?.ToolBehavior); - var service2FunctionCallBehavior = service2ExecutionSettings.ToolBehavior as FunctionCallBehavior; - Assert.NotNull(service2FunctionCallBehavior?.Choice); + var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; + Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); + Assert.Equal("p2.f2", requiredFunctionChoiceBehavior.Functions.Single()); + + // Service with none function choice behavior + var service3ExecutionSettings = promptTemplateConfig.ExecutionSettings["service3"]; - var requiredFunctionCallChoice = service2FunctionCallBehavior.Choice as RequiredFunctionCallChoice; - Assert.NotNull(requiredFunctionCallChoice?.Functions); - Assert.Equal("p2.f2", requiredFunctionCallChoice.Functions.Single()); + var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; + Assert.NotNull(noneFunctionChoiceBehavior); } [Fact] @@ -193,11 +190,10 @@ string CreateYaml(object defaultValue) frequency_penalty: 0.0 max_tokens: 256 stop_sequences: [] - tool_behavior: - !function_call_behavior - choice: !auto - functions: - - p1.f1 + function_choice_behavior: + !auto + functions: + - p1.f1 service2: model_id: gpt-3.5 temperature: 1.0 @@ -206,11 +202,20 @@ string CreateYaml(object defaultValue) frequency_penalty: 0.0 max_tokens: 256 stop_sequences: [ "foo", "bar", "baz" ] - tool_behavior: - !function_call_behavior - choice: !required - functions: - - p2.f2 + function_choice_behavior: + !required + functions: + - p2.f2 + service3: + model_id: gpt-3.5 + temperature: 1.0 + top_p: 0.0 + presence_penalty: 0.0 + frequency_penalty: 0.0 + max_tokens: 256 + stop_sequences: [ "foo", "bar", "baz" ] + function_choice_behavior: + !none """; private readonly string _yamlWithCustomSettings = """ diff --git a/dotnet/src/Functions/Functions.Yaml/FunctionCallBehaviorTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/FunctionCallBehaviorTypeConverter.cs deleted file mode 100644 index 4e32454dbe06..000000000000 --- a/dotnet/src/Functions/Functions.Yaml/FunctionCallBehaviorTypeConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.SemanticKernel.AI.ToolBehaviors; -using YamlDotNet.Core; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace Microsoft.SemanticKernel; - -internal sealed class FunctionCallBehaviorTypeConverter : IYamlTypeConverter -{ - private static IDeserializer? s_deserializer; - - public bool Accepts(Type type) - { - return typeof(FunctionCallBehavior) == type; - } - - public object? ReadYaml(IParser parser, Type type) - { - s_deserializer ??= new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithTagMapping("!function_call_behavior", typeof(FunctionCallBehavior)) - .WithTagMapping("!auto", typeof(AutoFunctionCallChoice)) - .WithTagMapping("!required", typeof(RequiredFunctionCallChoice)) - .Build(); - - return s_deserializer.Deserialize(parser, type); - } - - public void WriteYaml(IEmitter emitter, object? value, Type type) - { - throw new NotImplementedException(); - } -} diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs index ca1a2e892b43..38172ba0e4da 100644 --- a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using Microsoft.SemanticKernel.AI.ToolBehaviors; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; @@ -23,8 +22,9 @@ public bool Accepts(Type type) { s_deserializer ??= new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithTypeConverter(new FunctionCallBehaviorTypeConverter()) - .WithTagMapping("!function_call_behavior", typeof(FunctionCallBehavior)) + .WithTagMapping("!auto", typeof(AutoFunctionChoiceBehavior)) + .WithTagMapping("!required", typeof(RequiredFunctionChoiceBehavior)) + .WithTagMapping("!none", typeof(NoneFunctionChoiceBehavior)) .Build(); parser.MoveNext(); // Move to the first property @@ -38,8 +38,8 @@ public bool Accepts(Type type) case "model_id": executionSettings.ModelId = s_deserializer.Deserialize(parser); break; - case "tool_behavior": - executionSettings.ToolBehavior = s_deserializer.Deserialize(parser); + case "function_choice_behavior": + executionSettings.FunctionChoiceBehavior = s_deserializer.Deserialize(parser); break; default: (executionSettings.ExtensionData ??= new Dictionary()).Add(propertyName, s_deserializer.Deserialize(parser)); diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs index 0c62ace921f9..fb1129ed28f3 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs @@ -9,8 +9,8 @@ using System.Threading.Tasks; using Azure.AI.OpenAI; using Microsoft.Extensions.Configuration; + using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI.ToolBehaviors; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using SemanticKernel.IntegrationTests.Planners.Stepwise; @@ -40,7 +40,7 @@ void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e) #pragma warning restore CS0618 // Events are deprecated // Act - OpenAIPromptExecutionSettings settings = new() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; + OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; var result = await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.", new(settings)); // Assert @@ -67,7 +67,7 @@ void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e) #pragma warning restore CS0618 // Events are deprecated // Act - OpenAIPromptExecutionSettings settings = new() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; + OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; string result = ""; await foreach (string c in kernel.InvokePromptStreamingAsync( $"How much older is John than Jim? Compute that value and pass it to the {nameof(TimeInformation)}.{nameof(TimeInformation.InterpretValue)} function, then respond only with its result.", @@ -91,7 +91,7 @@ public async Task CanAutoInvokeKernelFunctionsWithComplexTypeParametersAsync() kernel.ImportPluginFromType(); // Act - OpenAIPromptExecutionSettings settings = new() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; + OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; var result = await kernel.InvokePromptAsync("What is the current temperature in Dublin, Ireland, in Fahrenheit?", new(settings)); // Assert @@ -107,7 +107,7 @@ public async Task CanAutoInvokeKernelFunctionsWithPrimitiveTypeParametersAsync() kernel.ImportPluginFromType(); // Act - OpenAIPromptExecutionSettings settings = new() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; + OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; var result = await kernel.InvokePromptAsync("Convert 50 degrees Fahrenheit to Celsius.", new(settings)); @@ -134,7 +134,7 @@ public async Task CanAutoInvokeKernelFunctionFromPromptAsync() [promptFunction])); // Act - OpenAIPromptExecutionSettings settings = new() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; + OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; var result = await kernel.InvokePromptAsync("Show me the latest news as they are.", new(settings)); // Assert @@ -159,7 +159,7 @@ public async Task CanAutoInvokeKernelFunctionFromPromptStreamingAsync() [promptFunction])); // Act - OpenAIPromptExecutionSettings settings = new() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; + OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; var streamingResult = kernel.InvokePromptStreamingAsync("Show me the latest news as they are.", new(settings)); var builder = new StringBuilder(); @@ -185,7 +185,7 @@ public async Task ConnectorSpecificChatMessageContentClassesCanBeUsedForManualFu var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false) }; + var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; var sut = kernel.GetRequiredService(); @@ -232,7 +232,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManual var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false) }; + var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; var sut = kernel.GetRequiredService(); @@ -273,7 +273,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc chatHistory.AddSystemMessage("If you are unable to answer the question for whatever reason, please add the 'error' keyword to the response."); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false) }; + var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; var completionService = kernel.GetRequiredService(); @@ -317,7 +317,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu chatHistory.AddSystemMessage("if there's a tornado warning, please add the 'tornado' keyword to the response."); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false) }; + var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; var completionService = kernel.GetRequiredService(); @@ -365,7 +365,7 @@ public async Task ItFailsIfNoFunctionResultProvidedAsync() var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false) }; + var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; var completionService = kernel.GetRequiredService(); @@ -389,7 +389,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFu var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice() }; + var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; var sut = kernel.GetRequiredService(); @@ -455,7 +455,7 @@ public async Task SubsetOfFunctionsCanBeUsedForFunctionCallingAsync() var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("What day is today?"); - var settings = new OpenAIPromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: true) }; + var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: true) }; var sut = kernel.GetRequiredService(); @@ -479,7 +479,7 @@ public async Task RequiredFunctionShouldBeCalledAsync() var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("What day is today?"); - var settings = new PromptExecutionSettings() { ToolBehavior = FunctionCallBehavior.RequiredFunctionChoice([function], true) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([function], true) }; var sut = kernel.GetRequiredService(); diff --git a/dotnet/src/InternalUtilities/src/PromptSerialization/FunctionChoiceBehaviorResolver.cs b/dotnet/src/InternalUtilities/src/PromptSerialization/FunctionChoiceBehaviorResolver.cs new file mode 100644 index 000000000000..6b1c5e0805bb --- /dev/null +++ b/dotnet/src/InternalUtilities/src/PromptSerialization/FunctionChoiceBehaviorResolver.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace Microsoft.SemanticKernel; + +[ExcludeFromCodeCoverage] +internal sealed class FunctionChoiceBehaviorResolver : DefaultJsonTypeInfoResolver +{ + public static FunctionChoiceBehaviorResolver Instance { get; } = new FunctionChoiceBehaviorResolver(); + + public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + var jsonTypeInfo = base.GetTypeInfo(type, options); + + if (jsonTypeInfo.Type == typeof(FunctionChoiceBehavior)) + { + jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions(); + + jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(RequiredFunctionChoiceBehavior), "required")); + jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(AutoFunctionChoiceBehavior), "auto")); + jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(NoneFunctionChoiceBehavior), "none")); + + jsonTypeInfo.PolymorphismOptions.TypeDiscriminatorPropertyName = "type"; + + return jsonTypeInfo; + } + + return jsonTypeInfo; + } +} diff --git a/dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorResolver.cs b/dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorResolver.cs deleted file mode 100644 index 75d46ee69c6b..000000000000 --- a/dotnet/src/InternalUtilities/src/PromptSerialization/ToolBehaviorResolver.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; -using Microsoft.SemanticKernel.AI.ToolBehaviors; - -namespace Microsoft.SemanticKernel; - -[ExcludeFromCodeCoverage] -internal sealed class ToolBehaviorResolver : DefaultJsonTypeInfoResolver -{ - public static ToolBehaviorResolver Instance { get; } = new ToolBehaviorResolver(); - - public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) - { - var jsonTypeInfo = base.GetTypeInfo(type, options); - - if (jsonTypeInfo.Type == typeof(ToolBehavior)) - { - jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions(); - - jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(FunctionCallBehavior), "function_call_behavior")); - - jsonTypeInfo.PolymorphismOptions.TypeDiscriminatorPropertyName = "type"; - - return jsonTypeInfo; - } - - if (jsonTypeInfo.Type == typeof(FunctionCallChoice)) - { - jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions(); - - jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(RequiredFunctionCallChoice), "required")); - jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(AutoFunctionCallChoice), "auto")); - jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(NoneFunctionCallChoice), "none")); - - jsonTypeInfo.PolymorphismOptions.TypeDiscriminatorPropertyName = "type"; - - return jsonTypeInfo; - } - - return jsonTypeInfo; - } -} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/AutoFunctionCallChoice.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs similarity index 80% rename from dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/AutoFunctionCallChoice.cs rename to dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index 7c0bec197a8d..5b12577521f1 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/AutoFunctionCallChoice.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -5,22 +5,20 @@ using System.Linq; using System.Text.Json.Serialization; -namespace Microsoft.SemanticKernel.AI.ToolBehaviors; +namespace Microsoft.SemanticKernel; -public sealed class AutoFunctionCallChoice : FunctionCallChoice +public sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior { internal const int DefaultMaximumAutoInvokeAttempts = 5; [JsonConstructor] - public AutoFunctionCallChoice() + public AutoFunctionChoiceBehavior() { } - public AutoFunctionCallChoice(IEnumerable functions) + public AutoFunctionChoiceBehavior(IEnumerable functions) { this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)); - - this.AllowAnyRequestedKernelFunction = !functions.Any(); } [JsonPropertyName("maximumAutoInvokeAttempts")] @@ -29,10 +27,7 @@ public AutoFunctionCallChoice(IEnumerable functions) [JsonPropertyName("functions")] public IEnumerable? Functions { get; init; } - [JsonPropertyName("allowAnyRequestedKernelFunction")] - public bool AllowAnyRequestedKernelFunction { get; init; } - - public override FunctionCallChoiceConfiguration Configure(FunctionCallChoiceContext context) + public override FunctionChoiceBehaviorConfiguration Configure(FunctionChoiceBehaviorContext context) { bool autoInvoke = this.MaximumAutoInvokeAttempts > 0; @@ -77,11 +72,10 @@ public override FunctionCallChoiceConfiguration Configure(FunctionCallChoiceCont } } - return new FunctionCallChoiceConfiguration() + return new FunctionChoiceBehaviorConfiguration() { AvailableFunctions = availableFunctions, - MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts, - AllowAnyRequestedKernelFunction = this.AllowAnyRequestedKernelFunction + MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts }; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs new file mode 100644 index 000000000000..af3364f0e88b --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel; + +public abstract class FunctionChoiceBehavior +{ + protected const string FunctionNameSeparator = "."; + + public static FunctionChoiceBehavior AutoFunctionChoice(IEnumerable? enabledFunctions = null, bool autoInvoke = true) + { + return new AutoFunctionChoiceBehavior(enabledFunctions ?? []) + { + MaximumAutoInvokeAttempts = autoInvoke ? AutoFunctionChoiceBehavior.DefaultMaximumAutoInvokeAttempts : 0 + }; + } + + public static FunctionChoiceBehavior RequiredFunctionChoice(IEnumerable functions, bool autoInvoke = true) + { + return new RequiredFunctionChoiceBehavior(functions) + { + MaximumAutoInvokeAttempts = autoInvoke ? AutoFunctionChoiceBehavior.DefaultMaximumAutoInvokeAttempts : 0 + }; + } + + public static FunctionChoiceBehavior None { get; } = new NoneFunctionChoiceBehavior(); + + public abstract FunctionChoiceBehaviorConfiguration Configure(FunctionChoiceBehaviorContext context); +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs similarity index 69% rename from dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceConfiguration.cs rename to dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index d29f53235214..2e56036a7595 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -2,16 +2,14 @@ using System.Collections.Generic; -namespace Microsoft.SemanticKernel.AI.ToolBehaviors; +namespace Microsoft.SemanticKernel; -public class FunctionCallChoiceConfiguration +public class FunctionChoiceBehaviorConfiguration { public IEnumerable? AvailableFunctions { get; init; } public IEnumerable? RequiredFunctions { get; init; } - public bool? AllowAnyRequestedKernelFunction { get; init; } - public int? MaximumAutoInvokeAttempts { get; init; } public int? MaximumUseAttempts { get; init; } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceContext.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs similarity index 51% rename from dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceContext.cs rename to dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs index b38d63cac410..0a3f20da2ced 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoiceContext.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. -namespace Microsoft.SemanticKernel.AI.ToolBehaviors; +namespace Microsoft.SemanticKernel; -public class FunctionCallChoiceContext +public class FunctionChoiceBehaviorContext { public Kernel? Kernel { get; init; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs new file mode 100644 index 000000000000..4f58a8bf40d5 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel; + +public sealed class NoneFunctionChoiceBehavior : FunctionChoiceBehavior +{ + public override FunctionChoiceBehaviorConfiguration Configure(FunctionChoiceBehaviorContext context) + { + return new FunctionChoiceBehaviorConfiguration() + { + MaximumAutoInvokeAttempts = 0, + MaximumUseAttempts = 0 + }; + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/RequiredFunctionCallChoice.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs similarity index 84% rename from dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/RequiredFunctionCallChoice.cs rename to dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index 17c1c1e1c325..864d04536458 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/RequiredFunctionCallChoice.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -5,20 +5,20 @@ using System.Linq; using System.Text.Json.Serialization; -namespace Microsoft.SemanticKernel.AI.ToolBehaviors; +namespace Microsoft.SemanticKernel; -public sealed class RequiredFunctionCallChoice : FunctionCallChoice +public sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior { internal const int DefaultMaximumAutoInvokeAttempts = 5; internal const int DefaultMaximumUseAttempts = 1; [JsonConstructor] - public RequiredFunctionCallChoice() + public RequiredFunctionChoiceBehavior() { } - public RequiredFunctionCallChoice(IEnumerable functions) + public RequiredFunctionChoiceBehavior(IEnumerable functions) { this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)); } @@ -33,7 +33,7 @@ public RequiredFunctionCallChoice(IEnumerable functions) [JsonPropertyName("maximumUseAttempts")] public int MaximumUseAttempts { get; init; } = DefaultMaximumUseAttempts; - public override FunctionCallChoiceConfiguration Configure(FunctionCallChoiceContext context) + public override FunctionChoiceBehaviorConfiguration Configure(FunctionChoiceBehaviorContext context) { List? requiredFunctions = null; @@ -70,12 +70,11 @@ public override FunctionCallChoiceConfiguration Configure(FunctionCallChoiceCont } } - return new FunctionCallChoiceConfiguration() + return new FunctionChoiceBehaviorConfiguration() { RequiredFunctions = requiredFunctions, MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts, - MaximumUseAttempts = this.MaximumUseAttempts, - AllowAnyRequestedKernelFunction = false + MaximumUseAttempts = this.MaximumUseAttempts }; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs index 50d035381068..09d423e1b82d 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text.Json.Serialization; -using Microsoft.SemanticKernel.AI.ToolBehaviors; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.TextGeneration; @@ -44,15 +43,15 @@ public string? ModelId } } - [JsonPropertyName("tool_behavior")] - public ToolBehavior? ToolBehavior + [JsonPropertyName("function_choice_behavior")] + public FunctionChoiceBehavior? FunctionChoiceBehavior { - get => this._toolBehavior; + get => this._functionChoiceBehavior; set { this.ThrowIfFrozen(); - this._toolBehavior = value; + this._functionChoiceBehavior = value; } } @@ -106,7 +105,7 @@ public virtual PromptExecutionSettings Clone() { ModelId = this.ModelId, ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, - ToolBehavior = this.ToolBehavior + FunctionChoiceBehavior = this.FunctionChoiceBehavior }; } @@ -126,7 +125,7 @@ protected void ThrowIfFrozen() private string? _modelId; private IDictionary? _extensionData; - private ToolBehavior? _toolBehavior; + private FunctionChoiceBehavior? _functionChoiceBehavior; #endregion } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoice.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoice.cs deleted file mode 100644 index 23b55ba8f51c..000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/FunctionCallChoice.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.SemanticKernel.AI.ToolBehaviors; - -public abstract class FunctionCallChoice -{ - protected const string FunctionNameSeparator = "."; - - public abstract FunctionCallChoiceConfiguration Configure(FunctionCallChoiceContext context); -} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/NoneFunctionCallChoice.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/NoneFunctionCallChoice.cs deleted file mode 100644 index 2c9c6f91dde1..000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/Choices/NoneFunctionCallChoice.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.SemanticKernel.AI.ToolBehaviors; - -public sealed class NoneFunctionCallChoice : FunctionCallChoice -{ - public override FunctionCallChoiceConfiguration Configure(FunctionCallChoiceContext context) - { - return new FunctionCallChoiceConfiguration() - { - MaximumAutoInvokeAttempts = 0, - MaximumUseAttempts = 0, - AllowAnyRequestedKernelFunction = false - }; - } -} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/FunctionCallBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/FunctionCallBehavior.cs deleted file mode 100644 index d83e43f69c9a..000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/FunctionCallBehavior.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.AI.ToolBehaviors; - -public class FunctionCallBehavior : ToolBehavior -{ - public static FunctionCallBehavior AutoFunctionChoice(IEnumerable? enabledFunctions = null, bool autoInvoke = true) - { - return new FunctionCallBehavior - { - Choice = new AutoFunctionCallChoice(enabledFunctions ?? []) - { - MaximumAutoInvokeAttempts = autoInvoke ? AutoFunctionCallChoice.DefaultMaximumAutoInvokeAttempts : 0 - } - }; - } - - public static FunctionCallBehavior RequiredFunctionChoice(IEnumerable functions, bool autoInvoke = true) - { - return new FunctionCallBehavior - { - Choice = new RequiredFunctionCallChoice(functions) - { - MaximumAutoInvokeAttempts = autoInvoke ? AutoFunctionCallChoice.DefaultMaximumAutoInvokeAttempts : 0 - } - }; - } - - public static FunctionCallBehavior None { get; } = new FunctionCallBehavior() { Choice = new NoneFunctionCallChoice() }; - - [JsonPropertyName("choice")] - public FunctionCallChoice Choice { get; init; } = new NoneFunctionCallChoice(); -} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/ToolBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/ToolBehavior.cs deleted file mode 100644 index fe403daf6db6..000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ToolBehaviors/ToolBehavior.cs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.SemanticKernel.AI.ToolBehaviors; - -public abstract class ToolBehavior -{ -} diff --git a/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs b/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs index af1424227538..94ba780b7c4a 100644 --- a/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs +++ b/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs @@ -25,7 +25,7 @@ namespace Microsoft.SemanticKernel; /// public sealed class PromptTemplateConfig { - private readonly static JsonSerializerOptions s_serializerOptions = new(JsonOptionsCache.ReadPermissive) { TypeInfoResolver = ToolBehaviorResolver.Instance }; + private readonly static JsonSerializerOptions s_serializerOptions = new(JsonOptionsCache.ReadPermissive) { TypeInfoResolver = FunctionChoiceBehaviorResolver.Instance }; /// The format of the prompt template. private string? _templateFormat; diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs similarity index 73% rename from dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallBehaviorTests.cs rename to dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs index cb4457838c5f..287ec2aa9abb 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs @@ -5,27 +5,26 @@ using System.Text.Json; using Azure.AI.OpenAI; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI.ToolBehaviors; using Xunit; namespace SemanticKernel.UnitTests.Functions; /// -/// Unit tests for +/// Unit tests for /// -public sealed class FunctionCallBehaviorTests +public sealed class FunctionCallChoiceTests { - private readonly static JsonSerializerOptions s_serializerOptions = new() { TypeInfoResolver = new ToolBehaviorResolver() }; + private readonly static JsonSerializerOptions s_serializerOptions = new() { TypeInfoResolver = new FunctionChoiceBehaviorResolver() }; [Fact] public void EnableKernelFunctionsAreNotAutoInvoked() { // Arrange var kernel = new Kernel(); - var behavior = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false); + var behavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); // Act - var config = behavior.Choice.Configure(new() { Kernel = kernel }); + var config = behavior.Configure(new() { Kernel = kernel }); // Assert Assert.NotNull(config); @@ -37,10 +36,10 @@ public void AutoInvokeKernelFunctionsShouldSpecifyNumberOfAutoInvokeAttempts() { // Arrange var kernel = new Kernel(); - var behavior = FunctionCallBehavior.AutoFunctionChoice(); + var behavior = FunctionChoiceBehavior.AutoFunctionChoice(); // Act - var config = behavior.Choice.Configure(new() { Kernel = kernel }); + var config = behavior.Configure(new() { Kernel = kernel }); // Assert Assert.NotNull(config); @@ -51,10 +50,10 @@ public void AutoInvokeKernelFunctionsShouldSpecifyNumberOfAutoInvokeAttempts() public void KernelFunctionsConfigureWithNullKernelDoesNotAddTools() { // Arrange - var kernelFunctions = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false); + var behavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); // Act - var config = kernelFunctions.Choice.Configure(new() { }); + var config = behavior.Configure(new() { }); // Assert Assert.Null(config.AvailableFunctions); @@ -65,12 +64,12 @@ public void KernelFunctionsConfigureWithNullKernelDoesNotAddTools() public void KernelFunctionsConfigureWithoutFunctionsDoesNotAddTools() { // Arrange - var kernelFunctions = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false); + var behavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); var kernel = Kernel.CreateBuilder().Build(); // Act - var config = kernelFunctions.Choice.Configure(new() { Kernel = kernel }); + var config = behavior.Configure(new() { Kernel = kernel }); // Assert Assert.Null(config.AvailableFunctions); @@ -81,7 +80,7 @@ public void KernelFunctionsConfigureWithoutFunctionsDoesNotAddTools() public void KernelFunctionsConfigureWithFunctionsAddsTools() { // Arrange - var kernelFunctions = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false); + var behavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); var kernel = Kernel.CreateBuilder().Build(); var plugin = this.GetTestPlugin(); @@ -89,7 +88,7 @@ public void KernelFunctionsConfigureWithFunctionsAddsTools() kernel.Plugins.Add(plugin); // Act - var config = kernelFunctions.Choice.Configure(new() { Kernel = kernel }); + var config = behavior.Configure(new() { Kernel = kernel }); // Assert Assert.Null(config.RequiredFunctions); @@ -101,11 +100,11 @@ public void KernelFunctionsConfigureWithFunctionsAddsTools() public void EnabledFunctionsConfigureWithoutFunctionsDoesNotAddTools() { // Arrange - var enabledFunctions = FunctionCallBehavior.AutoFunctionChoice(autoInvoke: false); + var behavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); var chatCompletionsOptions = new ChatCompletionsOptions(); // Act - var config = enabledFunctions.Choice.Configure(new() { }); + var config = behavior.Configure(new() { }); // Assert Assert.Null(chatCompletionsOptions.ToolChoice); @@ -119,10 +118,10 @@ public void EnabledFunctionsConfigureWithAutoInvokeAndNullKernelThrowsException( var kernel = new Kernel(); var function = this.GetTestPlugin().Single(); - var enabledFunctions = FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: true); + var behavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: true); // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.Choice.Configure(new() { })); ; + var exception = Assert.Throws(() => behavior.Configure(new() { })); ; Assert.Equal("Auto-invocation in Auto mode is not supported when no kernel is provided.", exception.Message); } @@ -131,11 +130,11 @@ public void EnabledFunctionsConfigureWithAutoInvokeAndEmptyKernelThrowsException { // Arrange var function = this.GetTestPlugin().Single(); - var enabledFunctions = FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: true); + var behavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: true); var kernel = Kernel.CreateBuilder().Build(); // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.Choice.Configure(new() { Kernel = kernel })); + var exception = Assert.Throws(() => behavior.Configure(new() { Kernel = kernel })); Assert.Equal("The specified function MyPlugin.MyFunction is not available in the kernel.", exception.Message); } @@ -147,13 +146,13 @@ public void EnabledFunctionsConfigureWithKernelAndPluginsAddsTools(bool autoInvo // Arrange var plugin = this.GetTestPlugin(); var function = plugin.Single(); - var enabledFunctions = FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: autoInvoke); + var behavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: autoInvoke); var kernel = Kernel.CreateBuilder().Build(); kernel.Plugins.Add(plugin); // Act - var config = enabledFunctions.Choice.Configure(new() { Kernel = kernel }); + var config = behavior.Configure(new() { Kernel = kernel }); // Assert this.AssertFunctions(config.AvailableFunctions); @@ -166,10 +165,10 @@ public void RequiredFunctionsConfigureWithAutoInvokeAndNullKernelThrowsException var kernel = new Kernel(); var function = this.GetTestPlugin().Single(); - var requiredFunction = FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: true); + var behavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: true); // Act & Assert - var exception = Assert.Throws(() => requiredFunction.Choice.Configure(new() { })); + var exception = Assert.Throws(() => behavior.Configure(new() { })); Assert.Equal("Auto-invocation in Auto mode is not supported when no kernel is provided.", exception.Message); } @@ -178,11 +177,11 @@ public void RequiredFunctionsConfigureWithAutoInvokeAndEmptyKernelThrowsExceptio { // Arrange var function = this.GetTestPlugin().Single(); - var requiredFunction = FunctionCallBehavior.AutoFunctionChoice([function], autoInvoke: true); + var behavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: true); var kernel = Kernel.CreateBuilder().Build(); // Act & Assert - var exception = Assert.Throws(() => requiredFunction.Choice.Configure(new() { Kernel = kernel })); + var exception = Assert.Throws(() => behavior.Configure(new() { Kernel = kernel })); Assert.Equal("The specified function MyPlugin.MyFunction is not available in the kernel.", exception.Message); } @@ -192,12 +191,12 @@ public void RequiredFunctionConfigureAddsTools() // Arrange var plugin = this.GetTestPlugin(); var function = plugin.Single(); - var requiredFunction = FunctionCallBehavior.RequiredFunctionChoice([function], autoInvoke: true); + var behavior = FunctionChoiceBehavior.RequiredFunctionChoice([function], autoInvoke: true); var kernel = new Kernel(); kernel.Plugins.Add(plugin); // Act - var config = requiredFunction.Choice.Configure(new() { Kernel = kernel }); + var config = behavior.Configure(new() { Kernel = kernel }); // Assert this.AssertFunctions(config.RequiredFunctions); @@ -220,11 +219,10 @@ public void ItShouldBePossibleToDeserializeAutoFunctionCallChoice() """; // Act - var deserializedFunction = JsonSerializer.Deserialize(json, s_serializerOptions) as AutoFunctionCallChoice; + var deserializedFunction = JsonSerializer.Deserialize(json, s_serializerOptions) as AutoFunctionChoiceBehavior; // Assert Assert.NotNull(deserializedFunction); - Assert.True(deserializedFunction.AllowAnyRequestedKernelFunction); Assert.Equal(12, deserializedFunction.MaximumAutoInvokeAttempts); Assert.NotNull(deserializedFunction.Functions); Assert.Single(deserializedFunction.Functions); @@ -248,7 +246,7 @@ public void ItShouldBePossibleToDeserializeForcedFunctionCallChoice() """; // Act - var deserializedFunction = JsonSerializer.Deserialize(json, s_serializerOptions) as RequiredFunctionCallChoice; + var deserializedFunction = JsonSerializer.Deserialize(json, s_serializerOptions) as RequiredFunctionChoiceBehavior; // Assert Assert.NotNull(deserializedFunction); @@ -271,7 +269,7 @@ public void ItShouldBePossibleToDeserializeNoneFunctionCallBehavior() """; // Act - var deserializedFunction = JsonSerializer.Deserialize(json, s_serializerOptions) as NoneFunctionCallChoice; + var deserializedFunction = JsonSerializer.Deserialize(json, s_serializerOptions) as NoneFunctionChoiceBehavior; // Assert Assert.NotNull(deserializedFunction); diff --git a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs index 8909091bd025..3fdff84de3ef 100644 --- a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text.Json; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI.ToolBehaviors; using Microsoft.SemanticKernel.Connectors.OpenAI; using Xunit; @@ -153,14 +152,11 @@ public void DeserializingAutoFunctionCallingChoice() "execution_settings": { "default": { "model_id": "gpt-4", - "tool_behavior": { - "type": "function_call_behavior", - "choice":{ - "type": "auto", - "allowAnyRequestedKernelFunction" : true, - "maximumAutoInvokeAttempts": 12, - "functions":["p1.f1"] - } + "function_choice_behavior": { + "type": "auto", + "allowAnyRequestedKernelFunction" : true, + "maximumAutoInvokeAttempts": 12, + "functions":["p1.f1"] } } } @@ -175,16 +171,10 @@ public void DeserializingAutoFunctionCallingChoice() Assert.Single(promptTemplateConfig.ExecutionSettings); var executionSettings = promptTemplateConfig.ExecutionSettings.Single(); - Assert.NotNull(executionSettings.Value.ToolBehavior); - - var functionCallBehavior = executionSettings.Value.ToolBehavior as FunctionCallBehavior; - Assert.NotNull(functionCallBehavior); - var autoFunctionCallChoice = functionCallBehavior.Choice as AutoFunctionCallChoice; + var autoFunctionCallChoice = executionSettings.Value.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; Assert.NotNull(autoFunctionCallChoice?.Functions); Assert.Equal("p1.f1", autoFunctionCallChoice.Functions.Single()); - - Assert.True(autoFunctionCallChoice.AllowAnyRequestedKernelFunction); } [Fact] @@ -197,15 +187,10 @@ public void DeserializingRequiredFunctionCallingChoice() "execution_settings": { "default": { "model_id": "gpt-4", - "tool_behavior": { - "type": "function_call_behavior", - "choice":{ - "type": "required", - "maximumAutoInvokeAttempts": 11, - "functions":[ - "p1.f1" - ] - } + "function_choice_behavior": { + "type": "required", + "maximumAutoInvokeAttempts": 11, + "functions":["p1.f1"] } } } @@ -220,12 +205,8 @@ public void DeserializingRequiredFunctionCallingChoice() Assert.Single(promptTemplateConfig.ExecutionSettings); var executionSettings = promptTemplateConfig.ExecutionSettings.Single(); - Assert.NotNull(executionSettings.Value.ToolBehavior); - - var functionCallBehavior = executionSettings.Value.ToolBehavior as FunctionCallBehavior; - Assert.NotNull(functionCallBehavior); - var requiredFunctionCallChoice = functionCallBehavior.Choice as RequiredFunctionCallChoice; + var requiredFunctionCallChoice = executionSettings.Value.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; Assert.NotNull(requiredFunctionCallChoice?.Functions); Assert.Equal("p1.f1", requiredFunctionCallChoice.Functions.Single()); @@ -243,11 +224,8 @@ public void DeserializingNoneFunctionCallingChoice() "execution_settings": { "default": { "model_id": "gpt-4", - "tool_behavior": { - "type": "function_call_behavior", - "choice":{ - "type": "none" - } + "function_choice_behavior": { + "type": "none", } } } @@ -262,11 +240,8 @@ public void DeserializingNoneFunctionCallingChoice() Assert.Single(promptTemplateConfig.ExecutionSettings); var executionSettings = promptTemplateConfig.ExecutionSettings.Single(); - Assert.NotNull(executionSettings.Value.ToolBehavior); - var functionCallBehavior = executionSettings.Value.ToolBehavior as FunctionCallBehavior; - Assert.NotNull(functionCallBehavior); - Assert.IsType(functionCallBehavior.Choice); + Assert.IsType(executionSettings.Value.FunctionChoiceBehavior); } [Fact] From 318879571c18cdcdef0008452262924bd8cfa4ad Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 8 May 2024 12:02:47 +0100 Subject: [PATCH 50/90] cleanup --- .../Markdown/Functions/KernelFunctionMarkdownTests.cs | 1 - .../Functions/FunctionCallChoiceTests.cs | 1 - .../PromptTemplate/PromptTemplateConfigTests.cs | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs index 0753083f0612..10396cc6219a 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs @@ -96,7 +96,6 @@ These are AI execution settings "temperature": 0.7, "function_choice_behavior": { "type": "auto", - "allowAnyRequestedKernelFunction" : true, "functions": ["p1.f1"] } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs index 287ec2aa9abb..ff09c4a9debc 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs @@ -210,7 +210,6 @@ public void ItShouldBePossibleToDeserializeAutoFunctionCallChoice() """ { "type":"auto", - "allowAnyRequestedKernelFunction":true, "maximumAutoInvokeAttempts":12, "functions":[ "MyPlugin.MyFunction" diff --git a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs index 3fdff84de3ef..358c9af9388e 100644 --- a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs @@ -154,7 +154,6 @@ public void DeserializingAutoFunctionCallingChoice() "model_id": "gpt-4", "function_choice_behavior": { "type": "auto", - "allowAnyRequestedKernelFunction" : true, "maximumAutoInvokeAttempts": 12, "functions":["p1.f1"] } @@ -174,6 +173,7 @@ public void DeserializingAutoFunctionCallingChoice() var autoFunctionCallChoice = executionSettings.Value.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; Assert.NotNull(autoFunctionCallChoice?.Functions); + Assert.Equal(12, autoFunctionCallChoice.MaximumAutoInvokeAttempts); Assert.Equal("p1.f1", autoFunctionCallChoice.Functions.Single()); } From 47da2868017df3fc2ab29c2a79d4ba9a0f2cf7ca Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 8 May 2024 19:05:34 +0100 Subject: [PATCH 51/90] fix: Use the 'type' discriminator for polymorphic deserialization from YAML. --- .../Yaml/Functions/KernelFunctionYamlTests.cs | 6 +++--- .../PromptExecutionSettingsTypeConverter.cs | 13 ++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs index 19403d1861d7..6d19fa43dbff 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs @@ -191,7 +191,7 @@ string CreateYaml(object defaultValue) max_tokens: 256 stop_sequences: [] function_choice_behavior: - !auto + type: auto functions: - p1.f1 service2: @@ -203,7 +203,7 @@ string CreateYaml(object defaultValue) max_tokens: 256 stop_sequences: [ "foo", "bar", "baz" ] function_choice_behavior: - !required + type: required functions: - p2.f2 service3: @@ -215,7 +215,7 @@ string CreateYaml(object defaultValue) max_tokens: 256 stop_sequences: [ "foo", "bar", "baz" ] function_choice_behavior: - !none + type: none """; private readonly string _yamlWithCustomSettings = """ diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs index 38172ba0e4da..96dd148faf11 100644 --- a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs @@ -22,9 +22,16 @@ public bool Accepts(Type type) { s_deserializer ??= new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) - .WithTagMapping("!auto", typeof(AutoFunctionChoiceBehavior)) - .WithTagMapping("!required", typeof(RequiredFunctionChoiceBehavior)) - .WithTagMapping("!none", typeof(NoneFunctionChoiceBehavior)) + .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used for type discrimination. Otherwise, the "Property '{name}' not found on type '{type.FullName}'" exception is thrown. + .WithTypeDiscriminatingNodeDeserializer((options) => + { + options.AddKeyValueTypeDiscriminator("type", new Dictionary + { + { "auto", typeof(AutoFunctionChoiceBehavior) }, + { "required", typeof(RequiredFunctionChoiceBehavior) }, + { "none", typeof(NoneFunctionChoiceBehavior) } + }); + }) .Build(); parser.MoveNext(); // Move to the first property From 7cd16856ff16ff8ebf52d2bdbcbe36aff06775b6 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 8 May 2024 21:08:29 +0100 Subject: [PATCH 52/90] small improvments --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 26 ++++++++++++------- .../RequiredFunctionChoiceBehavior.cs | 1 - 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 7effa4c85165..1c82f2b8b23c 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -318,7 +318,7 @@ internal async Task> GetChatMessageContentsAsy // Create the Azure SDK ChatCompletionOptions instance from all available information. var chatOptions = CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); - var functionCallConfiguration = this.ConfigureFunctionCallingOptions(kernel, chatExecutionSettings, chatOptions, 0); + var functionCallConfiguration = this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, 0); bool autoInvoke = kernel is not null && functionCallConfiguration?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); @@ -505,7 +505,7 @@ static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory c chatOptions.ToolChoice = ChatCompletionsToolChoice.None; chatOptions.Tools.Clear(); - this.ConfigureFunctionCallingOptions(kernel, chatExecutionSettings, chatOptions, requestIndex); + this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, requestIndex); // 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. @@ -540,7 +540,7 @@ internal async IAsyncEnumerable GetStreamingC var chatOptions = CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); - var functionCallConfiguration = this.ConfigureFunctionCallingOptions(kernel, chatExecutionSettings, chatOptions, 0); + var functionCallConfiguration = this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, 0); bool autoInvoke = kernel is not null && functionCallConfiguration?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); @@ -742,7 +742,7 @@ static void AddResponseMessage( chatOptions.ToolChoice = ChatCompletionsToolChoice.None; chatOptions.Tools.Clear(); - this.ConfigureFunctionCallingOptions(kernel, chatExecutionSettings, chatOptions, requestIndex); + this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, requestIndex); // 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. @@ -1369,7 +1369,15 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context } } - private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts, int? MaximumUseAttempts)? ConfigureFunctionCallingOptions(Kernel? kernel, OpenAIPromptExecutionSettings executionSettings, ChatCompletionsOptions chatOptions, int iteration) + /// + /// Configures the function calling functionality based on the provided parameters. + /// + /// The to be used for function calling. + /// Execution settings for the completion API. + /// The chat completion options from the Azure.AI.OpenAI package. + /// Request sequence index of automatic function invocation process. + /// A tuple containing the AllowAnyRequestedKernelFunction and MaximumAutoInvokeAttempts settings. + private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(Kernel? kernel, OpenAIPromptExecutionSettings executionSettings, ChatCompletionsOptions chatOptions, int requestIndex) { if (executionSettings.FunctionChoiceBehavior is not null && executionSettings.ToolCallBehavior is not null) { @@ -1379,7 +1387,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context // Handling old-style tool call behavior represented by `OpenAIPromptExecutionSettings.ToolCallBehavior` property. if (executionSettings.ToolCallBehavior is { } toolCallBehavior) { - if (iteration >= toolCallBehavior.MaximumUseAttempts) + if (requestIndex >= toolCallBehavior.MaximumUseAttempts) { // Don't add any tools as we've reached the maximum attempts limit. if (this.Logger.IsEnabled(LogLevel.Debug)) @@ -1398,7 +1406,6 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context { AllowAnyRequestedKernelFunction = toolCallBehavior.AllowAnyRequestedKernelFunction, MaximumAutoInvokeAttempts = toolCallBehavior.MaximumAutoInvokeAttempts, - MaximumUseAttempts = toolCallBehavior.MaximumUseAttempts }; } @@ -1413,14 +1420,13 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context return null; } - (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts, int? MaximumUseAttempts) result = new() + (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts) result = new() { AllowAnyRequestedKernelFunction = false, MaximumAutoInvokeAttempts = config.MaximumAutoInvokeAttempts, - MaximumUseAttempts = config.MaximumUseAttempts }; - if (iteration >= config.MaximumUseAttempts) + if (requestIndex >= config.MaximumUseAttempts) { // Don't add any tools as we've reached the maximum attempts limit. if (this.Logger.IsEnabled(LogLevel.Debug)) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index 864d04536458..cbd647f0c703 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -24,7 +24,6 @@ public RequiredFunctionChoiceBehavior(IEnumerable functions) } [JsonPropertyName("functions")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IEnumerable? Functions { get; init; } [JsonPropertyName("maximumAutoInvokeAttempts")] From 6698219f5b2fbd87fc87f9afdcdbfa6c40af60ff Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 9 May 2024 11:42:27 +0100 Subject: [PATCH 53/90] * Add XML comments to classes and their members. * Remove unnecessary type info resolver --- dotnet/SK-dotnet.sln | 20 +++----- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 4 +- .../OpenAIPromptExecutionSettings.cs | 6 +-- .../KernelFunctionMarkdown.cs | 4 +- .../PromptExecutionSettingsTypeConverter.cs | 15 ++++-- .../Connectors/OpenAI/OpenAIFunctionsTests.cs | 1 - .../FunctionChoiceBehaviorResolver.cs | 34 ------------- .../AutoFunctionChoiceBehavior.cs | 35 +++++++++++-- .../FunctionChoiceBehavior.cs | 51 +++++++++++++++++-- .../FunctionChoiceBehaviorConfiguration.cs | 28 +++++++++- .../FunctionChoiceBehaviorContext.cs | 6 +++ .../NoneFunctionChoiceBehavior.cs | 17 +++++-- .../RequiredFunctionChoiceBehavior.cs | 47 ++++++++++++++--- .../AI/PromptExecutionSettings.cs | 27 ++++++++++ .../PromptTemplate/PromptTemplateConfig.cs | 4 +- .../Functions/FunctionCallChoiceTests.cs | 32 ++++++------ 16 files changed, 230 insertions(+), 101 deletions(-) delete mode 100644 dotnet/src/InternalUtilities/src/PromptSerialization/FunctionChoiceBehaviorResolver.cs diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 8764ba1af9ac..53205fecbde4 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -111,11 +111,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Diagnostics", "Diagnostics" src\InternalUtilities\src\Diagnostics\Verify.cs = src\InternalUtilities\src\Diagnostics\Verify.cs EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PromptSerialization", "PromptSerialization", "{6A4C2EAA-E1B4-4F33-A4E6-21ED36413919}" - ProjectSection(SolutionItems) = preProject - src\InternalUtilities\src\PromptSerialization\ToolBehaviorResolver.cs = src\InternalUtilities\src\PromptSerialization\ToolBehaviorResolver.cs - EndProjectSection -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linq", "Linq", "{B00AD427-0047-4850-BEF9-BA8237EA9D8B}" ProjectSection(SolutionItems) = preProject src\InternalUtilities\src\Linq\AsyncEnumerable.cs = src\InternalUtilities\src\Linq\AsyncEnumerable.cs @@ -659,18 +654,18 @@ Global {1D98CF16-5156-40F0-91F0-76294B153DB3}.Publish|Any CPU.Build.0 = Debug|Any CPU {1D98CF16-5156-40F0-91F0-76294B153DB3}.Release|Any CPU.ActiveCfg = Release|Any CPU {1D98CF16-5156-40F0-91F0-76294B153DB3}.Release|Any CPU.Build.0 = Release|Any CPU - {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2}.Publish|Any CPU.ActiveCfg = Debug|Any CPU - {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2}.Publish|Any CPU.Build.0 = Debug|Any CPU - {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2}.Release|Any CPU.Build.0 = Release|Any CPU {87DA81FE-112E-4AF5-BEFB-0B91B993F749}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {87DA81FE-112E-4AF5-BEFB-0B91B993F749}.Debug|Any CPU.Build.0 = Debug|Any CPU {87DA81FE-112E-4AF5-BEFB-0B91B993F749}.Publish|Any CPU.ActiveCfg = Debug|Any CPU {87DA81FE-112E-4AF5-BEFB-0B91B993F749}.Publish|Any CPU.Build.0 = Debug|Any CPU {87DA81FE-112E-4AF5-BEFB-0B91B993F749}.Release|Any CPU.ActiveCfg = Release|Any CPU {87DA81FE-112E-4AF5-BEFB-0B91B993F749}.Release|Any CPU.Build.0 = Release|Any CPU + {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2}.Publish|Any CPU.Build.0 = Debug|Any CPU + {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2}.Release|Any CPU.Build.0 = Release|Any CPU {925B1185-8B58-4E2D-95C9-4CA0BA9364E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {925B1185-8B58-4E2D-95C9-4CA0BA9364E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {925B1185-8B58-4E2D-95C9-4CA0BA9364E5}.Publish|Any CPU.ActiveCfg = Debug|Any CPU @@ -715,7 +710,6 @@ Global {5C246969-D794-4EC3-8E8F-F90D4D166420} = {4D3DAE63-41C6-4E1C-A35A-E77BDFC40675} {958AD708-F048-4FAF-94ED-D2F2B92748B9} = {4D3DAE63-41C6-4E1C-A35A-E77BDFC40675} {29E7D971-1308-4171-9872-E8E4669A1134} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} - {6A4C2EAA-E1B4-4F33-A4E6-21ED36413919} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} {B00AD427-0047-4850-BEF9-BA8237EA9D8B} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} {1C19D805-3573-4477-BF07-40180FCDE1BD} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} {3CDE10B2-AE8F-4FC4-8D55-92D4AD32E144} = {958AD708-F048-4FAF-94ED-D2F2B92748B9} @@ -768,9 +762,9 @@ Global {5C813F83-9FD8-462A-9B38-865CA01C384C} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {D5E4C960-53B3-4C35-99C1-1BA97AECC489} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {1D98CF16-5156-40F0-91F0-76294B153DB3} = {FA3720F1-C99A-49B2-9577-A940257098BF} - {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {87DA81FE-112E-4AF5-BEFB-0B91B993F749} = {FA3720F1-C99A-49B2-9577-A940257098BF} {77E141BA-AF5E-4C01-A970-6C07AC3CD55A} = {4D3DAE63-41C6-4E1C-A35A-E77BDFC40675} + {6EF9663D-976C-4A27-B8D3-8B1E63BA3BF2} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {925B1185-8B58-4E2D-95C9-4CA0BA9364E5} = {FA3720F1-C99A-49B2-9577-A940257098BF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 1c82f2b8b23c..6bb3f20a7ba7 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1414,7 +1414,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context { // Regenerate the tool list as necessary and getting other call behavior properties. The invocation of the function(s) could have augmented // what functions are available in the kernel. - var config = functionChoiceBehavior.Configure(new() { Kernel = kernel }); + var config = functionChoiceBehavior.GetConfiguration(new() { Kernel = kernel }); if (config is null) { return null; @@ -1428,7 +1428,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context if (requestIndex >= config.MaximumUseAttempts) { - // Don't add any tools as we've reached the maximum attempts limit. + // Don't add any tools as we've reached the maximum use attempts limit. if (this.Logger.IsEnabled(LogLevel.Debug)) { this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the functions.", config.MaximumUseAttempts); diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs index 48497f522fa0..b731db727149 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs @@ -18,8 +18,6 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings { - private readonly static JsonSerializerOptions s_serializerOptions = new(JsonOptionsCache.ReadPermissive) { TypeInfoResolver = FunctionChoiceBehaviorResolver.Instance }; - /// /// Temperature controls the randomness of the completion. /// The higher the temperature, the more random the completion. @@ -327,9 +325,9 @@ public static OpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutio return settings; } - var json = JsonSerializer.Serialize(executionSettings, s_serializerOptions); + var json = JsonSerializer.Serialize(executionSettings); - var openAIExecutionSettings = JsonSerializer.Deserialize(json, s_serializerOptions); + var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); if (openAIExecutionSettings is not null) { return openAIExecutionSettings; diff --git a/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs b/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs index 93a288164fd9..9753051aea4e 100644 --- a/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs +++ b/dotnet/src/Functions/Functions.Markdown/KernelFunctionMarkdown.cs @@ -13,8 +13,6 @@ namespace Microsoft.SemanticKernel; /// public static class KernelFunctionMarkdown { - private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() { TypeInfoResolver = FunctionChoiceBehaviorResolver.Instance }; - /// /// Creates a instance for a prompt function using the specified markdown text. /// @@ -58,7 +56,7 @@ internal static PromptTemplateConfig CreateFromPromptMarkdown(string text, strin case "sk.execution_settings": var modelSettings = codeBlock.Lines.ToString(); - var settingsDictionary = JsonSerializer.Deserialize>(modelSettings, s_jsonSerializerOptions); + var settingsDictionary = JsonSerializer.Deserialize>(modelSettings); if (settingsDictionary is not null) { foreach (var keyValue in settingsDictionary) diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs index 96dd148faf11..f7e98b4029c8 100644 --- a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs @@ -9,27 +9,33 @@ namespace Microsoft.SemanticKernel; +/// +/// Allows custom deserialization for from YAML prompts. +/// internal sealed class PromptExecutionSettingsTypeConverter : IYamlTypeConverter { private static IDeserializer? s_deserializer; + /// public bool Accepts(Type type) { return type == typeof(PromptExecutionSettings); } + /// public object? ReadYaml(IParser parser, Type type) { s_deserializer ??= new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) - .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used for type discrimination. Otherwise, the "Property '{name}' not found on type '{type.FullName}'" exception is thrown. + .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination in the WithTypeDiscriminatingNodeDeserializer method below. + // Otherwise, the "Property '{name}' not found on type '{type.FullName}'" exception is thrown. .WithTypeDiscriminatingNodeDeserializer((options) => { options.AddKeyValueTypeDiscriminator("type", new Dictionary { - { "auto", typeof(AutoFunctionChoiceBehavior) }, - { "required", typeof(RequiredFunctionChoiceBehavior) }, - { "none", typeof(NoneFunctionChoiceBehavior) } + { AutoFunctionChoiceBehavior.Alias, typeof(AutoFunctionChoiceBehavior) }, + { RequiredFunctionChoiceBehavior.Alias, typeof(RequiredFunctionChoiceBehavior) }, + { NoneFunctionChoiceBehavior.Alias, typeof(NoneFunctionChoiceBehavior) } }); }) .Build(); @@ -57,6 +63,7 @@ public bool Accepts(Type type) return executionSettings; } + /// public void WriteYaml(IEmitter emitter, object? value, Type type) { throw new NotImplementedException(); diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs index fb1129ed28f3..e6ed9634ba5a 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs @@ -109,7 +109,6 @@ public async Task CanAutoInvokeKernelFunctionsWithPrimitiveTypeParametersAsync() // Act OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; - var result = await kernel.InvokePromptAsync("Convert 50 degrees Fahrenheit to Celsius.", new(settings)); // Assert diff --git a/dotnet/src/InternalUtilities/src/PromptSerialization/FunctionChoiceBehaviorResolver.cs b/dotnet/src/InternalUtilities/src/PromptSerialization/FunctionChoiceBehaviorResolver.cs deleted file mode 100644 index 6b1c5e0805bb..000000000000 --- a/dotnet/src/InternalUtilities/src/PromptSerialization/FunctionChoiceBehaviorResolver.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; - -namespace Microsoft.SemanticKernel; - -[ExcludeFromCodeCoverage] -internal sealed class FunctionChoiceBehaviorResolver : DefaultJsonTypeInfoResolver -{ - public static FunctionChoiceBehaviorResolver Instance { get; } = new FunctionChoiceBehaviorResolver(); - - public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) - { - var jsonTypeInfo = base.GetTypeInfo(type, options); - - if (jsonTypeInfo.Type == typeof(FunctionChoiceBehavior)) - { - jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions(); - - jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(RequiredFunctionChoiceBehavior), "required")); - jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(AutoFunctionChoiceBehavior), "auto")); - jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(typeof(NoneFunctionChoiceBehavior), "none")); - - jsonTypeInfo.PolymorphismOptions.TypeDiscriminatorPropertyName = "type"; - - return jsonTypeInfo; - } - - return jsonTypeInfo; - } -} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index 5b12577521f1..6df855fe3f10 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -7,27 +7,54 @@ namespace Microsoft.SemanticKernel; +/// +/// Represent that provides either all of the 's plugins' function information to the model or a specified subset. +/// This behavior allows the model to decide whether to call the functions and, if so, which ones to call. +/// public sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior { - internal const int DefaultMaximumAutoInvokeAttempts = 5; - + /// + /// The class alias. Used as a value for the discriminator property for polymorphic deserialization + /// of function choice behavior specified in JSON and YAML prompts. + /// + public const string Alias = "auto"; + + /// + /// Initializes a new instance of the class. + /// [JsonConstructor] public AutoFunctionChoiceBehavior() { } + /// + /// Initializes a new instance of the class. + /// + /// The subset of the 's plugins' functions information. public AutoFunctionChoiceBehavior(IEnumerable functions) { this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)); } + /// + /// The maximum number of function auto-invokes that can be made in a single user request. + /// + /// + /// After this number of iterations as part of a single user request is reached, auto-invocation + /// will be disabled. This is a safeguard against possible runaway execution if the model routinely re-requests + /// the same function over and over. To disable auto invocation, this can be set to 0. + /// [JsonPropertyName("maximumAutoInvokeAttempts")] public int MaximumAutoInvokeAttempts { get; init; } = DefaultMaximumAutoInvokeAttempts; + /// + /// Fully qualified names of subset of the 's plugins' functions information to provide to the model. + /// [JsonPropertyName("functions")] public IEnumerable? Functions { get; init; } - public override FunctionChoiceBehaviorConfiguration Configure(FunctionChoiceBehaviorContext context) + /// + public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { bool autoInvoke = this.MaximumAutoInvokeAttempts > 0; @@ -51,7 +78,7 @@ public override FunctionChoiceBehaviorConfiguration Configure(FunctionChoiceBeha { availableFunctions ??= new List(); - // Make sure that every enabled function can be found in the kernel. + // Make sure that every function can be found in the kernel. Debug.Assert(context.Kernel is not null); var name = FunctionName.Parse(functionFQN, FunctionNameSeparator); diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index af3364f0e88b..b0dd90850b26 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -1,30 +1,71 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Microsoft.SemanticKernel; +/// +/// Represents the base class for different function choice behaviors. +/// +[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] +[JsonDerivedType(typeof(AutoFunctionChoiceBehavior), typeDiscriminator: AutoFunctionChoiceBehavior.Alias)] +[JsonDerivedType(typeof(RequiredFunctionChoiceBehavior), typeDiscriminator: RequiredFunctionChoiceBehavior.Alias)] +[JsonDerivedType(typeof(NoneFunctionChoiceBehavior), typeDiscriminator: NoneFunctionChoiceBehavior.Alias)] public abstract class FunctionChoiceBehavior { + /// The separator used to separate plugin name and function name. protected const string FunctionNameSeparator = "."; - public static FunctionChoiceBehavior AutoFunctionChoice(IEnumerable? enabledFunctions = null, bool autoInvoke = true) + /// + /// The default maximum number of function auto-invokes that can be made in a single user request. + /// + /// + /// After this number of iterations as part of a single user request is reached, auto-invocation + /// will be disabled. This is a safeguard against possible runaway execution if the model routinely re-requests + /// the same function over and over. + /// + protected const int DefaultMaximumAutoInvokeAttempts = 5; + + /// + /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. + /// This behavior allows the model to decide whether to call the functions and, if so, which ones to call. + /// + /// The subset of the 's plugins' function information. + /// Indicates whether the functions should be automatically invoked by the AI service/connector. + /// An instance of one of the derivatives. + public static FunctionChoiceBehavior AutoFunctionChoice(IEnumerable? functions = null, bool autoInvoke = true) { - return new AutoFunctionChoiceBehavior(enabledFunctions ?? []) + return new AutoFunctionChoiceBehavior(functions ?? []) { - MaximumAutoInvokeAttempts = autoInvoke ? AutoFunctionChoiceBehavior.DefaultMaximumAutoInvokeAttempts : 0 + MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0 }; } + /// + /// Gets an instance of the that provides a subset of the 's plugins' function information to the model. + /// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. + /// + /// The subset of the 's plugins' function information. + /// Indicates whether the functions should be automatically invoked by the AI service/connector. + /// An instance of one of the derivatives. public static FunctionChoiceBehavior RequiredFunctionChoice(IEnumerable functions, bool autoInvoke = true) { return new RequiredFunctionChoiceBehavior(functions) { - MaximumAutoInvokeAttempts = autoInvoke ? AutoFunctionChoiceBehavior.DefaultMaximumAutoInvokeAttempts : 0 + MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0 }; } + /// + /// Gets an instance of the that does not provides any 's plugins' function information to the model. + /// This behavior forces the model to not call any functions and only generate a user-facing message. + /// + /// An instance of one of the derivatives. public static FunctionChoiceBehavior None { get; } = new NoneFunctionChoiceBehavior(); - public abstract FunctionChoiceBehaviorConfiguration Configure(FunctionChoiceBehaviorContext context); + /// Returns the configuration specified by the . + /// The caller context. + /// The configuration. + public abstract FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context); } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index 2e56036a7595..d4a5f9d0a3ab 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -4,13 +4,39 @@ namespace Microsoft.SemanticKernel; -public class FunctionChoiceBehaviorConfiguration +/// +/// Represent function choice behavior configuration. +/// +public sealed class FunctionChoiceBehaviorConfiguration { + /// + /// The functions that are available for the model to call. + /// public IEnumerable? AvailableFunctions { get; init; } + /// + /// The functions that the model is required to call. + /// public IEnumerable? RequiredFunctions { get; init; } + /// + /// The maximum number of function auto-invokes that can be made in a single user request. + /// + /// + /// After this number of iterations as part of a single user request is reached, auto-invocation + /// will be disabled. This is a safeguard against possible runaway execution if the model routinely re-requests + /// the same function over and over. To disable auto invocation, this can be set to 0. + /// public int? MaximumAutoInvokeAttempts { get; init; } + /// + /// Number of requests that are part of a single user interaction that should include this functions in the request. + /// + /// + /// This should be greater than or equal to . + /// Once this limit is reached, the functions will no longer be included in subsequent requests that are part of the user operation, e.g. + /// if this is 1, the first request will include the functions, but the subsequent response sending back the functions' result + /// will not include the functions for further use. + /// public int? MaximumUseAttempts { get; init; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs index 0a3f20da2ced..727b7af2c3af 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs @@ -2,7 +2,13 @@ namespace Microsoft.SemanticKernel; +/// +/// The context to be provided by the choice behavior consumer in order to obtain the choice behavior configuration. +/// public class FunctionChoiceBehaviorContext { + /// + /// The to be used for function calling. + /// public Kernel? Kernel { get; init; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs index 4f58a8bf40d5..2c193a6e44bd 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -2,14 +2,25 @@ namespace Microsoft.SemanticKernel; +/// +/// Represents that does not provides any 's plugins' function information to the model. +/// This behavior forces the model to not call any functions and only generate a user-facing message. +/// public sealed class NoneFunctionChoiceBehavior : FunctionChoiceBehavior { - public override FunctionChoiceBehaviorConfiguration Configure(FunctionChoiceBehaviorContext context) + /// + /// The class alias. Used as a value for the discriminator property for polymorphic deserialization + /// of function choice behavior specified in JSON and YAML prompts. + /// + public const string Alias = "none"; + + /// + public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { return new FunctionChoiceBehaviorConfiguration() { - MaximumAutoInvokeAttempts = 0, - MaximumUseAttempts = 0 + // By not providing either available or required functions, we are telling the model to not call any functions. + MaximumAutoInvokeAttempts = 0, // Disable unnecessary auto-invocation }; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index cbd647f0c703..fa032f4d5e62 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -7,32 +7,66 @@ namespace Microsoft.SemanticKernel; +/// +/// Represent that provides a subset of the 's plugins' function information to the model. +/// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. +/// public sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior { - internal const int DefaultMaximumAutoInvokeAttempts = 5; - - internal const int DefaultMaximumUseAttempts = 1; - + /// + /// The class alias. Used as a value for the discriminator property for polymorphic deserialization + /// of function choice behavior specified in JSON and YAML prompts. + /// + public const string Alias = "required"; + + /// + /// Initializes a new instance of the class. + /// [JsonConstructor] public RequiredFunctionChoiceBehavior() { } + /// + /// Initializes a new instance of the class. + /// + /// The subset of the 's plugins' functions information. public RequiredFunctionChoiceBehavior(IEnumerable functions) { this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)); } + /// + /// Fully qualified names of subset of the 's plugins' functions information to provide to the model. + /// [JsonPropertyName("functions")] public IEnumerable? Functions { get; init; } + /// + /// The maximum number of function auto-invokes that can be made in a single user request. + /// + /// + /// After this number of iterations as part of a single user request is reached, auto-invocation + /// will be disabled. This is a safeguard against possible runaway execution if the model routinely re-requests + /// the same function over and over. To disable auto invocation, this can be set to 0. + /// [JsonPropertyName("maximumAutoInvokeAttempts")] public int MaximumAutoInvokeAttempts { get; init; } = DefaultMaximumAutoInvokeAttempts; + /// + /// Number of requests that are part of a single user interaction that should include this functions in the request. + /// + /// + /// This should be greater than or equal to . + /// Once this limit is reached, the functions will no longer be included in subsequent requests that are part of the user operation, e.g. + /// if this is 1, the first request will include the functions, but the subsequent response sending back the functions' result + /// will not include the functions for further use. + /// [JsonPropertyName("maximumUseAttempts")] - public int MaximumUseAttempts { get; init; } = DefaultMaximumUseAttempts; + public int MaximumUseAttempts { get; init; } = 1; - public override FunctionChoiceBehaviorConfiguration Configure(FunctionChoiceBehaviorContext context) + /// + public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { List? requiredFunctions = null; @@ -54,7 +88,6 @@ public override FunctionChoiceBehaviorConfiguration Configure(FunctionChoiceBeha { requiredFunctions ??= []; - // Make sure that every enabled function can be found in the kernel. Debug.Assert(context.Kernel is not null); var name = FunctionName.Parse(functionFQN, FunctionNameSeparator); diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs index 09d423e1b82d..483936032ea8 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs @@ -43,6 +43,33 @@ public string? ModelId } } + /// + /// Gets or sets the behavior for how functions are chosen by the model and how their calls are handled. + /// + /// + /// + /// To disable function calling, and have the model only generate a user-facing message, set the property to null (the default). + /// + /// To allow the model to decide whether to call the functions and, if so, which ones to call, set the property to an instance returned + /// from method. By default, all functions in the will be available. + /// To limit the functions available, pass a list of the functions when calling the method. + /// + /// + /// To force the model to always call one or more functions, set the property to an instance returned + /// from method. + /// Pass list of the functions when calling the method. + /// + /// + /// To force the model to not call any functions and only generate a user-facing message, set the property to an instance returned + /// from property. + /// + /// + /// For all the behaviors that presume the model to call functions, auto-invoke behavior may be selected. If the service + /// sends a request for a function call, if auto-invoke has been requested, the client will attempt to + /// resolve that function from the functions available in the , and if found, rather + /// than returning the response back to the caller, it will handle the request automatically, invoking + /// the function, and sending back the result. The intermediate messages will be retained in the provided . + /// [JsonPropertyName("function_choice_behavior")] public FunctionChoiceBehavior? FunctionChoiceBehavior { diff --git a/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs b/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs index 94ba780b7c4a..7048a5e76062 100644 --- a/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs +++ b/dotnet/src/SemanticKernel.Abstractions/PromptTemplate/PromptTemplateConfig.cs @@ -25,8 +25,6 @@ namespace Microsoft.SemanticKernel; /// public sealed class PromptTemplateConfig { - private readonly static JsonSerializerOptions s_serializerOptions = new(JsonOptionsCache.ReadPermissive) { TypeInfoResolver = FunctionChoiceBehaviorResolver.Instance }; - /// The format of the prompt template. private string? _templateFormat; /// The prompt template string. @@ -68,7 +66,7 @@ public static PromptTemplateConfig FromJson(string json) PromptTemplateConfig? config = null; try { - config = JsonSerializer.Deserialize(json, s_serializerOptions); + config = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); if (config is null) { throw new ArgumentException($"Unable to deserialize {nameof(PromptTemplateConfig)} from the specified JSON.", nameof(json)); diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs index ff09c4a9debc..00d140ab94c7 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs @@ -14,8 +14,6 @@ namespace SemanticKernel.UnitTests.Functions; /// public sealed class FunctionCallChoiceTests { - private readonly static JsonSerializerOptions s_serializerOptions = new() { TypeInfoResolver = new FunctionChoiceBehaviorResolver() }; - [Fact] public void EnableKernelFunctionsAreNotAutoInvoked() { @@ -24,7 +22,7 @@ public void EnableKernelFunctionsAreNotAutoInvoked() var behavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); // Act - var config = behavior.Configure(new() { Kernel = kernel }); + var config = behavior.GetConfiguration(new() { Kernel = kernel }); // Assert Assert.NotNull(config); @@ -39,7 +37,7 @@ public void AutoInvokeKernelFunctionsShouldSpecifyNumberOfAutoInvokeAttempts() var behavior = FunctionChoiceBehavior.AutoFunctionChoice(); // Act - var config = behavior.Configure(new() { Kernel = kernel }); + var config = behavior.GetConfiguration(new() { Kernel = kernel }); // Assert Assert.NotNull(config); @@ -53,7 +51,7 @@ public void KernelFunctionsConfigureWithNullKernelDoesNotAddTools() var behavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); // Act - var config = behavior.Configure(new() { }); + var config = behavior.GetConfiguration(new() { }); // Assert Assert.Null(config.AvailableFunctions); @@ -69,7 +67,7 @@ public void KernelFunctionsConfigureWithoutFunctionsDoesNotAddTools() var kernel = Kernel.CreateBuilder().Build(); // Act - var config = behavior.Configure(new() { Kernel = kernel }); + var config = behavior.GetConfiguration(new() { Kernel = kernel }); // Assert Assert.Null(config.AvailableFunctions); @@ -88,7 +86,7 @@ public void KernelFunctionsConfigureWithFunctionsAddsTools() kernel.Plugins.Add(plugin); // Act - var config = behavior.Configure(new() { Kernel = kernel }); + var config = behavior.GetConfiguration(new() { Kernel = kernel }); // Assert Assert.Null(config.RequiredFunctions); @@ -104,7 +102,7 @@ public void EnabledFunctionsConfigureWithoutFunctionsDoesNotAddTools() var chatCompletionsOptions = new ChatCompletionsOptions(); // Act - var config = behavior.Configure(new() { }); + var config = behavior.GetConfiguration(new() { }); // Assert Assert.Null(chatCompletionsOptions.ToolChoice); @@ -121,7 +119,7 @@ public void EnabledFunctionsConfigureWithAutoInvokeAndNullKernelThrowsException( var behavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: true); // Act & Assert - var exception = Assert.Throws(() => behavior.Configure(new() { })); ; + var exception = Assert.Throws(() => behavior.GetConfiguration(new() { })); ; Assert.Equal("Auto-invocation in Auto mode is not supported when no kernel is provided.", exception.Message); } @@ -134,7 +132,7 @@ public void EnabledFunctionsConfigureWithAutoInvokeAndEmptyKernelThrowsException var kernel = Kernel.CreateBuilder().Build(); // Act & Assert - var exception = Assert.Throws(() => behavior.Configure(new() { Kernel = kernel })); + var exception = Assert.Throws(() => behavior.GetConfiguration(new() { Kernel = kernel })); Assert.Equal("The specified function MyPlugin.MyFunction is not available in the kernel.", exception.Message); } @@ -152,7 +150,7 @@ public void EnabledFunctionsConfigureWithKernelAndPluginsAddsTools(bool autoInvo kernel.Plugins.Add(plugin); // Act - var config = behavior.Configure(new() { Kernel = kernel }); + var config = behavior.GetConfiguration(new() { Kernel = kernel }); // Assert this.AssertFunctions(config.AvailableFunctions); @@ -168,7 +166,7 @@ public void RequiredFunctionsConfigureWithAutoInvokeAndNullKernelThrowsException var behavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: true); // Act & Assert - var exception = Assert.Throws(() => behavior.Configure(new() { })); + var exception = Assert.Throws(() => behavior.GetConfiguration(new() { })); Assert.Equal("Auto-invocation in Auto mode is not supported when no kernel is provided.", exception.Message); } @@ -181,7 +179,7 @@ public void RequiredFunctionsConfigureWithAutoInvokeAndEmptyKernelThrowsExceptio var kernel = Kernel.CreateBuilder().Build(); // Act & Assert - var exception = Assert.Throws(() => behavior.Configure(new() { Kernel = kernel })); + var exception = Assert.Throws(() => behavior.GetConfiguration(new() { Kernel = kernel })); Assert.Equal("The specified function MyPlugin.MyFunction is not available in the kernel.", exception.Message); } @@ -196,7 +194,7 @@ public void RequiredFunctionConfigureAddsTools() kernel.Plugins.Add(plugin); // Act - var config = behavior.Configure(new() { Kernel = kernel }); + var config = behavior.GetConfiguration(new() { Kernel = kernel }); // Assert this.AssertFunctions(config.RequiredFunctions); @@ -218,7 +216,7 @@ public void ItShouldBePossibleToDeserializeAutoFunctionCallChoice() """; // Act - var deserializedFunction = JsonSerializer.Deserialize(json, s_serializerOptions) as AutoFunctionChoiceBehavior; + var deserializedFunction = JsonSerializer.Deserialize(json) as AutoFunctionChoiceBehavior; // Assert Assert.NotNull(deserializedFunction); @@ -245,7 +243,7 @@ public void ItShouldBePossibleToDeserializeForcedFunctionCallChoice() """; // Act - var deserializedFunction = JsonSerializer.Deserialize(json, s_serializerOptions) as RequiredFunctionChoiceBehavior; + var deserializedFunction = JsonSerializer.Deserialize(json) as RequiredFunctionChoiceBehavior; // Assert Assert.NotNull(deserializedFunction); @@ -268,7 +266,7 @@ public void ItShouldBePossibleToDeserializeNoneFunctionCallBehavior() """; // Act - var deserializedFunction = JsonSerializer.Deserialize(json, s_serializerOptions) as NoneFunctionChoiceBehavior; + var deserializedFunction = JsonSerializer.Deserialize(json) as NoneFunctionChoiceBehavior; // Assert Assert.NotNull(deserializedFunction); From 827e50f7c7c8e17e7910ad6300231ac2012f92e0 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 9 May 2024 13:37:25 +0100 Subject: [PATCH 54/90] * Rename Alias const to TypeDiscriminator * Mark all new functionality as experimental --- .../PromptExecutionSettingsTypeConverter.cs | 8 +++++--- .../FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs | 7 ++++--- .../AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs | 8 +++++--- .../FunctionChoiceBehaviorConfiguration.cs | 2 ++ .../FunctionChoiceBehaviorContext.cs | 3 +++ .../FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs | 8 +++++--- .../RequiredFunctionChoiceBehavior.cs | 7 ++++--- .../AI/PromptExecutionSettings.cs | 2 ++ 8 files changed, 30 insertions(+), 15 deletions(-) diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs index f7e98b4029c8..87135aa2c073 100644 --- a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs @@ -31,12 +31,14 @@ public bool Accepts(Type type) // Otherwise, the "Property '{name}' not found on type '{type.FullName}'" exception is thrown. .WithTypeDiscriminatingNodeDeserializer((options) => { +#pragma warning disable SKEXP0001 options.AddKeyValueTypeDiscriminator("type", new Dictionary { - { AutoFunctionChoiceBehavior.Alias, typeof(AutoFunctionChoiceBehavior) }, - { RequiredFunctionChoiceBehavior.Alias, typeof(RequiredFunctionChoiceBehavior) }, - { NoneFunctionChoiceBehavior.Alias, typeof(NoneFunctionChoiceBehavior) } + { AutoFunctionChoiceBehavior.TypeDiscriminator, typeof(AutoFunctionChoiceBehavior) }, + { RequiredFunctionChoiceBehavior.TypeDiscriminator, typeof(RequiredFunctionChoiceBehavior) }, + { NoneFunctionChoiceBehavior.TypeDiscriminator, typeof(NoneFunctionChoiceBehavior) } }); +#pragma warning restore SKEXP0010 }) .Build(); diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index 6df855fe3f10..9ffee857cf6c 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -11,13 +12,13 @@ namespace Microsoft.SemanticKernel; /// Represent that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior allows the model to decide whether to call the functions and, if so, which ones to call. /// +[Experimental("SKEXP0001")] public sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior { /// - /// The class alias. Used as a value for the discriminator property for polymorphic deserialization - /// of function choice behavior specified in JSON and YAML prompts. + /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. /// - public const string Alias = "auto"; + public const string TypeDiscriminator = "auto"; /// /// Initializes a new instance of the class. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index b0dd90850b26..e17fa7e4b051 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace Microsoft.SemanticKernel; @@ -9,9 +10,10 @@ namespace Microsoft.SemanticKernel; /// Represents the base class for different function choice behaviors. /// [JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] -[JsonDerivedType(typeof(AutoFunctionChoiceBehavior), typeDiscriminator: AutoFunctionChoiceBehavior.Alias)] -[JsonDerivedType(typeof(RequiredFunctionChoiceBehavior), typeDiscriminator: RequiredFunctionChoiceBehavior.Alias)] -[JsonDerivedType(typeof(NoneFunctionChoiceBehavior), typeDiscriminator: NoneFunctionChoiceBehavior.Alias)] +[JsonDerivedType(typeof(AutoFunctionChoiceBehavior), typeDiscriminator: AutoFunctionChoiceBehavior.TypeDiscriminator)] +[JsonDerivedType(typeof(RequiredFunctionChoiceBehavior), typeDiscriminator: RequiredFunctionChoiceBehavior.TypeDiscriminator)] +[JsonDerivedType(typeof(NoneFunctionChoiceBehavior), typeDiscriminator: NoneFunctionChoiceBehavior.TypeDiscriminator)] +[Experimental("SKEXP0001")] public abstract class FunctionChoiceBehavior { /// The separator used to separate plugin name and function name. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index d4a5f9d0a3ab..c25f1781a2ff 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -1,12 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.SemanticKernel; /// /// Represent function choice behavior configuration. /// +[Experimental("SKEXP0001")] public sealed class FunctionChoiceBehaviorConfiguration { /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs index 727b7af2c3af..db6bcb10b944 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Diagnostics.CodeAnalysis; + namespace Microsoft.SemanticKernel; /// /// The context to be provided by the choice behavior consumer in order to obtain the choice behavior configuration. /// +[Experimental("SKEXP0001")] public class FunctionChoiceBehaviorContext { /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs index 2c193a6e44bd..ba0b1983d6c6 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -1,18 +1,20 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Diagnostics.CodeAnalysis; + namespace Microsoft.SemanticKernel; /// /// Represents that does not provides any 's plugins' function information to the model. /// This behavior forces the model to not call any functions and only generate a user-facing message. /// +[Experimental("SKEXP0001")] public sealed class NoneFunctionChoiceBehavior : FunctionChoiceBehavior { /// - /// The class alias. Used as a value for the discriminator property for polymorphic deserialization - /// of function choice behavior specified in JSON and YAML prompts. + /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. /// - public const string Alias = "none"; + public const string TypeDiscriminator = "none"; /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index fa032f4d5e62..ad9541d22a35 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -11,13 +12,13 @@ namespace Microsoft.SemanticKernel; /// Represent that provides a subset of the 's plugins' function information to the model. /// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. /// +[Experimental("SKEXP0001")] public sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior { /// - /// The class alias. Used as a value for the discriminator property for polymorphic deserialization - /// of function choice behavior specified in JSON and YAML prompts. + /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. /// - public const string Alias = "required"; + public const string TypeDiscriminator = "required"; /// /// Initializes a new instance of the class. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs index 483936032ea8..0f1e0ee45571 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.TextGeneration; @@ -71,6 +72,7 @@ public string? ModelId /// the function, and sending back the result. The intermediate messages will be retained in the provided . /// [JsonPropertyName("function_choice_behavior")] + [Experimental("SKEXP0001")] public FunctionChoiceBehavior? FunctionChoiceBehavior { get => this._functionChoiceBehavior; From 348ecde4984a7a4d6cdcff19fb55b93f6404a011 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 9 May 2024 16:35:51 +0100 Subject: [PATCH 55/90] * GetConfiguration methods refactored to handle all cases --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 6 +- .../AutoFunctionChoiceBehavior.cs | 85 ++++++++++++----- .../FunctionChoiceBehaviorConfiguration.cs | 9 +- .../NoneFunctionChoiceBehavior.cs | 3 +- .../RequiredFunctionChoiceBehavior.cs | 94 ++++++++++++++----- .../Functions/FunctionCallChoiceTests.cs | 4 +- 6 files changed, 149 insertions(+), 52 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 6bb3f20a7ba7..84bde2a47a6a 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1422,7 +1422,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts) result = new() { - AllowAnyRequestedKernelFunction = false, + AllowAnyRequestedKernelFunction = config.AllowAnyRequestedKernelFunction, MaximumAutoInvokeAttempts = config.MaximumAutoInvokeAttempts, }; @@ -1445,7 +1445,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context throw new KernelException("Only one required function is allowed."); } - var functionDefinition = requiredFunctions.First().ToOpenAIFunction().ToFunctionDefinition(); + var functionDefinition = requiredFunctions.First().Metadata.ToOpenAIFunction().ToFunctionDefinition(); chatOptions.ToolChoice = new ChatCompletionsToolChoice(functionDefinition); chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); @@ -1460,7 +1460,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context foreach (var function in availableFunctions) { - var functionDefinition = function.ToOpenAIFunction().ToFunctionDefinition(); + var functionDefinition = function.Metadata.ToOpenAIFunction().ToFunctionDefinition(); chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index 9ffee857cf6c..23790dd032df 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -15,6 +14,16 @@ namespace Microsoft.SemanticKernel; [Experimental("SKEXP0001")] public sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior { + /// + /// List of the functions that the model can choose from. + /// + private readonly IEnumerable? _functions; + + /// + /// List of the fully qualified names of the functions that the model can choose from. + /// + private readonly IEnumerable? _functionFQNs; + /// /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. /// @@ -34,7 +43,7 @@ public AutoFunctionChoiceBehavior() /// The subset of the 's plugins' functions information. public AutoFunctionChoiceBehavior(IEnumerable functions) { - this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)); + this._functions = functions; } /// @@ -52,7 +61,19 @@ public AutoFunctionChoiceBehavior(IEnumerable functions) /// Fully qualified names of subset of the 's plugins' functions information to provide to the model. /// [JsonPropertyName("functions")] - public IEnumerable? Functions { get; init; } + public IEnumerable? Functions + { + get => this._functionFQNs; + init + { + if (value?.Count() > 0 && this._functions?.Count() > 0) + { + throw new KernelException("Functions are already provided via the constructor."); + } + + this._functionFQNs = value; + } + } /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) @@ -69,41 +90,61 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho throw new KernelException("Auto-invocation in Auto mode is not supported when no kernel is provided."); } - IList? availableFunctions = null; + List? availableFunctions = null; + bool allowAnyRequestedKernelFunction = false; - if (context.Kernel is not null) + // Handle functions provided via constructor as function instances. + if (this._functions is { } functions && functions.Any()) { - if (this.Functions is { } functionFQNs && functionFQNs.Any()) + // Make sure that every function can be found in the kernel. + if (autoInvoke) { - foreach (var functionFQN in functionFQNs) + foreach (var function in functions) { - availableFunctions ??= new List(); - - // Make sure that every function can be found in the kernel. - Debug.Assert(context.Kernel is not null); - - var name = FunctionName.Parse(functionFQN, FunctionNameSeparator); - - if (!context.Kernel!.Plugins.TryGetFunction(name.PluginName, name.Name, out var function)) + if (!context.Kernel!.Plugins.TryGetFunction(function.PluginName, function.Name, out _)) { - throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); + throw new KernelException($"The specified function {function.PluginName}.{function.Name} is not available in the kernel."); } + } + } + + availableFunctions = functions.ToList(); + } + // Handle functions provided via the 'Functions' property as function fully qualified names. + else if (this.Functions is { } functionFQNs && functionFQNs.Any()) + { + availableFunctions = []; - availableFunctions.Add(function.Metadata); + foreach (var functionFQN in functionFQNs) + { + // Make sure that every function can be found in the kernel. + var name = FunctionName.Parse(functionFQN, FunctionNameSeparator); + + if (!context.Kernel!.Plugins.TryGetFunction(name.PluginName, name.Name, out var function)) + { + throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); } + + availableFunctions.Add(function); } - else + } + // Provide all functions from the kernel. + else if (context.Kernel is not null) + { + allowAnyRequestedKernelFunction = true; + + foreach (var plugin in context.Kernel.Plugins) { - // Provide all functions from the kernel. - var kernelFunctions = context.Kernel.Plugins.GetFunctionsMetadata(); - availableFunctions = kernelFunctions.Any() ? kernelFunctions : null; + availableFunctions ??= []; + availableFunctions.AddRange(plugin); } } return new FunctionChoiceBehaviorConfiguration() { AvailableFunctions = availableFunctions, - MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts + MaximumAutoInvokeAttempts = availableFunctions?.Count > 0 ? this.MaximumAutoInvokeAttempts : 0, + AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index c25f1781a2ff..5204cc48bffb 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -14,12 +14,12 @@ public sealed class FunctionChoiceBehaviorConfiguration /// /// The functions that are available for the model to call. /// - public IEnumerable? AvailableFunctions { get; init; } + public IEnumerable? AvailableFunctions { get; init; } /// /// The functions that the model is required to call. /// - public IEnumerable? RequiredFunctions { get; init; } + public IEnumerable? RequiredFunctions { get; init; } /// /// The maximum number of function auto-invokes that can be made in a single user request. @@ -41,4 +41,9 @@ public sealed class FunctionChoiceBehaviorConfiguration /// will not include the functions for further use. /// public int? MaximumUseAttempts { get; init; } + + /// + /// Specifies whether validation against a specified list of functions is required before allowing the model to request a function from the kernel. + /// + public bool? AllowAnyRequestedKernelFunction { get; init; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs index ba0b1983d6c6..2eb8586c514c 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -22,7 +22,8 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho return new FunctionChoiceBehaviorConfiguration() { // By not providing either available or required functions, we are telling the model to not call any functions. - MaximumAutoInvokeAttempts = 0, // Disable unnecessary auto-invocation + AvailableFunctions = null, + RequiredFunctions = null, }; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index ad9541d22a35..aa18c61e4069 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -15,6 +14,16 @@ namespace Microsoft.SemanticKernel; [Experimental("SKEXP0001")] public sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior { + /// + /// List of the functions that the model can choose from. + /// + private readonly IEnumerable? _functions; + + /// + /// List of the fully qualified names of the functions that the model can choose from. + /// + private readonly IEnumerable? _functionFQNs; + /// /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. /// @@ -34,14 +43,26 @@ public RequiredFunctionChoiceBehavior() /// The subset of the 's plugins' functions information. public RequiredFunctionChoiceBehavior(IEnumerable functions) { - this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)); + this._functions = functions; } /// /// Fully qualified names of subset of the 's plugins' functions information to provide to the model. /// [JsonPropertyName("functions")] - public IEnumerable? Functions { get; init; } + public IEnumerable? Functions + { + get => this._functionFQNs; + init + { + if (value?.Count() > 0 && this._functions?.Count() > 0) + { + throw new KernelException("Functions are already provided via the constructor."); + } + + this._functionFQNs = value; + } + } /// /// The maximum number of function auto-invokes that can be made in a single user request. @@ -69,45 +90,74 @@ public RequiredFunctionChoiceBehavior(IEnumerable functions) /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { - List? requiredFunctions = null; + bool autoInvoke = this.MaximumAutoInvokeAttempts > 0; + + // If auto-invocation is specified, we need a kernel to be able to invoke the functions. + // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions + // and then fail to do so, so we fail before we get to that point. This is an error + // on the consumers behalf: if they specify auto-invocation with any functions, they must + // specify the kernel and the kernel must contain those functions. + if (autoInvoke && context.Kernel is null) + { + throw new KernelException("Auto-invocation in Auto mode is not supported when no kernel is provided."); + } - if (this.Functions is { } functionFQNs && functionFQNs.Any()) + List? requiredFunctions = null; + bool allowAnyRequestedKernelFunction = false; + + // Handle functions provided via constructor as function instances. + if (this._functions is { } functions && functions.Any()) { - bool autoInvoke = this.MaximumAutoInvokeAttempts > 0; - - // If auto-invocation is specified, we need a kernel to be able to invoke the functions. - // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions - // and then fail to do so, so we fail before we get to that point. This is an error - // on the consumers behalf: if they specify auto-invocation with any functions, they must - // specify the kernel and the kernel must contain those functions. - if (autoInvoke && context.Kernel is null) + // Make sure that every function can be found in the kernel. + if (autoInvoke) { - throw new KernelException("Auto-invocation in Any mode is not supported when no kernel is provided."); + foreach (var function in functions) + { + if (!context.Kernel!.Plugins.TryGetFunction(function.PluginName, function.Name, out _)) + { + throw new KernelException($"The specified function {function.PluginName}.{function.Name} is not available in the kernel."); + } + } } + requiredFunctions = functions.ToList(); + } + // Handle functions provided via the 'Functions' property as function fully qualified names. + else if (this.Functions is { } functionFQNs && functionFQNs.Any()) + { + requiredFunctions = []; + foreach (var functionFQN in functionFQNs) { - requiredFunctions ??= []; - - Debug.Assert(context.Kernel is not null); - + // Make sure that every function can be found in the kernel. var name = FunctionName.Parse(functionFQN, FunctionNameSeparator); - // Make sure that the required functions can be found in the kernel. if (!context.Kernel!.Plugins.TryGetFunction(name.PluginName, name.Name, out var function)) { throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); } - requiredFunctions.Add(function.Metadata); + requiredFunctions.Add(function); + } + } + // Provide all functions from the kernel. + else if (context.Kernel is not null) + { + allowAnyRequestedKernelFunction = true; + + foreach (var plugin in context.Kernel.Plugins) + { + requiredFunctions ??= []; + requiredFunctions.AddRange(plugin); } } return new FunctionChoiceBehaviorConfiguration() { RequiredFunctions = requiredFunctions, - MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts, - MaximumUseAttempts = this.MaximumUseAttempts + MaximumAutoInvokeAttempts = requiredFunctions?.Count > 0 ? this.MaximumAutoInvokeAttempts : 0, + MaximumUseAttempts = this.MaximumUseAttempts, + AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; } } diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs index 00d140ab94c7..a3bf69d00d6f 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs @@ -284,7 +284,7 @@ private KernelPlugin GetTestPlugin() return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); } - private void AssertFunctions(IEnumerable? kernelFunctionsMetadata) + private void AssertFunctions(IEnumerable? kernelFunctionsMetadata) { Assert.NotNull(kernelFunctionsMetadata); Assert.Single(kernelFunctionsMetadata); @@ -296,6 +296,6 @@ private void AssertFunctions(IEnumerable? kernelFunction Assert.Equal("MyPlugin", functionMetadata.PluginName); Assert.Equal("MyFunction", functionMetadata.Name); Assert.Equal("Test Function", functionMetadata.Description); - Assert.Equal(2, functionMetadata.Parameters.Count); + Assert.Equal(2, functionMetadata.Metadata.Parameters.Count); } } From fa5525ef3f17567655d5fa757e076658ed12e2da Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 9 May 2024 17:10:04 +0100 Subject: [PATCH 56/90] a few small fixes --- .../Connectors.OpenAI/OpenAIPromptExecutionSettings.cs | 1 + .../AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs | 2 +- .../FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs index b731db727149..d682c3f2e237 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs @@ -294,6 +294,7 @@ public override PromptExecutionSettings Clone() ResponseFormat = this.ResponseFormat, TokenSelectionBiases = this.TokenSelectionBiases is not null ? new Dictionary(this.TokenSelectionBiases) : null, ToolCallBehavior = this.ToolCallBehavior, + FunctionChoiceBehavior = this.FunctionChoiceBehavior, User = this.User, ChatSystemPrompt = this.ChatSystemPrompt }; diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index 23790dd032df..04822ae8403f 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -143,7 +143,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho return new FunctionChoiceBehaviorConfiguration() { AvailableFunctions = availableFunctions, - MaximumAutoInvokeAttempts = availableFunctions?.Count > 0 ? this.MaximumAutoInvokeAttempts : 0, + MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index aa18c61e4069..faf9c3703c97 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -155,7 +155,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho return new FunctionChoiceBehaviorConfiguration() { RequiredFunctions = requiredFunctions, - MaximumAutoInvokeAttempts = requiredFunctions?.Count > 0 ? this.MaximumAutoInvokeAttempts : 0, + MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts, MaximumUseAttempts = this.MaximumUseAttempts, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; From 399f0a3baf45f481a9295cb97f2b7fef6a587567 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 9 May 2024 22:25:50 +0100 Subject: [PATCH 57/90] small fixes + first portion of unit tests --- .../Functions/KernelFunctionMarkdownTests.cs | 34 +- .../Yaml/Functions/KernelFunctionYamlTests.cs | 7 +- .../PromptExecutionSettingsTypeConverter.cs | 2 +- .../AutoFunctionChoiceBehavior.cs | 2 +- .../FunctionChoiceBehavior.cs | 6 +- .../RequiredFunctionChoiceBehavior.cs | 4 +- .../AutoFunctionChoiceBehaviorTests.cs | 298 ++++++++++++++++ .../Functions/FunctionCallChoiceTests.cs | 301 ---------------- .../Functions/FunctionChoiceBehaviorTests.cs | 240 +++++++++++++ .../NoneFunctionChoiceBehaviorTests.cs | 47 +++ .../RequiredFunctionChoiceBehaviorTests.cs | 335 ++++++++++++++++++ .../PromptTemplateConfigTests.cs | 27 +- 12 files changed, 969 insertions(+), 334 deletions(-) create mode 100644 dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs delete mode 100644 dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Functions/NoneFunctionChoiceBehaviorTests.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs diff --git a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs index 10396cc6219a..8a5ab2c15f98 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs @@ -26,7 +26,7 @@ public void ItShouldCreatePromptFunctionConfigFromMarkdown() } [Fact] - public void ItShouldInitializeFunctionCallChoicesFromMarkdown() + public void ItShouldInitializeFunctionChoiceBehaviorsFromMarkdown() { // Arrange var kernel = new Kernel(); @@ -44,29 +44,31 @@ public void ItShouldInitializeFunctionCallChoicesFromMarkdown() var service1ExecutionSettings = function.ExecutionSettings["service1"]; Assert.NotNull(service1ExecutionSettings); - var service1AutoFunctionChoiceBehavior = service1ExecutionSettings?.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; - Assert.NotNull(service1AutoFunctionChoiceBehavior); + var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; + Assert.NotNull(autoFunctionChoiceBehavior); - Assert.NotNull(service1AutoFunctionChoiceBehavior.Functions); - Assert.Single(service1AutoFunctionChoiceBehavior.Functions); - Assert.Equal("p1.f1", service1AutoFunctionChoiceBehavior.Functions.First()); + Assert.NotNull(autoFunctionChoiceBehavior.Functions); + Assert.Single(autoFunctionChoiceBehavior.Functions); + Assert.Equal("p1.f1", autoFunctionChoiceBehavior.Functions.First()); + Assert.Equal(8, autoFunctionChoiceBehavior.MaximumAutoInvokeAttempts); // RequiredFunctionCallChoice for service2 var service2ExecutionSettings = function.ExecutionSettings["service2"]; Assert.NotNull(service2ExecutionSettings); - var service2RequiredFunctionChoiceBehavior = service2ExecutionSettings?.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; - Assert.NotNull(service2RequiredFunctionChoiceBehavior); - Assert.NotNull(service2RequiredFunctionChoiceBehavior.Functions); - Assert.Single(service2RequiredFunctionChoiceBehavior.Functions); - Assert.Equal("p1.f1", service2RequiredFunctionChoiceBehavior.Functions.First()); + var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; + Assert.NotNull(requiredFunctionChoiceBehavior); + Assert.NotNull(requiredFunctionChoiceBehavior.Functions); + Assert.Single(requiredFunctionChoiceBehavior.Functions); + Assert.Equal("p1.f1", requiredFunctionChoiceBehavior.Functions.First()); + Assert.Equal(2, requiredFunctionChoiceBehavior.MaximumUseAttempts); // NoneFunctionCallChoice for service3 var service3ExecutionSettings = function.ExecutionSettings["service3"]; Assert.NotNull(service3ExecutionSettings); - var service3NoneFunctionChoiceBehavior = service3ExecutionSettings?.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; - Assert.NotNull(service3NoneFunctionChoiceBehavior); + var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; + Assert.NotNull(noneFunctionChoiceBehavior); } [Fact] @@ -96,7 +98,8 @@ These are AI execution settings "temperature": 0.7, "function_choice_behavior": { "type": "auto", - "functions": ["p1.f1"] + "functions": ["p1.f1"], + "maximumAutoInvokeAttempts": 8 } } } @@ -109,7 +112,8 @@ These are more AI execution settings "temperature": 0.8, "function_choice_behavior": { "type": "required", - "functions": ["p1.f1"] + "functions": ["p1.f1"], + "maximumUseAttempts": 2 } } } diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs index 6d19fa43dbff..f8eb74a6f722 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs @@ -4,7 +4,6 @@ using System.Linq; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; - using Xunit; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -100,6 +99,7 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; Assert.NotNull(autoFunctionChoiceBehavior?.Functions); Assert.Equal("p1.f1", autoFunctionChoiceBehavior.Functions.Single()); + Assert.Equal(9, autoFunctionChoiceBehavior.MaximumAutoInvokeAttempts); // Service with required function choice behavior var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; @@ -107,6 +107,8 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); Assert.Equal("p2.f2", requiredFunctionChoiceBehavior.Functions.Single()); + Assert.Equal(6, requiredFunctionChoiceBehavior.MaximumAutoInvokeAttempts); + Assert.Equal(3, requiredFunctionChoiceBehavior.MaximumUseAttempts); // Service with none function choice behavior var service3ExecutionSettings = promptTemplateConfig.ExecutionSettings["service3"]; @@ -192,6 +194,7 @@ string CreateYaml(object defaultValue) stop_sequences: [] function_choice_behavior: type: auto + maximum_auto_invoke_attempts: 9 functions: - p1.f1 service2: @@ -204,6 +207,8 @@ string CreateYaml(object defaultValue) stop_sequences: [ "foo", "bar", "baz" ] function_choice_behavior: type: required + maximum_auto_invoke_attempts: 6 + maximum_use_attempts: 3 functions: - p2.f2 service3: diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs index 87135aa2c073..c7889c4035fa 100644 --- a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs @@ -26,7 +26,7 @@ public bool Accepts(Type type) public object? ReadYaml(IParser parser, Type type) { s_deserializer ??= new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithNamingConvention(UnderscoredNamingConvention.Instance) .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination in the WithTypeDiscriminatingNodeDeserializer method below. // Otherwise, the "Property '{name}' not found on type '{type.FullName}'" exception is thrown. .WithTypeDiscriminatingNodeDeserializer((options) => diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index 04822ae8403f..0bf1a9a91dcd 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -87,7 +87,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho // specify the kernel and the kernel must contain those functions. if (autoInvoke && context.Kernel is null) { - throw new KernelException("Auto-invocation in Auto mode is not supported when no kernel is provided."); + throw new KernelException("Auto-invocation for Auto choice behavior is not supported when no kernel is provided."); } List? availableFunctions = null; diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index e17fa7e4b051..50e64526f339 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -45,15 +45,15 @@ public static FunctionChoiceBehavior AutoFunctionChoice(IEnumerable - /// Gets an instance of the that provides a subset of the 's plugins' function information to the model. + /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. /// /// The subset of the 's plugins' function information. /// Indicates whether the functions should be automatically invoked by the AI service/connector. /// An instance of one of the derivatives. - public static FunctionChoiceBehavior RequiredFunctionChoice(IEnumerable functions, bool autoInvoke = true) + public static FunctionChoiceBehavior RequiredFunctionChoice(IEnumerable? functions = null, bool autoInvoke = true) { - return new RequiredFunctionChoiceBehavior(functions) + return new RequiredFunctionChoiceBehavior(functions ?? []) { MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0 }; diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index faf9c3703c97..1109a2cd2c77 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel; /// -/// Represent that provides a subset of the 's plugins' function information to the model. +/// Represent that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. /// [Experimental("SKEXP0001")] @@ -99,7 +99,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho // specify the kernel and the kernel must contain those functions. if (autoInvoke && context.Kernel is null) { - throw new KernelException("Auto-invocation in Auto mode is not supported when no kernel is provided."); + throw new KernelException("Auto-invocation for Required choice behavior is not supported when no kernel is provided."); } List? requiredFunctions = null; diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs new file mode 100644 index 000000000000..d89ab0d0f273 --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs @@ -0,0 +1,298 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.UnitTests.Functions; + +/// +/// Unit tests for +/// +public sealed class AutoFunctionChoiceBehaviorTests +{ + private readonly Kernel _kernel; + + public AutoFunctionChoiceBehaviorTests() + { + this._kernel = new Kernel(); + } + + [Fact] + public void ItShouldAdvertiseKernelFunctionsAsAvailableOnes() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new AutoFunctionChoiceBehavior(); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.RequiredFunctions); + + Assert.NotNull(config.AvailableFunctions); + Assert.Equal(3, config.AvailableFunctions.Count()); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function3"); + } + + [Fact] + public void ItShouldAdvertiseFunctionsProvidedAsInstancesAsAvailableOnes() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new AutoFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.RequiredFunctions); + + Assert.NotNull(config.AvailableFunctions); + Assert.Equal(2, config.AvailableFunctions.Count()); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); + } + + [Fact] + public void ItShouldAdvertiseFunctionsProvidedAsFunctionFQNsAsAvailableOnes() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new AutoFunctionChoiceBehavior() + { + Functions = ["MyPlugin.Function1", "MyPlugin.Function2"] + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.RequiredFunctions); + + Assert.NotNull(config.AvailableFunctions); + Assert.Equal(2, config.AvailableFunctions.Count()); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); + } + + [Fact] + public void ItShouldHaveFiveMaxAutoInvokeAttemptsByDefault() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new AutoFunctionChoiceBehavior(); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(5, config.MaximumAutoInvokeAttempts); + } + + [Fact] + public void ItShouldAllowAutoInvocation() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new AutoFunctionChoiceBehavior() + { + MaximumAutoInvokeAttempts = 8 + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(8, config.MaximumAutoInvokeAttempts); + } + + [Fact] + public void ItShouldAllowManualInvocation() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new AutoFunctionChoiceBehavior() + { + MaximumAutoInvokeAttempts = 0 + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(0, config.MaximumAutoInvokeAttempts); + } + + [Fact] + public void ItShouldThrowExceptionIfFunctionProvidedAsInstancesAndAsFunctionFQNsAtTheSameTime() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var exception = Assert.Throws(() => + { + var choiceBehavior = new AutoFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]) + { + Functions = ["MyPlugin.Function1"] + }; + }); + + Assert.Equal("Functions are already provided via the constructor.", exception.Message); + } + + [Fact] + public void ItShouldThrowExceptionIfAutoInvocationRequestedButNoKernelIsProvided() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + var choiceBehavior = new AutoFunctionChoiceBehavior() + { + MaximumAutoInvokeAttempts = 8 + }; + + // Act + var exception = Assert.Throws(() => + { + choiceBehavior.GetConfiguration(new() { Kernel = null }); + }); + + Assert.Equal("Auto-invocation for Auto choice behavior is not supported when no kernel is provided.", exception.Message); + } + + [Fact] + public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegisteredInKernel() + { + // Arrange + var plugin = GetTestPlugin(); + + var choiceBehavior = new AutoFunctionChoiceBehavior([plugin.ElementAt(0)]) + { + MaximumAutoInvokeAttempts = 5 + }; + + // Act + var exception = Assert.Throws(() => + { + choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + }); + + Assert.Equal("The specified function MyPlugin.Function1 is not available in the kernel.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ItShouldThrowExceptionIfFunctionProvidedAsFunctionFQNIsNotRegisteredInKernel(bool autoInvoke) + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + var choiceBehavior = new AutoFunctionChoiceBehavior() + { + MaximumAutoInvokeAttempts = autoInvoke ? 5 : 0, + Functions = ["MyPlugin.NonKernelFunction"] + }; + + // Act + var exception = Assert.Throws(() => + { + choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + }); + + Assert.Equal("The specified function MyPlugin.NonKernelFunction is not available in the kernel.", exception.Message); + } + + [Fact] + public void ItShouldAllowToInvokeAnyRequestedKernelFunctionForKernelFunctions() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new AutoFunctionChoiceBehavior(); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.True(config.AllowAnyRequestedKernelFunction); + } + + [Fact] + public void ItShouldNotAllowInvokingAnyRequestedKernelFunctionForProvidedAsInstancesFunctions() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new AutoFunctionChoiceBehavior(functions: [plugin.ElementAt(1)]); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.False(config.AllowAnyRequestedKernelFunction); + } + + [Fact] + public void ItShouldNotAllowInvokingAnyRequestedKernelFunctionForFunctionsProvidedAsFunctionFQNs() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new AutoFunctionChoiceBehavior() + { + Functions = ["MyPlugin.Function2"] + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.False(config.AllowAnyRequestedKernelFunction); + } + + private static KernelPlugin GetTestPlugin() + { + var function1 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function2"); + var function3 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function3"); + + return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2, function3]); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs deleted file mode 100644 index a3bf69d00d6f..000000000000 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionCallChoiceTests.cs +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using Azure.AI.OpenAI; -using Microsoft.SemanticKernel; -using Xunit; - -namespace SemanticKernel.UnitTests.Functions; - -/// -/// Unit tests for -/// -public sealed class FunctionCallChoiceTests -{ - [Fact] - public void EnableKernelFunctionsAreNotAutoInvoked() - { - // Arrange - var kernel = new Kernel(); - var behavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); - - // Act - var config = behavior.GetConfiguration(new() { Kernel = kernel }); - - // Assert - Assert.NotNull(config); - Assert.Equal(0, config.MaximumAutoInvokeAttempts); - } - - [Fact] - public void AutoInvokeKernelFunctionsShouldSpecifyNumberOfAutoInvokeAttempts() - { - // Arrange - var kernel = new Kernel(); - var behavior = FunctionChoiceBehavior.AutoFunctionChoice(); - - // Act - var config = behavior.GetConfiguration(new() { Kernel = kernel }); - - // Assert - Assert.NotNull(config); - Assert.Equal(5, config.MaximumAutoInvokeAttempts); - } - - [Fact] - public void KernelFunctionsConfigureWithNullKernelDoesNotAddTools() - { - // Arrange - var behavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); - - // Act - var config = behavior.GetConfiguration(new() { }); - - // Assert - Assert.Null(config.AvailableFunctions); - Assert.Null(config.RequiredFunctions); - } - - [Fact] - public void KernelFunctionsConfigureWithoutFunctionsDoesNotAddTools() - { - // Arrange - var behavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); - - var kernel = Kernel.CreateBuilder().Build(); - - // Act - var config = behavior.GetConfiguration(new() { Kernel = kernel }); - - // Assert - Assert.Null(config.AvailableFunctions); - Assert.Null(config.RequiredFunctions); - } - - [Fact] - public void KernelFunctionsConfigureWithFunctionsAddsTools() - { - // Arrange - var behavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); - var kernel = Kernel.CreateBuilder().Build(); - - var plugin = this.GetTestPlugin(); - - kernel.Plugins.Add(plugin); - - // Act - var config = behavior.GetConfiguration(new() { Kernel = kernel }); - - // Assert - Assert.Null(config.RequiredFunctions); - - this.AssertFunctions(config.AvailableFunctions); - } - - [Fact] - public void EnabledFunctionsConfigureWithoutFunctionsDoesNotAddTools() - { - // Arrange - var behavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); - - // Act - var config = behavior.GetConfiguration(new() { }); - - // Assert - Assert.Null(chatCompletionsOptions.ToolChoice); - Assert.Empty(chatCompletionsOptions.Tools); - } - - [Fact] - public void EnabledFunctionsConfigureWithAutoInvokeAndNullKernelThrowsException() - { - // Arrange - var kernel = new Kernel(); - - var function = this.GetTestPlugin().Single(); - var behavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: true); - - // Act & Assert - var exception = Assert.Throws(() => behavior.GetConfiguration(new() { })); ; - Assert.Equal("Auto-invocation in Auto mode is not supported when no kernel is provided.", exception.Message); - } - - [Fact] - public void EnabledFunctionsConfigureWithAutoInvokeAndEmptyKernelThrowsException() - { - // Arrange - var function = this.GetTestPlugin().Single(); - var behavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: true); - var kernel = Kernel.CreateBuilder().Build(); - - // Act & Assert - var exception = Assert.Throws(() => behavior.GetConfiguration(new() { Kernel = kernel })); - Assert.Equal("The specified function MyPlugin.MyFunction is not available in the kernel.", exception.Message); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void EnabledFunctionsConfigureWithKernelAndPluginsAddsTools(bool autoInvoke) - { - // Arrange - var plugin = this.GetTestPlugin(); - var function = plugin.Single(); - var behavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: autoInvoke); - var kernel = Kernel.CreateBuilder().Build(); - - kernel.Plugins.Add(plugin); - - // Act - var config = behavior.GetConfiguration(new() { Kernel = kernel }); - - // Assert - this.AssertFunctions(config.AvailableFunctions); - } - - [Fact] - public void RequiredFunctionsConfigureWithAutoInvokeAndNullKernelThrowsException() - { - // Arrange - var kernel = new Kernel(); - - var function = this.GetTestPlugin().Single(); - var behavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: true); - - // Act & Assert - var exception = Assert.Throws(() => behavior.GetConfiguration(new() { })); - Assert.Equal("Auto-invocation in Auto mode is not supported when no kernel is provided.", exception.Message); - } - - [Fact] - public void RequiredFunctionsConfigureWithAutoInvokeAndEmptyKernelThrowsException() - { - // Arrange - var function = this.GetTestPlugin().Single(); - var behavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: true); - var kernel = Kernel.CreateBuilder().Build(); - - // Act & Assert - var exception = Assert.Throws(() => behavior.GetConfiguration(new() { Kernel = kernel })); - Assert.Equal("The specified function MyPlugin.MyFunction is not available in the kernel.", exception.Message); - } - - [Fact] - public void RequiredFunctionConfigureAddsTools() - { - // Arrange - var plugin = this.GetTestPlugin(); - var function = plugin.Single(); - var behavior = FunctionChoiceBehavior.RequiredFunctionChoice([function], autoInvoke: true); - var kernel = new Kernel(); - kernel.Plugins.Add(plugin); - - // Act - var config = behavior.GetConfiguration(new() { Kernel = kernel }); - - // Assert - this.AssertFunctions(config.RequiredFunctions); - } - - [Fact] - public void ItShouldBePossibleToDeserializeAutoFunctionCallChoice() - { - // Arrange - var json = - """ - { - "type":"auto", - "maximumAutoInvokeAttempts":12, - "functions":[ - "MyPlugin.MyFunction" - ] - } - """; - - // Act - var deserializedFunction = JsonSerializer.Deserialize(json) as AutoFunctionChoiceBehavior; - - // Assert - Assert.NotNull(deserializedFunction); - Assert.Equal(12, deserializedFunction.MaximumAutoInvokeAttempts); - Assert.NotNull(deserializedFunction.Functions); - Assert.Single(deserializedFunction.Functions); - Assert.Equal("MyPlugin.MyFunction", deserializedFunction.Functions.ElementAt(0)); - } - - [Fact] - public void ItShouldBePossibleToDeserializeForcedFunctionCallChoice() - { - // Arrange - var json = - """ - { - "type": "required", - "maximumAutoInvokeAttempts": 12, - "maximumUseAttempts": 10, - "functions":[ - "MyPlugin.MyFunction" - ] - } - """; - - // Act - var deserializedFunction = JsonSerializer.Deserialize(json) as RequiredFunctionChoiceBehavior; - - // Assert - Assert.NotNull(deserializedFunction); - Assert.Equal(10, deserializedFunction.MaximumUseAttempts); - Assert.Equal(12, deserializedFunction.MaximumAutoInvokeAttempts); - Assert.NotNull(deserializedFunction.Functions); - Assert.Single(deserializedFunction.Functions); - Assert.Equal("MyPlugin.MyFunction", deserializedFunction.Functions.ElementAt(0)); - } - - [Fact] - public void ItShouldBePossibleToDeserializeNoneFunctionCallBehavior() - { - // Arrange - var json = - """ - { - "type": "none" - } - """; - - // Act - var deserializedFunction = JsonSerializer.Deserialize(json) as NoneFunctionChoiceBehavior; - - // Assert - Assert.NotNull(deserializedFunction); - } - - private KernelPlugin GetTestPlugin() - { - var function = KernelFunctionFactory.CreateFromMethod( - (string parameter1, string parameter2) => "Result1", - "MyFunction", - "Test Function", - [new KernelParameterMetadata("parameter1"), new KernelParameterMetadata("parameter2")], - new KernelReturnParameterMetadata { ParameterType = typeof(string), Description = "Function Result" }); - - return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - } - - private void AssertFunctions(IEnumerable? kernelFunctionsMetadata) - { - Assert.NotNull(kernelFunctionsMetadata); - Assert.Single(kernelFunctionsMetadata); - - var functionMetadata = kernelFunctionsMetadata.ElementAt(0); - - Assert.NotNull(functionMetadata); - - Assert.Equal("MyPlugin", functionMetadata.PluginName); - Assert.Equal("MyFunction", functionMetadata.Name); - Assert.Equal("Test Function", functionMetadata.Description); - Assert.Equal(2, functionMetadata.Metadata.Parameters.Count); - } -} diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs new file mode 100644 index 000000000000..f7988c1329d0 --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.UnitTests.Functions; + +/// +/// Unit tests for +/// +public sealed class FunctionChoiceBehaviorTests +{ + private readonly Kernel _kernel; + + public FunctionChoiceBehaviorTests() + { + this._kernel = new Kernel(); + } + + [Fact] + public void AutoFunctionChoiceShouldBeUsed() + { + // Act + var choiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(); + + // Assert + Assert.IsType(choiceBehavior); + } + + [Fact] + public void RequiredFunctionChoiceShouldBeUsed() + { + // Act + var choiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(); + + // Assert + Assert.IsType(choiceBehavior); + } + + [Fact] + public void NoneFunctionChoiceShouldBeUsed() + { + // Act + var choiceBehavior = FunctionChoiceBehavior.None; + + // Assert + Assert.IsType(choiceBehavior); + } + + [Fact] + public void AutoFunctionChoiceShouldAdvertiseKernelFunctionsAsAvailableOnes() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(functions: null); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.RequiredFunctions); + + Assert.NotNull(config.AvailableFunctions); + Assert.Equal(3, config.AvailableFunctions.Count()); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function3"); + } + + [Fact] + public void AutoFunctionChoiceShouldAdvertiseProvidedFunctionsAsAvailableOnes() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.RequiredFunctions); + + Assert.NotNull(config.AvailableFunctions); + Assert.Equal(2, config.AvailableFunctions.Count()); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); + } + + [Fact] + public void AutoFunctionChoiceShouldAllowAutoInvocation() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: true); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(5, config.MaximumAutoInvokeAttempts); + } + + [Fact] + public void AutoFunctionChoiceShouldAllowManualInvocation() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(0, config.MaximumAutoInvokeAttempts); + } + + [Fact] + public void RequiredFunctionChoiceShouldAdvertiseKernelFunctionsAsRequiredOnes() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(functions: null); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.AvailableFunctions); + + Assert.NotNull(config.RequiredFunctions); + Assert.Equal(3, config.RequiredFunctions.Count()); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function3"); + } + + [Fact] + public void RequiredFunctionChoiceShouldAdvertiseProvidedFunctionsAsRequiredOnes() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.AvailableFunctions); + + Assert.NotNull(config.RequiredFunctions); + Assert.Equal(2, config.RequiredFunctions.Count()); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); + } + + [Fact] + public void RequiredFunctionChoiceShouldAllowAutoInvocation() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(autoInvoke: true); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(5, config.MaximumAutoInvokeAttempts); + } + + [Fact] + public void RequiredFunctionChoiceShouldAllowManualInvocation() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(autoInvoke: false); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(0, config.MaximumAutoInvokeAttempts); + } + + [Fact] + public void NoneFunctionChoiceShouldAdvertiseNoFunctions() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = FunctionChoiceBehavior.None; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.AvailableFunctions); + Assert.Null(config.RequiredFunctions); + } + + private static KernelPlugin GetTestPlugin() + { + var function1 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function2"); + var function3 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function3"); + + return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2, function3]); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/NoneFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/NoneFunctionChoiceBehaviorTests.cs new file mode 100644 index 000000000000..1e1186be449c --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/NoneFunctionChoiceBehaviorTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.UnitTests.Functions; + +/// +/// Unit tests for +/// +public sealed class NoneFunctionChoiceBehaviorTests +{ + private readonly Kernel _kernel; + + public NoneFunctionChoiceBehaviorTests() + { + this._kernel = new Kernel(); + } + + [Fact] + public void ItShouldAdvertiseNeitherAvailableNorRequiredFunctions() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new NoneFunctionChoiceBehavior(); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.AvailableFunctions); + Assert.Null(config.RequiredFunctions); + } + + private static KernelPlugin GetTestPlugin() + { + var function1 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function2"); + var function3 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function3"); + + return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2, function3]); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs new file mode 100644 index 000000000000..210eb91f7365 --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs @@ -0,0 +1,335 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.UnitTests.Functions; + +/// +/// Unit tests for +/// +public sealed class RequiredFunctionChoiceBehaviorTests +{ + private readonly Kernel _kernel; + + public RequiredFunctionChoiceBehaviorTests() + { + this._kernel = new Kernel(); + } + + [Fact] + public void ItShouldAdvertiseKernelFunctionsAsRequiredOnes() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior(); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.AvailableFunctions); + + Assert.NotNull(config.RequiredFunctions); + Assert.Equal(3, config.RequiredFunctions.Count()); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function3"); + } + + [Fact] + public void ItShouldAdvertiseFunctionsProvidedAsInstancesAsRequiredOnes() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.AvailableFunctions); + + Assert.NotNull(config.RequiredFunctions); + Assert.Equal(2, config.RequiredFunctions.Count()); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); + } + + [Fact] + public void ItShouldAdvertiseFunctionsProvidedAsFunctionFQNsAsRequiredOnes() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior() + { + Functions = ["MyPlugin.Function1", "MyPlugin.Function2"] + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.AvailableFunctions); + + Assert.NotNull(config.RequiredFunctions); + Assert.Equal(2, config.RequiredFunctions.Count()); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); + } + + [Fact] + public void ItShouldHaveFiveMaxAutoInvokeAttemptsByDefault() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior(); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(5, config.MaximumAutoInvokeAttempts); + } + + [Fact] + public void ItShouldAllowAutoInvocation() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior() + { + MaximumAutoInvokeAttempts = 8 + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(8, config.MaximumAutoInvokeAttempts); + } + + [Fact] + public void ItShouldAllowManualInvocation() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior() + { + MaximumAutoInvokeAttempts = 0 + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(0, config.MaximumAutoInvokeAttempts); + } + + [Fact] + public void ItShouldThrowExceptionIfFunctionProvidedAsInstancesAndAsFunctionFQNsAtTheSameTime() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var exception = Assert.Throws(() => + { + var choiceBehavior = new RequiredFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]) + { + Functions = ["MyPlugin.Function1"] + }; + }); + + Assert.Equal("Functions are already provided via the constructor.", exception.Message); + } + + [Fact] + public void ItShouldThrowExceptionIfAutoInvocationRequestedButNoKernelIsProvided() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + var choiceBehavior = new RequiredFunctionChoiceBehavior() + { + MaximumAutoInvokeAttempts = 8 + }; + + // Act + var exception = Assert.Throws(() => + { + choiceBehavior.GetConfiguration(new() { Kernel = null }); + }); + + Assert.Equal("Auto-invocation for Required choice behavior is not supported when no kernel is provided.", exception.Message); + } + + [Fact] + public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegisteredInKernel() + { + // Arrange + var plugin = GetTestPlugin(); + + var choiceBehavior = new RequiredFunctionChoiceBehavior([plugin.ElementAt(0)]) + { + MaximumAutoInvokeAttempts = 5 + }; + + // Act + var exception = Assert.Throws(() => + { + choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + }); + + Assert.Equal("The specified function MyPlugin.Function1 is not available in the kernel.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ItShouldThrowExceptionIfFunctionProvidedAsFunctionFQNIsNotRegisteredInKernel(bool autoInvoke) + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + var choiceBehavior = new RequiredFunctionChoiceBehavior() + { + MaximumAutoInvokeAttempts = autoInvoke ? 5 : 0, + Functions = ["MyPlugin.NonKernelFunction"] + }; + + // Act + var exception = Assert.Throws(() => + { + choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + }); + + Assert.Equal("The specified function MyPlugin.NonKernelFunction is not available in the kernel.", exception.Message); + } + + [Fact] + public void ItShouldAllowToInvokeAnyRequestedKernelFunctionForKernelFunctions() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior(); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.True(config.AllowAnyRequestedKernelFunction); + } + + [Fact] + public void ItShouldNotAllowInvokingAnyRequestedKernelFunctionForProvidedAsInstancesFunctions() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior(functions: [plugin.ElementAt(1)]); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.False(config.AllowAnyRequestedKernelFunction); + } + + [Fact] + public void ItShouldNotAllowInvokingAnyRequestedKernelFunctionForFunctionsProvidedAsFunctionFQNs() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior() + { + Functions = ["MyPlugin.Function2"] + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.False(config.AllowAnyRequestedKernelFunction); + } + + [Fact] + public void ItShouldHaveOneMaxUseAttemptsByDefault() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior(); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(1, config.MaximumUseAttempts); + } + + [Fact] + public void ItShouldAllowChangingMaxUseAttempts() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior() + { + MaximumUseAttempts = 2 + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + Assert.Equal(2, config.MaximumUseAttempts); + } + + private static KernelPlugin GetTestPlugin() + { + var function1 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function2"); + var function3 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function3"); + + return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2, function3]); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs index 358c9af9388e..ebd32c22bec3 100644 --- a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs @@ -169,12 +169,15 @@ public void DeserializingAutoFunctionCallingChoice() Assert.NotNull(promptTemplateConfig); Assert.Single(promptTemplateConfig.ExecutionSettings); - var executionSettings = promptTemplateConfig.ExecutionSettings.Single(); + var executionSettings = promptTemplateConfig.ExecutionSettings.Single().Value; - var autoFunctionCallChoice = executionSettings.Value.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; - Assert.NotNull(autoFunctionCallChoice?.Functions); - Assert.Equal(12, autoFunctionCallChoice.MaximumAutoInvokeAttempts); + var autoFunctionCallChoice = executionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; + Assert.NotNull(autoFunctionCallChoice); + + Assert.NotNull(autoFunctionCallChoice.Functions); Assert.Equal("p1.f1", autoFunctionCallChoice.Functions.Single()); + + Assert.Equal(12, autoFunctionCallChoice.MaximumAutoInvokeAttempts); } [Fact] @@ -190,6 +193,7 @@ public void DeserializingRequiredFunctionCallingChoice() "function_choice_behavior": { "type": "required", "maximumAutoInvokeAttempts": 11, + "maximumUseAttempts": 2, "functions":["p1.f1"] } } @@ -204,14 +208,17 @@ public void DeserializingRequiredFunctionCallingChoice() Assert.NotNull(promptTemplateConfig); Assert.Single(promptTemplateConfig.ExecutionSettings); - var executionSettings = promptTemplateConfig.ExecutionSettings.Single(); + var executionSettings = promptTemplateConfig.ExecutionSettings.Single().Value; + Assert.NotNull(executionSettings); + + var requiredFunctionCallChoice = executionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; + Assert.NotNull(requiredFunctionCallChoice); - var requiredFunctionCallChoice = executionSettings.Value.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; - Assert.NotNull(requiredFunctionCallChoice?.Functions); + Assert.NotNull(requiredFunctionCallChoice.Functions); Assert.Equal("p1.f1", requiredFunctionCallChoice.Functions.Single()); Assert.Equal(11, requiredFunctionCallChoice.MaximumAutoInvokeAttempts); - Assert.Equal(1, requiredFunctionCallChoice.MaximumUseAttempts); + Assert.Equal(2, requiredFunctionCallChoice.MaximumUseAttempts); } [Fact] @@ -239,9 +246,9 @@ public void DeserializingNoneFunctionCallingChoice() Assert.NotNull(promptTemplateConfig); Assert.Single(promptTemplateConfig.ExecutionSettings); - var executionSettings = promptTemplateConfig.ExecutionSettings.Single(); + var executionSettings = promptTemplateConfig.ExecutionSettings.Single().Value; - Assert.IsType(executionSettings.Value.FunctionChoiceBehavior); + Assert.IsType(executionSettings.FunctionChoiceBehavior); } [Fact] From b6b5b032300fa5bf060c0ac99dc082209dee9f51 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 10 May 2024 09:33:27 +0100 Subject: [PATCH 58/90] fix: Address PR feedback --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 21 ++++++-------- .../Connectors/OpenAI/OpenAIFunctionsTests.cs | 29 +++++++++++++++---- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 84bde2a47a6a..05f1c7c59d48 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -455,7 +455,7 @@ internal async Task> GetChatMessageContentsAsy functionResult = invocationContext.Result; object functionResultValue = functionResult.GetValue() ?? string.Empty; - var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings); + var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); AddResponseMessage(chatOptions, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); @@ -703,7 +703,7 @@ internal async IAsyncEnumerable GetStreamingC functionResult = invocationContext.Result; object functionResultValue = functionResult.GetValue() ?? string.Empty; - var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings); + var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); AddResponseMessage(chatOptions, chat, streamedRole, toolCall, metadata, stringResult, errorMessage: null, this.Logger); @@ -1066,7 +1066,7 @@ private static IEnumerable GetRequestMessages(ChatMessageCon continue; } - var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, executionSettings); + var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, executionSettings.ToolCallBehavior); toolMessages.Add(new ChatRequestToolMessage(stringResult ?? string.Empty, resultContent.Id)); } @@ -1304,9 +1304,9 @@ private void CaptureUsageDetails(CompletionsUsage usage) /// Processes the function result. /// /// The result of the function call. - /// The prompt execution settings. + /// The ToolCallBehavior object containing optional settings like JsonSerializerOptions.TypeInfoResolver. /// A string representation of the function result. - private static string? ProcessFunctionResult(object functionResult, OpenAIPromptExecutionSettings promptExecutionSettings) + private static string? ProcessFunctionResult(object functionResult, ToolCallBehavior? toolCallBehavior) { if (functionResult is string stringResult) { @@ -1320,15 +1320,13 @@ private void CaptureUsageDetails(CompletionsUsage usage) return chatMessageContent.ToString(); } -#pragma warning disable CS0618 // Type or member is obsolete - var serializerOptions = promptExecutionSettings?.ToolCallBehavior?.ToolCallResultSerializerOptions; -#pragma warning restore CS0618 // Type or member is obsolete - // For polymorphic serialization of unknown in advance child classes of the KernelContent class, // a corresponding JsonTypeInfoResolver should be provided via the JsonSerializerOptions.TypeInfoResolver property. // For more details about the polymorphic serialization, see the article at: // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0 - return JsonSerializer.Serialize(functionResult, serializerOptions); +#pragma warning disable CS0618 // Type or member is obsolete + return JsonSerializer.Serialize(functionResult, toolCallBehavior?.ToolCallResultSerializerOptions); +#pragma warning restore CS0618 // Type or member is obsolete } /// @@ -1376,7 +1374,6 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context /// Execution settings for the completion API. /// The chat completion options from the Azure.AI.OpenAI package. /// Request sequence index of automatic function invocation process. - /// A tuple containing the AllowAnyRequestedKernelFunction and MaximumAutoInvokeAttempts settings. private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(Kernel? kernel, OpenAIPromptExecutionSettings executionSettings, ChatCompletionsOptions chatOptions, int requestIndex) { if (executionSettings.FunctionChoiceBehavior is not null && executionSettings.ToolCallBehavior is not null) @@ -1409,7 +1406,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context }; } - // Handling new tool behavior represented by `PromptExecutionSettings.ToolBehaviors` property. + // Handling new tool behavior represented by `PromptExecutionSettings.FunctionChoiceBehavior` property. if (executionSettings.FunctionChoiceBehavior is FunctionChoiceBehavior functionChoiceBehavior) { // Regenerate the tool list as necessary and getting other call behavior properties. The invocation of the function(s) could have augmented diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs index e6ed9634ba5a..fd652e3696f8 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs @@ -30,14 +30,13 @@ public async Task CanAutoInvokeKernelFunctionsAsync() var invokedFunctions = new List(); -#pragma warning disable CS0618 // Events are deprecated - void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e) + var filter = new FakeFunctionFilter(async (context, next) => { - invokedFunctions.Add(e.Function.Name); - } + invokedFunctions.Add(context.Function.Name); + await next(context); + }); - kernel.FunctionInvoking += MyInvokingHandler; -#pragma warning restore CS0618 // Events are deprecated + kernel.FunctionInvocationFilters.Add(filter); // Act OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; @@ -584,4 +583,22 @@ public class City public string Name { get; set; } = string.Empty; public string Country { get; set; } = string.Empty; } + + #region private + + private sealed class FakeFunctionFilter : IFunctionInvocationFilter + { + private readonly Func, Task>? _onFunctionInvocation; + + public FakeFunctionFilter( + Func, Task>? onFunctionInvocation = null) + { + this._onFunctionInvocation = onFunctionInvocation; + } + + public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) => + this._onFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask; + } + + #endregion } From e3e82b83f0311f97a940b33a843ec8722b41d096 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 10 May 2024 10:55:01 +0100 Subject: [PATCH 59/90] fix: improve exception message --- dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 05f1c7c59d48..70ec6afef67f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1378,7 +1378,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context { if (executionSettings.FunctionChoiceBehavior is not null && executionSettings.ToolCallBehavior is not null) { - throw new ArgumentException("ToolBehaviors and ToolCallBehavior cannot be used together."); + throw new ArgumentException($"{nameof(executionSettings.ToolCallBehavior)} and {nameof(executionSettings.FunctionChoiceBehavior)} cannot be used together."); } // Handling old-style tool call behavior represented by `OpenAIPromptExecutionSettings.ToolCallBehavior` property. From 58a5cb5fbc22dd6b24cc5818cf377f68579edb0d Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 10 May 2024 20:02:46 +0100 Subject: [PATCH 60/90] * Add tests for auto/required/none function choice behaviors * Fix issue in CLientCore when none tool choice specified in first request --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 184 +++--- .../OpenAIPromptExecutionSettings.cs | 2 + ...omptExecutionSettingsTypeConverterTests.cs | 59 +- .../OpenAIAutoFunctionChoiceBehaviorTests.cs | 427 +++++++++++++ .../Connectors/OpenAI/OpenAIFunctionsTests.cs | 604 ------------------ .../OpenAINoneFunctionChoiceBehaviorTests.cs | 259 ++++++++ ...enAIRequiredFunctionChoiceBehaviorTests.cs | 438 +++++++++++++ .../AI/PromptExecutionSettingsTests.cs | 8 +- 8 files changed, 1278 insertions(+), 703 deletions(-) create mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs delete mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 70ec6afef67f..44b3ad151deb 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -499,21 +499,7 @@ static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory c } // Update tool use information for the next go-around based on having completed another iteration. - Debug.Assert(functionCallConfiguration 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(); - - this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, requestIndex); - - // 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); - } + functionCallConfiguration = this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, requestIndex); // Disable auto invocation if we've exceeded the allowed limit. if (requestIndex >= functionCallConfiguration?.MaximumAutoInvokeAttempts) @@ -736,21 +722,7 @@ static void AddResponseMessage( } // Update tool use information for the next go-around based on having completed another iteration. - Debug.Assert(functionCallConfiguration 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(); - - this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, requestIndex); - - // 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); - } + functionCallConfiguration = this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, requestIndex); // Disable auto invocation if we've exceeded the allowed limit. if (requestIndex >= functionCallConfiguration?.MaximumAutoInvokeAttempts) @@ -1381,95 +1353,113 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context throw new ArgumentException($"{nameof(executionSettings.ToolCallBehavior)} and {nameof(executionSettings.FunctionChoiceBehavior)} cannot be used together."); } + // Set the tool choice to none. If we end up wanting to use tools, we'll set it to the desired value. + chatOptions.ToolChoice = ChatCompletionsToolChoice.None; + chatOptions.Tools.Clear(); + + (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? result = null; + + // Handling new tool behavior represented by `PromptExecutionSettings.FunctionChoiceBehavior` property. + if (executionSettings.FunctionChoiceBehavior is { } functionChoiceBehavior) + { + result = this.ConfigureFunctionCallingFromFunctionChoiceBehavior(kernel, chatOptions, requestIndex, functionChoiceBehavior); + } // Handling old-style tool call behavior represented by `OpenAIPromptExecutionSettings.ToolCallBehavior` property. - if (executionSettings.ToolCallBehavior is { } toolCallBehavior) + else if (executionSettings.ToolCallBehavior is { } toolCallBehavior) { - if (requestIndex >= 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 tools.", 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. - toolCallBehavior.ConfigureOptions(kernel, chatOptions); - } + result = this.ConfigureFunctionCallingFromToolCallBehavior(kernel, chatOptions, requestIndex, toolCallBehavior); + } - return new() - { - AllowAnyRequestedKernelFunction = toolCallBehavior.AllowAnyRequestedKernelFunction, - MaximumAutoInvokeAttempts = toolCallBehavior.MaximumAutoInvokeAttempts, - }; + // 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. + // Similarly, if we say not to use any tool (ToolChoice = ChatCompletionsToolChoice.None) for the first request, + // the service fails with "'tool_choice' is only allowed when 'tools' are specified." + if (chatOptions.ToolChoice == ChatCompletionsToolChoice.None) + { + Debug.Assert(chatOptions.Tools.Count == 0); + chatOptions.Tools.Add(s_nonInvocableFunctionTool); } - // Handling new tool behavior represented by `PromptExecutionSettings.FunctionChoiceBehavior` property. - if (executionSettings.FunctionChoiceBehavior is FunctionChoiceBehavior functionChoiceBehavior) + return result; + } + + private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCallingFromFunctionChoiceBehavior(Kernel? kernel, ChatCompletionsOptions chatOptions, int requestIndex, FunctionChoiceBehavior functionChoiceBehavior) + { + // Regenerate the tool list as necessary and getting other call behavior properties. The invocation of the function(s) could have augmented + // what functions are available in the kernel. + var config = functionChoiceBehavior.GetConfiguration(new() { Kernel = kernel }); + + (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts) result = new() { - // Regenerate the tool list as necessary and getting other call behavior properties. The invocation of the function(s) could have augmented - // what functions are available in the kernel. - var config = functionChoiceBehavior.GetConfiguration(new() { Kernel = kernel }); - if (config is null) + AllowAnyRequestedKernelFunction = config.AllowAnyRequestedKernelFunction, + MaximumAutoInvokeAttempts = config.MaximumAutoInvokeAttempts, + }; + + if (requestIndex >= config.MaximumUseAttempts) + { + // Don't add any tools as we've reached the maximum use attempts limit. + if (this.Logger.IsEnabled(LogLevel.Debug)) { - return null; + this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the functions.", config.MaximumUseAttempts); } - (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts) result = new() - { - AllowAnyRequestedKernelFunction = config.AllowAnyRequestedKernelFunction, - MaximumAutoInvokeAttempts = config.MaximumAutoInvokeAttempts, - }; + return result; + } - if (requestIndex >= config.MaximumUseAttempts) + // If we have a required function, it means we want to force LLM to invoke that function. + if (config.RequiredFunctions is { } requiredFunctions && requiredFunctions.Any()) + { + if (requiredFunctions.Count() > 1) { - // Don't add any tools as we've reached the maximum use attempts limit. - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the functions.", config.MaximumUseAttempts); - } - - return result; + throw new KernelException("Only one required function is allowed."); } - // If we have a required function, it means we want to force LLM to invoke that function. - if (config.RequiredFunctions is { } requiredFunctions && requiredFunctions.Any()) - { - if (requiredFunctions.Count() > 1) - { - throw new KernelException("Only one required function is allowed."); - } + var functionDefinition = requiredFunctions.First().Metadata.ToOpenAIFunction().ToFunctionDefinition(); - var functionDefinition = requiredFunctions.First().Metadata.ToOpenAIFunction().ToFunctionDefinition(); + chatOptions.ToolChoice = new ChatCompletionsToolChoice(functionDefinition); + chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); - chatOptions.ToolChoice = new ChatCompletionsToolChoice(functionDefinition); - chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); + return result; + } - return result; - } + // If we have available functions, we want LLM to choose which function(s) to call. + if (config.AvailableFunctions is { } availableFunctions && availableFunctions.Any()) + { + chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; - // If we have available functions, we want LLM to choose which function(s) to call. - if (config.AvailableFunctions is { } availableFunctions && availableFunctions.Any()) + foreach (var function in availableFunctions) { - chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; - - foreach (var function in availableFunctions) - { - var functionDefinition = function.Metadata.ToOpenAIFunction().ToFunctionDefinition(); - chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); - } - - return result; + var functionDefinition = function.Metadata.ToOpenAIFunction().ToFunctionDefinition(); + chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); } - // If we have neither required nor available functions, we don't want LLM to call any functions. - chatOptions.ToolChoice = ChatCompletionsToolChoice.None; - return result; } - return null; + return result; + } + + private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCallingFromToolCallBehavior(Kernel? kernel, ChatCompletionsOptions chatOptions, int requestIndex, ToolCallBehavior toolCallBehavior) + { + if (requestIndex >= 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 tools.", 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. + toolCallBehavior.ConfigureOptions(kernel, chatOptions); + } + + return new() + { + AllowAnyRequestedKernelFunction = toolCallBehavior.AllowAnyRequestedKernelFunction, + MaximumAutoInvokeAttempts = toolCallBehavior.MaximumAutoInvokeAttempts, + }; } } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs index d682c3f2e237..25652674aa9b 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs @@ -331,6 +331,8 @@ public static OpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutio var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); if (openAIExecutionSettings is not null) { + // Restores the original function choice behavior that lost internal state(list of functions) during serialization/deserialization process. + openAIExecutionSettings.FunctionChoiceBehavior = executionSettings.FunctionChoiceBehavior; return openAIExecutionSettings; } diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs index 1c4c1d9cfe03..ac53ff3d9e3c 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using Microsoft.SemanticKernel; using Xunit; using YamlDotNet.Serialization; @@ -30,9 +31,44 @@ public void ItShouldCreatePromptFunctionFromYamlWithCustomModelSettings() Assert.Equal("Say hello to the specified person using the specified language", semanticFunctionConfig.Description); Assert.Equal(2, semanticFunctionConfig.InputVariables.Count); Assert.Equal("language", semanticFunctionConfig.InputVariables[1].Name); - Assert.Equal(2, semanticFunctionConfig.ExecutionSettings.Count); + Assert.Equal(3, semanticFunctionConfig.ExecutionSettings.Count); Assert.Equal("gpt-4", semanticFunctionConfig.ExecutionSettings["service1"].ModelId); Assert.Equal("gpt-3.5", semanticFunctionConfig.ExecutionSettings["service2"].ModelId); + Assert.Equal("gpt-3.5-turbo", semanticFunctionConfig.ExecutionSettings["service3"].ModelId); + } + + [Fact] + public void ItShouldDeserializeFunctionChoiceBehaviors() + { + // Act + var promptTemplateConfig = KernelFunctionYaml.ToPromptTemplateConfig(this._yaml); + + // Assert + Assert.NotNull(promptTemplateConfig?.ExecutionSettings); + Assert.Equal(3, promptTemplateConfig.ExecutionSettings.Count); + + // Service with auto function choice behavior + var service1ExecutionSettings = promptTemplateConfig.ExecutionSettings["service1"]; + + var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; + Assert.NotNull(autoFunctionChoiceBehavior?.Functions); + Assert.Equal("p1.f1", autoFunctionChoiceBehavior.Functions.Single()); + Assert.Equal(9, autoFunctionChoiceBehavior.MaximumAutoInvokeAttempts); + + // Service with required function choice behavior + var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; + + var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; + Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); + Assert.Equal("p2.f2", requiredFunctionChoiceBehavior.Functions.Single()); + Assert.Equal(6, requiredFunctionChoiceBehavior.MaximumAutoInvokeAttempts); + Assert.Equal(3, requiredFunctionChoiceBehavior.MaximumUseAttempts); + + // Service with none function choice behavior + var service3ExecutionSettings = promptTemplateConfig.ExecutionSettings["service3"]; + + var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; + Assert.NotNull(noneFunctionChoiceBehavior); } private readonly string _yaml = """ @@ -56,6 +92,11 @@ public void ItShouldCreatePromptFunctionFromYamlWithCustomModelSettings() frequency_penalty: 0.0 max_tokens: 256 stop_sequences: [] + function_choice_behavior: + type: auto + maximum_auto_invoke_attempts: 9 + functions: + - p1.f1 service2: model_id: gpt-3.5 temperature: 1.0 @@ -64,5 +105,21 @@ public void ItShouldCreatePromptFunctionFromYamlWithCustomModelSettings() frequency_penalty: 0.0 max_tokens: 256 stop_sequences: [ "foo", "bar", "baz" ] + function_choice_behavior: + type: required + maximum_auto_invoke_attempts: 6 + maximum_use_attempts: 3 + functions: + - p2.f2 + service3: + model_id: gpt-3.5-turbo + temperature: 1.0 + top_p: 0.0 + presence_penalty: 0.0 + frequency_penalty: 0.0 + max_tokens: 256 + stop_sequences: [ "foo", "bar", "baz" ] + function_choice_behavior: + type: none """; } diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs new file mode 100644 index 000000000000..636f64818e00 --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs @@ -0,0 +1,427 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Configuration; + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using SemanticKernel.IntegrationTests.Planners.Stepwise; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; + +public sealed class OpenAIAutoFunctionChoiceBehaviorTests : BaseIntegrationTest +{ + private readonly Kernel _kernel; + private readonly FakeFunctionFilter _autoFunctionInvocationFilter; + + public OpenAIAutoFunctionChoiceBehaviorTests() + { + this._autoFunctionInvocationFilter = new FakeFunctionFilter(); + + this._kernel = this.InitializeKernel(); + this._kernel.AutoFunctionInvocationFilters.Add(this._autoFunctionInvocationFilter); + } + + [Fact] + public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomaticallyAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + // Act + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: true) }; + + var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); + + // Assert + Assert.NotNull(result); + + Assert.Single(invokedFunctions); + Assert.Contains("GetCurrentDate", invokedFunctions); + } + + [Fact] + public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionAutomaticallyAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var promptTemplate = """" + template_format: semantic-kernel + template: How many days until Christmas? + execution_settings: + default: + temperature: 0.1 + function_choice_behavior: + type: auto + maximum_auto_invoke_attempts: 3 + """"; + + var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); + + // Act + var result = await this._kernel.InvokeAsync(promptFunction); + + // Assert + Assert.NotNull(result); + + Assert.Single(invokedFunctions); + Assert.Contains("GetCurrentDate", invokedFunctions); + } + + [Fact] + public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuallyAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + // Act + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; + + var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); + + // Assert + Assert.NotNull(result); + + Assert.Empty(invokedFunctions); + + var responseContent = result.GetValue(); + Assert.NotNull(responseContent); + + var functionCalls = FunctionCallContent.GetFunctionCalls(responseContent); + Assert.NotNull(functionCalls); + Assert.Single(functionCalls); + + var functionCall = functionCalls.First(); + Assert.Equal("DateTimeUtils", functionCall.PluginName); + Assert.Equal("GetCurrentDate", functionCall.FunctionName); + } + + [Fact] + public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionManuallyAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var promptTemplate = """" + template_format: semantic-kernel + template: How many days until Christmas? + execution_settings: + default: + temperature: 0.1 + function_choice_behavior: + type: auto + maximum_auto_invoke_attempts: 0 + """"; + + var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); + + // Act + var result = await this._kernel.InvokeAsync(promptFunction); + + // Assert + Assert.NotNull(result); + + Assert.Empty(invokedFunctions); + + var responseContent = result.GetValue(); + Assert.NotNull(responseContent); + + var functionCalls = FunctionCallContent.GetFunctionCalls(responseContent); + Assert.NotNull(functionCalls); + Assert.Single(functionCalls); + + var functionCall = functionCalls.First(); + Assert.Equal("DateTimeUtils", functionCall.PluginName); + Assert.Equal("GetCurrentDate", functionCall.FunctionName); + } + + [Fact] + public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomaticallyForStreamingAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: true) }; + + string result = ""; + + // Act + await foreach (string c in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) + { + result += c; + } + + // Assert + Assert.NotNull(result); + + Assert.Single(invokedFunctions); + Assert.Contains("GetCurrentDate", invokedFunctions); + } + + [Fact] + public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionAutomaticallyForStreamingAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var promptTemplate = """" + template_format: semantic-kernel + template: How many days until Christmas? + execution_settings: + default: + temperature: 0.1 + function_choice_behavior: + type: auto + maximum_auto_invoke_attempts: 3 + """"; + + var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); + + string result = ""; + + // Act + await foreach (string c in promptFunction.InvokeStreamingAsync(this._kernel)) + { + result += c; + } + + // Assert + Assert.NotNull(result); + + Assert.Single(invokedFunctions); + Assert.Contains("GetCurrentDate", invokedFunctions); + } + + [Fact] + public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuallyForStreamingAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var functionsForManualInvocation = new List(); + + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; + + // Act + await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) + { + if (content.ToolCallUpdate is StreamingFunctionToolCallUpdate functionUpdate && !string.IsNullOrEmpty(functionUpdate.Name)) + { + functionsForManualInvocation.Add(functionUpdate.Name); + } + } + + // Assert + Assert.Single(functionsForManualInvocation); + Assert.Contains("DateTimeUtils-GetCurrentDate", functionsForManualInvocation); + + Assert.Empty(invokedFunctions); + } + + [Fact] + public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionManuallyForStreamingAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var promptTemplate = """" + template_format: semantic-kernel + template: How many days until Christmas? + execution_settings: + default: + temperature: 0.1 + function_choice_behavior: + type: auto + maximum_auto_invoke_attempts: 0 + """"; + + var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); + + var functionsForManualInvocation = new List(); + + // Act + await foreach (var content in promptFunction.InvokeStreamingAsync(this._kernel)) + { + if (content.ToolCallUpdate is StreamingFunctionToolCallUpdate functionUpdate && !string.IsNullOrEmpty(functionUpdate.Name)) + { + functionsForManualInvocation.Add(functionUpdate.Name); + } + } + + // Assert + Assert.Single(functionsForManualInvocation); + Assert.Contains("DateTimeUtils-GetCurrentDate", functionsForManualInvocation); + + Assert.Empty(invokedFunctions); + } + + private Kernel InitializeKernel() + { + OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("Planners:OpenAI").Get(); + Assert.NotNull(openAIConfiguration); + + IKernelBuilder builder = this.CreateKernelBuilder() + .AddOpenAIChatCompletion( + modelId: openAIConfiguration.ModelId, + apiKey: openAIConfiguration.ApiKey); + + return builder.Build(); + } + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + /// + /// A plugin that returns the current time. + /// + public class DateTimeUtils + { + [KernelFunction] + [Description("Retrieves the current time in UTC.")] + public string GetCurrentUtcTime() => DateTime.UtcNow.ToString("R"); + + [KernelFunction] + [Description("Retrieves the current date.")] + public string GetCurrentDate() => DateTime.UtcNow.ToString("d", CultureInfo.InvariantCulture); + } + + public class WeatherPlugin + { + [KernelFunction, Description("Get current temperature.")] + public Task GetCurrentTemperatureAsync(WeatherParameters parameters) + { + if (parameters.City.Name == "Dublin" && (parameters.City.Country == "Ireland" || parameters.City.Country == "IE")) + { + return Task.FromResult(42.8); // 42.8 Fahrenheit. + } + + throw new NotSupportedException($"Weather in {parameters.City.Name} ({parameters.City.Country}) is not supported."); + } + + [KernelFunction, Description("Convert temperature from Fahrenheit to Celsius.")] + public Task ConvertTemperatureAsync(double temperatureInFahrenheit) + { + double temperatureInCelsius = (temperatureInFahrenheit - 32) * 5 / 9; + return Task.FromResult(temperatureInCelsius); + } + + [KernelFunction, Description("Get the current weather for the specified city.")] + public Task GetWeatherForCityAsync(string cityName) + { + return Task.FromResult(cityName switch + { + "Boston" => "61 and rainy", + _ => "31 and snowing", + }); + } + } + + public record WeatherParameters(City City); + + public class City + { + public string Name { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + } + + #region private + + private sealed class FakeFunctionFilter : IAutoFunctionInvocationFilter + { + private Func, Task>? _onFunctionInvocation; + + public void RegisterFunctionInvocationHandler(Func, Task> onFunctionInvocation) + { + this._onFunctionInvocation = onFunctionInvocation; + } + + public Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + if (this._onFunctionInvocation is null) + { + return next(context); + } + + return this._onFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask; + } + } + + #endregion +} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs deleted file mode 100644 index fd652e3696f8..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFunctionsTests.cs +++ /dev/null @@ -1,604 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Configuration; - -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using SemanticKernel.IntegrationTests.Planners.Stepwise; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; - -public sealed class OpenAIFunctionsTests : BaseIntegrationTest -{ - [Fact] - public async Task CanAutoInvokeKernelFunctionsAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - kernel.ImportPluginFromType(); - - var invokedFunctions = new List(); - - var filter = new FakeFunctionFilter(async (context, next) => - { - invokedFunctions.Add(context.Function.Name); - await next(context); - }); - - kernel.FunctionInvocationFilters.Add(filter); - - // Act - OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; - var result = await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.", new(settings)); - - // Assert - Assert.NotNull(result); - Assert.Contains("GetCurrentUtcTime", invokedFunctions); - } - - [Fact] - public async Task CanAutoInvokeKernelFunctionsStreamingAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - kernel.ImportPluginFromType(); - - var invokedFunctions = new List(); - -#pragma warning disable CS0618 // Events are deprecated - void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e) - { - invokedFunctions.Add($"{e.Function.Name}({string.Join(", ", e.Arguments)})"); - } - - kernel.FunctionInvoking += MyInvokingHandler; -#pragma warning restore CS0618 // Events are deprecated - - // Act - OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; - string result = ""; - await foreach (string c in kernel.InvokePromptStreamingAsync( - $"How much older is John than Jim? Compute that value and pass it to the {nameof(TimeInformation)}.{nameof(TimeInformation.InterpretValue)} function, then respond only with its result.", - new(settings))) - { - result += c; - } - - // Assert - Assert.Contains("6", result, StringComparison.InvariantCulture); - Assert.Contains("GetAge([personName, John])", invokedFunctions); - Assert.Contains("GetAge([personName, Jim])", invokedFunctions); - Assert.Contains("InterpretValue([value, 3])", invokedFunctions); - } - - [Fact] - public async Task CanAutoInvokeKernelFunctionsWithComplexTypeParametersAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - kernel.ImportPluginFromType(); - - // Act - OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; - var result = await kernel.InvokePromptAsync("What is the current temperature in Dublin, Ireland, in Fahrenheit?", new(settings)); - - // Assert - Assert.NotNull(result); - Assert.Contains("42.8", result.GetValue(), StringComparison.InvariantCulture); // The WeatherPlugin always returns 42.8 for Dublin, Ireland. - } - - [Fact] - public async Task CanAutoInvokeKernelFunctionsWithPrimitiveTypeParametersAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - kernel.ImportPluginFromType(); - - // Act - OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; - - var result = await kernel.InvokePromptAsync("Convert 50 degrees Fahrenheit to Celsius.", new(settings)); - - // Assert - Assert.NotNull(result); - Assert.Contains("10", result.GetValue(), StringComparison.InvariantCulture); - } - - [Fact] - public async Task CanAutoInvokeKernelFunctionFromPromptAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - - var promptFunction = KernelFunctionFactory.CreateFromPrompt( - "Your role is always to return this text - 'A Game-Changer for the Transportation Industry'. Don't ask for more details or context.", - functionName: "FindLatestNews", - description: "Searches for the latest news."); - - kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions( - "NewsProvider", - "Delivers up-to-date news content.", - [promptFunction])); - - // Act - OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; - var result = await kernel.InvokePromptAsync("Show me the latest news as they are.", new(settings)); - - // Assert - Assert.NotNull(result); - Assert.Contains("Transportation", result.GetValue(), StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task CanAutoInvokeKernelFunctionFromPromptStreamingAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - - var promptFunction = KernelFunctionFactory.CreateFromPrompt( - "Your role is always to return this text - 'A Game-Changer for the Transportation Industry'. Don't ask for more details or context.", - functionName: "FindLatestNews", - description: "Searches for the latest news."); - - kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions( - "NewsProvider", - "Delivers up-to-date news content.", - [promptFunction])); - - // Act - OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; - var streamingResult = kernel.InvokePromptStreamingAsync("Show me the latest news as they are.", new(settings)); - - var builder = new StringBuilder(); - - await foreach (var update in streamingResult) - { - builder.Append(update.ToString()); - } - - var result = builder.ToString(); - - // Assert - Assert.NotNull(result); - Assert.Contains("Transportation", result, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task ConnectorSpecificChatMessageContentClassesCanBeUsedForManualFunctionCallingAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; - - var sut = kernel.GetRequiredService(); - - // Act - var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - - // Current way of handling function calls manually using connector specific chat message content class. - var toolCalls = ((OpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); - - while (toolCalls.Count > 0) - { - // Adding LLM function call request to chat history - chatHistory.Add(result); - - // Iterating over the requested function calls and invoking them - foreach (var toolCall in toolCalls) - { - string content = kernel.Plugins.TryGetFunctionAndArguments(toolCall, out KernelFunction? function, out KernelArguments? arguments) ? - JsonSerializer.Serialize((await function.InvokeAsync(kernel, arguments)).GetValue()) : - "Unable to find function. Please try again!"; - - // Adding the result of the function call to the chat history - chatHistory.Add(new ChatMessageContent( - AuthorRole.Tool, - content, - metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } })); - } - - // Sending the functions invocation results back to the LLM to get the final response - result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - toolCalls = ((OpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); - } - - // Assert - Assert.Contains("rain", result.Content, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManualFunctionCallingAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; - - var sut = kernel.GetRequiredService(); - - // Act - var messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - - var functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); - - while (functionCalls.Length != 0) - { - // Adding function call request from LLM to chat history - chatHistory.Add(messageContent); - - // Iterating over the requested function calls and invoking them - foreach (var functionCall in functionCalls) - { - var result = await functionCall.InvokeAsync(kernel); - - chatHistory.Add(result.ToChatMessage()); - } - - // Sending the functions invocation results to the LLM to get the final response - messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); - } - - // Assert - Assert.Contains("rain", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExceptionToConnectorAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddSystemMessage("If you are unable to answer the question for whatever reason, please add the 'error' keyword to the response."); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; - - var completionService = kernel.GetRequiredService(); - - // Act - var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - - var functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); - - while (functionCalls.Length != 0) - { - // Adding function call request from LLM to chat history - chatHistory.Add(messageContent); - - // Iterating over the requested function calls and invoking them - foreach (var functionCall in functionCalls) - { - // Simulating an exception - var exception = new OperationCanceledException("The operation was canceled due to timeout."); - - chatHistory.Add(new FunctionResultContent(functionCall, exception).ToChatMessage()); - } - - // Sending the functions execution results back to the LLM to get the final response - messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); - } - - // Assert - Assert.NotNull(messageContent.Content); - - Assert.Contains("error", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFunctionCallsAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddSystemMessage("if there's a tornado warning, please add the 'tornado' keyword to the response."); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; - - var completionService = kernel.GetRequiredService(); - - // Act - var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - - var functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); - - while (functionCalls.Length > 0) - { - // Adding function call request from LLM to chat history - chatHistory.Add(messageContent); - - // Iterating over the requested function calls and invoking them - foreach (var functionCall in functionCalls) - { - var result = await functionCall.InvokeAsync(kernel); - - chatHistory.AddMessage(AuthorRole.Tool, new ChatMessageContentItemCollection() { result }); - } - - // Adding a simulated function call to the connector response message - var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); - messageContent.Items.Add(simulatedFunctionCall); - - // Adding a simulated function result to chat history - var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; - chatHistory.Add(new FunctionResultContent(simulatedFunctionCall, simulatedFunctionResult).ToChatMessage()); - - // Sending the functions invocation results back to the LLM to get the final response - messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); - } - - // Assert - Assert.Contains("tornado", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task ItFailsIfNoFunctionResultProvidedAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; - - var completionService = kernel.GetRequiredService(); - - // Act - var result = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - - chatHistory.Add(result); - - var exception = await Assert.ThrowsAsync(() => completionService.GetChatMessageContentAsync(chatHistory, settings, kernel)); - - // Assert - Assert.Contains("'tool_calls' must be followed by tool", exception.Message, StringComparison.InvariantCulture); - } - - [Fact] - public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFunctionCallingAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; - - var sut = kernel.GetRequiredService(); - - // Act - await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - - // Assert - Assert.Equal(5, chatHistory.Count); - - var userMessage = chatHistory[0]; - Assert.Equal(AuthorRole.User, userMessage.Role); - - // LLM requested the current time. - var getCurrentTimeFunctionCallMessage = chatHistory[1]; - Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallMessage.Role); - - var getCurrentTimeFunctionCall = getCurrentTimeFunctionCallMessage.Items.OfType().Single(); - Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCall.FunctionName); - Assert.Equal("HelperFunctions", getCurrentTimeFunctionCall.PluginName); - Assert.NotNull(getCurrentTimeFunctionCall.Id); - - // Connector invoked the GetCurrentUtcTime function and added result to chat history. - var getCurrentTimeFunctionResultMessage = chatHistory[2]; - Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionResultMessage.Role); - Assert.Single(getCurrentTimeFunctionResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. - - var getCurrentTimeFunctionResult = getCurrentTimeFunctionResultMessage.Items.OfType().Single(); - Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionResult.FunctionName); - Assert.Equal("HelperFunctions", getCurrentTimeFunctionResult.PluginName); - Assert.Equal(getCurrentTimeFunctionCall.Id, getCurrentTimeFunctionResult.Id); - Assert.NotNull(getCurrentTimeFunctionResult.Result); - - // LLM requested the weather for Boston. - var getWeatherForCityFunctionCallMessage = chatHistory[3]; - Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallMessage.Role); - - var getWeatherForCityFunctionCall = getWeatherForCityFunctionCallMessage.Items.OfType().Single(); - Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCall.FunctionName); - Assert.Equal("HelperFunctions", getWeatherForCityFunctionCall.PluginName); - Assert.NotNull(getWeatherForCityFunctionCall.Id); - - // Connector invoked the Get_Weather_For_City function and added result to chat history. - var getWeatherForCityFunctionResultMessage = chatHistory[4]; - Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionResultMessage.Role); - Assert.Single(getWeatherForCityFunctionResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. - - var getWeatherForCityFunctionResult = getWeatherForCityFunctionResultMessage.Items.OfType().Single(); - Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionResult.FunctionName); - Assert.Equal("HelperFunctions", getWeatherForCityFunctionResult.PluginName); - Assert.Equal(getWeatherForCityFunctionCall.Id, getWeatherForCityFunctionResult.Id); - Assert.NotNull(getWeatherForCityFunctionResult.Result); - } - - [Fact] - public async Task SubsetOfFunctionsCanBeUsedForFunctionCallingAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: false); - - var function = kernel.CreateFunctionFromMethod(() => DayOfWeek.Friday, "GetDayOfWeek", "Retrieves the current day of the week."); - kernel.ImportPluginFromFunctions("HelperFunctions", [function]); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("What day is today?"); - - var settings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice([function], autoInvoke: true) }; - - var sut = kernel.GetRequiredService(); - - // Act - var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - - // Assert - Assert.NotNull(result); - Assert.Contains("Friday", result.Content, StringComparison.InvariantCulture); - } - - [Fact] - public async Task RequiredFunctionShouldBeCalledAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: false); - - var function = kernel.CreateFunctionFromMethod(() => DayOfWeek.Friday, "GetDayOfWeek", "Retrieves the current day of the week."); - kernel.ImportPluginFromFunctions("HelperFunctions", [function]); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("What day is today?"); - - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([function], true) }; - - var sut = kernel.GetRequiredService(); - - // Act - var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - - // Assert - Assert.NotNull(result); - Assert.Contains("Friday", result.Content, StringComparison.InvariantCulture); - } - - private Kernel InitializeKernel(bool importHelperPlugin = false) - { - OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("Planners:OpenAI").Get(); - Assert.NotNull(openAIConfiguration); - - IKernelBuilder builder = this.CreateKernelBuilder() - .AddOpenAIChatCompletion( - modelId: openAIConfiguration.ModelId, - apiKey: openAIConfiguration.ApiKey); - - var kernel = builder.Build(); - - if (importHelperPlugin) - { - kernel.ImportPluginFromFunctions("HelperFunctions", new[] - { - kernel.CreateFunctionFromMethod(() => DateTime.UtcNow.ToString("R"), "GetCurrentUtcTime", "Retrieves the current time in UTC."), - kernel.CreateFunctionFromMethod((string cityName) => - cityName switch - { - "Boston" => "61 and rainy", - _ => "31 and snowing", - }, "Get_Weather_For_City", "Gets the current weather for the specified city"), - }); - } - - return kernel; - } - - private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - /// - /// A plugin that returns the current time. - /// - public class TimeInformation - { - [KernelFunction] - [Description("Retrieves the current time in UTC.")] - public string GetCurrentUtcTime() => DateTime.UtcNow.ToString("R"); - - [KernelFunction] - [Description("Gets the age of the specified person.")] - public int GetAge(string personName) - { - if ("John".Equals(personName, StringComparison.OrdinalIgnoreCase)) - { - return 33; - } - - if ("Jim".Equals(personName, StringComparison.OrdinalIgnoreCase)) - { - return 30; - } - - return -1; - } - - [KernelFunction] - public int InterpretValue(int value) => value * 2; - } - - public class WeatherPlugin - { - [KernelFunction, Description("Get current temperature.")] - public Task GetCurrentTemperatureAsync(WeatherParameters parameters) - { - if (parameters.City.Name == "Dublin" && (parameters.City.Country == "Ireland" || parameters.City.Country == "IE")) - { - return Task.FromResult(42.8); // 42.8 Fahrenheit. - } - - throw new NotSupportedException($"Weather in {parameters.City.Name} ({parameters.City.Country}) is not supported."); - } - - [KernelFunction, Description("Convert temperature from Fahrenheit to Celsius.")] - public Task ConvertTemperatureAsync(double temperatureInFahrenheit) - { - double temperatureInCelsius = (temperatureInFahrenheit - 32) * 5 / 9; - return Task.FromResult(temperatureInCelsius); - } - } - - public record WeatherParameters(City City); - - public class City - { - public string Name { get; set; } = string.Empty; - public string Country { get; set; } = string.Empty; - } - - #region private - - private sealed class FakeFunctionFilter : IFunctionInvocationFilter - { - private readonly Func, Task>? _onFunctionInvocation; - - public FakeFunctionFilter( - Func, Task>? onFunctionInvocation = null) - { - this._onFunctionInvocation = onFunctionInvocation; - } - - public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) => - this._onFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask; - } - - #endregion -} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs new file mode 100644 index 000000000000..d559736af37e --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; + +using Microsoft.SemanticKernel; +using SemanticKernel.IntegrationTests.Planners.Stepwise; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; + +public sealed class OpenAINoneFunctionChoiceBehaviorTests : BaseIntegrationTest +{ + private readonly Kernel _kernel; + private readonly FakeFunctionFilter _autoFunctionInvocationFilter; + + public OpenAINoneFunctionChoiceBehaviorTests() + { + this._autoFunctionInvocationFilter = new FakeFunctionFilter(); + + this._kernel = this.InitializeKernel(); + this._kernel.AutoFunctionInvocationFilters.Add(this._autoFunctionInvocationFilter); + } + + [Fact] + public async Task SpecifiedInCodeInstructsConnectorNotToInvokeKernelFunctionAsync() + { + // Arrange + var plugin = this._kernel.CreatePluginFromType(); + this._kernel.Plugins.Add(plugin); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + // Act + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.None }; + + var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); + + // Assert + Assert.NotNull(result); + + Assert.Empty(invokedFunctions); + } + + [Fact] + public async Task SpecifiedInPromptInstructsConnectorNotToInvokeKernelFunctionAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var promptTemplate = """" + template_format: semantic-kernel + template: How many days until Christmas? + execution_settings: + default: + temperature: 0.1 + function_choice_behavior: + type: none + maximum_auto_invoke_attempts: 3 + """"; + + var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); + + // Act + var result = await this._kernel.InvokeAsync(promptFunction); + + // Assert + Assert.NotNull(result); + + Assert.Empty(invokedFunctions); + } + + [Fact] + public async Task SpecifiedInCodeInstructsConnectorNotToInvokeKernelFunctionForStreamingAsync() + { + // Arrange + var plugin = this._kernel.CreatePluginFromType(); + this._kernel.Plugins.Add(plugin); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.None }; + + string result = ""; + + // Act + await foreach (string c in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) + { + result += c; + } + + // Assert + Assert.NotNull(result); + + Assert.Empty(invokedFunctions); + } + + [Fact] + public async Task SpecifiedInPromptInstructsConnectorNotToInvokeKernelFunctionForStreamingAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var promptTemplate = """" + template_format: semantic-kernel + template: How many days until Christmas? + execution_settings: + default: + temperature: 0.1 + function_choice_behavior: + type: none + """"; + + var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); + + string result = ""; + + // Act + await foreach (string c in promptFunction.InvokeStreamingAsync(this._kernel)) + { + result += c; + } + + // Assert + Assert.NotNull(result); + + Assert.Empty(invokedFunctions); + } + + private Kernel InitializeKernel() + { + OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("Planners:OpenAI").Get(); + Assert.NotNull(openAIConfiguration); + + IKernelBuilder builder = this.CreateKernelBuilder() + .AddOpenAIChatCompletion( + modelId: openAIConfiguration.ModelId, + apiKey: openAIConfiguration.ApiKey); + + return builder.Build(); + } + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + /// + /// A plugin that returns the current time. + /// + public class DateTimeUtils + { + [KernelFunction] + [Description("Retrieves the current time in UTC.")] + public string GetCurrentUtcTime() => DateTime.UtcNow.ToString("R"); + + [KernelFunction] + [Description("Retrieves the current date.")] + public string GetCurrentDate() => DateTime.UtcNow.ToString("d", CultureInfo.InvariantCulture); + } + + public class WeatherPlugin + { + [KernelFunction, Description("Get current temperature.")] + public Task GetCurrentTemperatureAsync(WeatherParameters parameters) + { + if (parameters.City.Name == "Dublin" && (parameters.City.Country == "Ireland" || parameters.City.Country == "IE")) + { + return Task.FromResult(42.8); // 42.8 Fahrenheit. + } + + throw new NotSupportedException($"Weather in {parameters.City.Name} ({parameters.City.Country}) is not supported."); + } + + [KernelFunction, Description("Convert temperature from Fahrenheit to Celsius.")] + public Task ConvertTemperatureAsync(double temperatureInFahrenheit) + { + double temperatureInCelsius = (temperatureInFahrenheit - 32) * 5 / 9; + return Task.FromResult(temperatureInCelsius); + } + + [KernelFunction, Description("Get the current weather for the specified city.")] + public Task GetWeatherForCityAsync(string cityName) + { + return Task.FromResult(cityName switch + { + "Boston" => "61 and rainy", + _ => "31 and snowing", + }); + } + } + + public record WeatherParameters(City City); + + public class City + { + public string Name { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + } + + #region private + + private sealed class FakeFunctionFilter : IAutoFunctionInvocationFilter + { + private Func, Task>? _onFunctionInvocation; + + public void RegisterFunctionInvocationHandler(Func, Task> onFunctionInvocation) + { + this._onFunctionInvocation = onFunctionInvocation; + } + + public Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + if (this._onFunctionInvocation is null) + { + return next(context); + } + + return this._onFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask; + } + } + + #endregion +} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs new file mode 100644 index 000000000000..6f51e2d78c14 --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs @@ -0,0 +1,438 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using SemanticKernel.IntegrationTests.Planners.Stepwise; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; + +public sealed class OpenAIRequiredFunctionChoiceBehaviorTests : BaseIntegrationTest +{ + private readonly Kernel _kernel; + private readonly FakeFunctionFilter _autoFunctionInvocationFilter; + + public OpenAIRequiredFunctionChoiceBehaviorTests() + { + this._autoFunctionInvocationFilter = new FakeFunctionFilter(); + + this._kernel = this.InitializeKernel(); + this._kernel.AutoFunctionInvocationFilters.Add(this._autoFunctionInvocationFilter); + } + + [Fact] + public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomaticallyAsync() + { + // Arrange + var plugin = this._kernel.CreatePluginFromType(); + this._kernel.Plugins.Add(plugin); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + // Act + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([plugin.ElementAt(1)], autoInvoke: true) }; + + var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); + + // Assert + Assert.NotNull(result); + + Assert.Single(invokedFunctions); + Assert.Contains("GetCurrentDate", invokedFunctions); + } + + [Fact] + public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionAutomaticallyAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var promptTemplate = """" + template_format: semantic-kernel + template: How many days until Christmas? + execution_settings: + default: + temperature: 0.1 + function_choice_behavior: + type: required + functions: + - DateTimeUtils.GetCurrentDate + maximum_auto_invoke_attempts: 3 + """"; + + var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); + + // Act + var result = await this._kernel.InvokeAsync(promptFunction); + + // Assert + Assert.NotNull(result); + + Assert.Single(invokedFunctions); + Assert.Contains("GetCurrentDate", invokedFunctions); + } + + [Fact] + public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuallyAsync() + { + // Arrange + var plugin = this._kernel.CreatePluginFromType(); + this._kernel.Plugins.Add(plugin); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + // Act + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([plugin.ElementAt(1)], autoInvoke: false) }; + + var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); + + // Assert + Assert.NotNull(result); + + Assert.Empty(invokedFunctions); + + var responseContent = result.GetValue(); + Assert.NotNull(responseContent); + + var functionCalls = FunctionCallContent.GetFunctionCalls(responseContent); + Assert.NotNull(functionCalls); + Assert.Single(functionCalls); + + var functionCall = functionCalls.First(); + Assert.Equal("DateTimeUtils", functionCall.PluginName); + Assert.Equal("GetCurrentDate", functionCall.FunctionName); + } + + [Fact] + public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionManuallyAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var promptTemplate = """" + template_format: semantic-kernel + template: How many days until Christmas? + execution_settings: + default: + temperature: 0.1 + function_choice_behavior: + type: required + functions: + - DateTimeUtils.GetCurrentDate + maximum_auto_invoke_attempts: 0 + """"; + + var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); + + // Act + var result = await this._kernel.InvokeAsync(promptFunction); + + // Assert + Assert.NotNull(result); + + Assert.Empty(invokedFunctions); + + var responseContent = result.GetValue(); + Assert.NotNull(responseContent); + + var functionCalls = FunctionCallContent.GetFunctionCalls(responseContent); + Assert.NotNull(functionCalls); + Assert.Single(functionCalls); + + var functionCall = functionCalls.First(); + Assert.Equal("DateTimeUtils", functionCall.PluginName); + Assert.Equal("GetCurrentDate", functionCall.FunctionName); + } + + [Fact] + public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomaticallyForStreamingAsync() + { + // Arrange + var plugin = this._kernel.CreatePluginFromType(); + this._kernel.Plugins.Add(plugin); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([plugin.ElementAt(1)], autoInvoke: true) }; + + string result = ""; + + // Act + await foreach (string c in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) + { + result += c; + } + + // Assert + Assert.NotNull(result); + + Assert.Single(invokedFunctions); + Assert.Contains("GetCurrentDate", invokedFunctions); + } + + [Fact] + public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionAutomaticallyForStreamingAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var promptTemplate = """" + template_format: semantic-kernel + template: How many days until Christmas? + execution_settings: + default: + temperature: 0.1 + function_choice_behavior: + type: required + functions: + - DateTimeUtils.GetCurrentDate + maximum_auto_invoke_attempts: 3 + """"; + + var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); + + string result = ""; + + // Act + await foreach (string c in promptFunction.InvokeStreamingAsync(this._kernel)) + { + result += c; + } + + // Assert + Assert.NotNull(result); + + Assert.Single(invokedFunctions); + Assert.Contains("GetCurrentDate", invokedFunctions); + } + + [Fact] + public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuallyForStreamingAsync() + { + // Arrange + var plugin = this._kernel.CreatePluginFromType(); + this._kernel.Plugins.Add(plugin); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var functionsForManualInvocation = new List(); + + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([plugin.ElementAt(1)], autoInvoke: false) }; + + // Act + await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) + { + if (content.ToolCallUpdate is StreamingFunctionToolCallUpdate functionUpdate && !string.IsNullOrEmpty(functionUpdate.Name)) + { + functionsForManualInvocation.Add(functionUpdate.Name); + } + } + + // Assert + Assert.Single(functionsForManualInvocation); + Assert.Contains("DateTimeUtils-GetCurrentDate", functionsForManualInvocation); + + Assert.Empty(invokedFunctions); + } + + [Fact] + public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionManuallyForStreamingAsync() + { + // Arrange + this._kernel.ImportPluginFromType(); + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var promptTemplate = """" + template_format: semantic-kernel + template: How many days until Christmas? + execution_settings: + default: + temperature: 0.1 + function_choice_behavior: + type: required + functions: + - DateTimeUtils.GetCurrentDate + maximum_auto_invoke_attempts: 0 + """"; + + var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); + + var functionsForManualInvocation = new List(); + + // Act + await foreach (var content in promptFunction.InvokeStreamingAsync(this._kernel)) + { + if (content.ToolCallUpdate is StreamingFunctionToolCallUpdate functionUpdate && !string.IsNullOrEmpty(functionUpdate.Name)) + { + functionsForManualInvocation.Add(functionUpdate.Name); + } + } + + // Assert + Assert.Single(functionsForManualInvocation); + Assert.Contains("DateTimeUtils-GetCurrentDate", functionsForManualInvocation); + + Assert.Empty(invokedFunctions); + } + + private Kernel InitializeKernel() + { + OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("Planners:OpenAI").Get(); + Assert.NotNull(openAIConfiguration); + + IKernelBuilder builder = this.CreateKernelBuilder() + .AddOpenAIChatCompletion( + modelId: openAIConfiguration.ModelId, + apiKey: openAIConfiguration.ApiKey); + + return builder.Build(); + } + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + /// + /// A plugin that returns the current time. + /// + public class DateTimeUtils + { + [KernelFunction] + [Description("Retrieves the current time in UTC.")] + public string GetCurrentUtcTime() => DateTime.UtcNow.ToString("R"); + + [KernelFunction] + [Description("Retrieves the current date.")] + public string GetCurrentDate() => DateTime.UtcNow.ToString("d", CultureInfo.InvariantCulture); + } + + public class WeatherPlugin + { + [KernelFunction, Description("Get current temperature.")] + public Task GetCurrentTemperatureAsync(WeatherParameters parameters) + { + if (parameters.City.Name == "Dublin" && (parameters.City.Country == "Ireland" || parameters.City.Country == "IE")) + { + return Task.FromResult(42.8); // 42.8 Fahrenheit. + } + + throw new NotSupportedException($"Weather in {parameters.City.Name} ({parameters.City.Country}) is not supported."); + } + + [KernelFunction, Description("Convert temperature from Fahrenheit to Celsius.")] + public Task ConvertTemperatureAsync(double temperatureInFahrenheit) + { + double temperatureInCelsius = (temperatureInFahrenheit - 32) * 5 / 9; + return Task.FromResult(temperatureInCelsius); + } + + [KernelFunction, Description("Get the current weather for the specified city.")] + public Task GetWeatherForCityAsync(string cityName) + { + return Task.FromResult(cityName switch + { + "Boston" => "61 and rainy", + _ => "31 and snowing", + }); + } + } + + public record WeatherParameters(City City); + + public class City + { + public string Name { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + } + + #region private + + private sealed class FakeFunctionFilter : IAutoFunctionInvocationFilter + { + private Func, Task>? _onFunctionInvocation; + + public void RegisterFunctionInvocationHandler(Func, Task> onFunctionInvocation) + { + this._onFunctionInvocation = onFunctionInvocation; + } + + public Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + if (this._onFunctionInvocation is null) + { + return next(context); + } + + return this._onFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask; + } + } + + #endregion +} diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/PromptExecutionSettingsTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/PromptExecutionSettingsTests.cs index 75b655fc27b7..c8adcded8c14 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/PromptExecutionSettingsTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/PromptExecutionSettingsTests.cs @@ -18,7 +18,11 @@ public void PromptExecutionSettingsCloneWorksAsExpected() "temperature": 0.5, "top_p": 0.0, "presence_penalty": 0.0, - "frequency_penalty": 0.0 + "frequency_penalty": 0.0, + "function_choice_behavior": { + "type": "auto", + "functions": ["p1.f1"] + } } """; var executionSettings = JsonSerializer.Deserialize(configPayload); @@ -30,6 +34,7 @@ public void PromptExecutionSettingsCloneWorksAsExpected() Assert.NotNull(clone); Assert.Equal(executionSettings.ModelId, clone.ModelId); Assert.Equivalent(executionSettings.ExtensionData, clone.ExtensionData); + Assert.Equivalent(executionSettings.FunctionChoiceBehavior, clone.FunctionChoiceBehavior); } [Fact] @@ -56,5 +61,6 @@ public void PromptExecutionSettingsFreezeWorksAsExpected() Assert.NotNull(executionSettings.ExtensionData); Assert.Throws(() => executionSettings.ExtensionData.Add("results_per_prompt", 2)); Assert.Throws(() => executionSettings.ExtensionData["temperature"] = 1); + Assert.Throws(() => executionSettings.FunctionChoiceBehavior = FunctionChoiceBehavior.None); } } From 79b068a18ccaf32c1a4c38a2142e641a5b9709ae Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 10 May 2024 21:18:42 +0100 Subject: [PATCH 61/90] fix: add tests to check that non-kernel funcitions can be used for manual invocation --- .../OpenAIAutoFunctionChoiceBehaviorTests.cs | 110 +++++++++++------- .../OpenAINoneFunctionChoiceBehaviorTests.cs | 40 ------- ...enAIRequiredFunctionChoiceBehaviorTests.cs | 109 ++++++++++------- 3 files changed, 140 insertions(+), 119 deletions(-) diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs index 636f64818e00..d247d500b133 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Azure.AI.OpenAI; using Microsoft.Extensions.Configuration; - using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; using SemanticKernel.IntegrationTests.Planners.Stepwise; @@ -328,6 +327,76 @@ public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionManua Assert.Empty(invokedFunctions); } + [Fact] + public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManuallyAsync() + { + // Arrange + var plugin = this._kernel.CreatePluginFromType(); // Creating plugin without importing it to the kernel. + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + // Act + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice([plugin.ElementAt(1)], autoInvoke: false) }; + + var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); + + // Assert + Assert.NotNull(result); + + Assert.Empty(invokedFunctions); + + var responseContent = result.GetValue(); + Assert.NotNull(responseContent); + + var functionCalls = FunctionCallContent.GetFunctionCalls(responseContent); + Assert.NotNull(functionCalls); + Assert.Single(functionCalls); + + var functionCall = functionCalls.First(); + Assert.Equal("DateTimeUtils", functionCall.PluginName); + Assert.Equal("GetCurrentDate", functionCall.FunctionName); + } + + [Fact] + public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManuallyForStreamingAsync() + { + // Arrange + var plugin = this._kernel.CreatePluginFromType(); // Creating plugin without importing it to the kernel. + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var functionsForManualInvocation = new List(); + + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice([plugin.ElementAt(1)], autoInvoke: false) }; + + // Act + await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) + { + if (content.ToolCallUpdate is StreamingFunctionToolCallUpdate functionUpdate && !string.IsNullOrEmpty(functionUpdate.Name)) + { + functionsForManualInvocation.Add(functionUpdate.Name); + } + } + + // Assert + Assert.Single(functionsForManualInvocation); + Assert.Contains("DateTimeUtils-GetCurrentDate", functionsForManualInvocation); + + Assert.Empty(invokedFunctions); + } + private Kernel InitializeKernel() { OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("Planners:OpenAI").Get(); @@ -362,45 +431,6 @@ public class DateTimeUtils public string GetCurrentDate() => DateTime.UtcNow.ToString("d", CultureInfo.InvariantCulture); } - public class WeatherPlugin - { - [KernelFunction, Description("Get current temperature.")] - public Task GetCurrentTemperatureAsync(WeatherParameters parameters) - { - if (parameters.City.Name == "Dublin" && (parameters.City.Country == "Ireland" || parameters.City.Country == "IE")) - { - return Task.FromResult(42.8); // 42.8 Fahrenheit. - } - - throw new NotSupportedException($"Weather in {parameters.City.Name} ({parameters.City.Country}) is not supported."); - } - - [KernelFunction, Description("Convert temperature from Fahrenheit to Celsius.")] - public Task ConvertTemperatureAsync(double temperatureInFahrenheit) - { - double temperatureInCelsius = (temperatureInFahrenheit - 32) * 5 / 9; - return Task.FromResult(temperatureInCelsius); - } - - [KernelFunction, Description("Get the current weather for the specified city.")] - public Task GetWeatherForCityAsync(string cityName) - { - return Task.FromResult(cityName switch - { - "Boston" => "61 and rainy", - _ => "31 and snowing", - }); - } - } - - public record WeatherParameters(City City); - - public class City - { - public string Name { get; set; } = string.Empty; - public string Country { get; set; } = string.Empty; - } - #region private private sealed class FakeFunctionFilter : IAutoFunctionInvocationFilter diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs index d559736af37e..a70e9a4338cb 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs @@ -6,7 +6,6 @@ using System.Globalization; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; - using Microsoft.SemanticKernel; using SemanticKernel.IntegrationTests.Planners.Stepwise; using SemanticKernel.IntegrationTests.TestSettings; @@ -194,45 +193,6 @@ public class DateTimeUtils public string GetCurrentDate() => DateTime.UtcNow.ToString("d", CultureInfo.InvariantCulture); } - public class WeatherPlugin - { - [KernelFunction, Description("Get current temperature.")] - public Task GetCurrentTemperatureAsync(WeatherParameters parameters) - { - if (parameters.City.Name == "Dublin" && (parameters.City.Country == "Ireland" || parameters.City.Country == "IE")) - { - return Task.FromResult(42.8); // 42.8 Fahrenheit. - } - - throw new NotSupportedException($"Weather in {parameters.City.Name} ({parameters.City.Country}) is not supported."); - } - - [KernelFunction, Description("Convert temperature from Fahrenheit to Celsius.")] - public Task ConvertTemperatureAsync(double temperatureInFahrenheit) - { - double temperatureInCelsius = (temperatureInFahrenheit - 32) * 5 / 9; - return Task.FromResult(temperatureInCelsius); - } - - [KernelFunction, Description("Get the current weather for the specified city.")] - public Task GetWeatherForCityAsync(string cityName) - { - return Task.FromResult(cityName switch - { - "Boston" => "61 and rainy", - _ => "31 and snowing", - }); - } - } - - public record WeatherParameters(City City); - - public class City - { - public string Name { get; set; } = string.Empty; - public string Country { get; set; } = string.Empty; - } - #region private private sealed class FakeFunctionFilter : IAutoFunctionInvocationFilter diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs index 6f51e2d78c14..98f1dda1024c 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs @@ -339,6 +339,76 @@ public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionManua Assert.Empty(invokedFunctions); } + [Fact] + public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManuallyAsync() + { + // Arrange + var plugin = this._kernel.CreatePluginFromType(); // Creating plugin without importing it to the kernel. + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + // Act + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([plugin.ElementAt(1)], autoInvoke: false) }; + + var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); + + // Assert + Assert.NotNull(result); + + Assert.Empty(invokedFunctions); + + var responseContent = result.GetValue(); + Assert.NotNull(responseContent); + + var functionCalls = FunctionCallContent.GetFunctionCalls(responseContent); + Assert.NotNull(functionCalls); + Assert.Single(functionCalls); + + var functionCall = functionCalls.First(); + Assert.Equal("DateTimeUtils", functionCall.PluginName); + Assert.Equal("GetCurrentDate", functionCall.FunctionName); + } + + [Fact] + public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManuallyForStreamingAsync() + { + // Arrange + var plugin = this._kernel.CreatePluginFromType(); // Creating plugin without importing it to the kernel. + + var invokedFunctions = new List(); + + this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => + { + invokedFunctions.Add(context.Function.Name); + await next(context); + }); + + var functionsForManualInvocation = new List(); + + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([plugin.ElementAt(1)], autoInvoke: false) }; + + // Act + await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) + { + if (content.ToolCallUpdate is StreamingFunctionToolCallUpdate functionUpdate && !string.IsNullOrEmpty(functionUpdate.Name)) + { + functionsForManualInvocation.Add(functionUpdate.Name); + } + } + + // Assert + Assert.Single(functionsForManualInvocation); + Assert.Contains("DateTimeUtils-GetCurrentDate", functionsForManualInvocation); + + Assert.Empty(invokedFunctions); + } + private Kernel InitializeKernel() { OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("Planners:OpenAI").Get(); @@ -373,45 +443,6 @@ public class DateTimeUtils public string GetCurrentDate() => DateTime.UtcNow.ToString("d", CultureInfo.InvariantCulture); } - public class WeatherPlugin - { - [KernelFunction, Description("Get current temperature.")] - public Task GetCurrentTemperatureAsync(WeatherParameters parameters) - { - if (parameters.City.Name == "Dublin" && (parameters.City.Country == "Ireland" || parameters.City.Country == "IE")) - { - return Task.FromResult(42.8); // 42.8 Fahrenheit. - } - - throw new NotSupportedException($"Weather in {parameters.City.Name} ({parameters.City.Country}) is not supported."); - } - - [KernelFunction, Description("Convert temperature from Fahrenheit to Celsius.")] - public Task ConvertTemperatureAsync(double temperatureInFahrenheit) - { - double temperatureInCelsius = (temperatureInFahrenheit - 32) * 5 / 9; - return Task.FromResult(temperatureInCelsius); - } - - [KernelFunction, Description("Get the current weather for the specified city.")] - public Task GetWeatherForCityAsync(string cityName) - { - return Task.FromResult(cityName switch - { - "Boston" => "61 and rainy", - _ => "31 and snowing", - }); - } - } - - public record WeatherParameters(City City); - - public class City - { - public string Name { get; set; } = string.Empty; - public string Country { get; set; } = string.Empty; - } - #region private private sealed class FakeFunctionFilter : IAutoFunctionInvocationFilter From 13a551583c3b37a4e53f841449a85a19baacd799 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 10 May 2024 22:47:51 +0100 Subject: [PATCH 62/90] Add unit tests for {Azure}OpenAI chat completion services. --- .../AzureOpenAIChatCompletionServiceTests.cs | 96 ++++++++++++++++ .../OpenAIChatCompletionServiceTests.cs | 108 ++++++++++++++++-- .../OpenAIPromptExecutionSettingsTests.cs | 16 +++ 3 files changed, 213 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index e2bb373514cf..3ef3d4c5e9ff 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -913,6 +913,102 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); } + [Fact] + public async Task ItCreatesCorrectFunctionToolCallsWhenUsingAutoFunctionChoiceBehaviorAsync() + { + // Arrange + var kernel = new Kernel(); + kernel.Plugins.AddFromFunctions("TimePlugin", [ + KernelFunctionFactory.CreateFromMethod(() => { }, "Date"), + KernelFunctionFactory.CreateFromMethod(() => { }, "Now") + ]); + + var chatCompletion = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; + + // Act + await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + Assert.Equal(2, optionsJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("TimePlugin-Date", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal("TimePlugin-Now", optionsJson.GetProperty("tools")[1].GetProperty("function").GetProperty("name").GetString()); + + Assert.Equal("auto", optionsJson.GetProperty("tool_choice").ToString()); + } + + [Fact] + public async Task ItCreatesCorrectFunctionToolCallsWhenUsingRequiredFunctionChoiceBehaviorAsync() + { + // Arrange + var kernel = new Kernel(); + kernel.Plugins.AddFromFunctions("TimePlugin", [ + KernelFunctionFactory.CreateFromMethod(() => { }, "Date"), + ]); + + var chatCompletion = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice() }; + + // Act + await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + Assert.Equal(1, optionsJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("TimePlugin-Date", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal("TimePlugin-Date", optionsJson.GetProperty("tool_choice").GetProperty("function").GetProperty("name").ToString()); + } + + [Fact] + public async Task ItCreatesCorrectFunctionToolCallsWhenUsingNoneFunctionChoiceBehaviorAsync() + { + // Arrange + var kernel = new Kernel(); + kernel.Plugins.AddFromFunctions("TimePlugin", [ + KernelFunctionFactory.CreateFromMethod(() => { }, "Date"), + ]); + + var chatCompletion = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.None }; + + // Act + await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + Assert.Equal(1, optionsJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("NonInvocableTool", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal("none", optionsJson.GetProperty("tool_choice").ToString()); + } + public void Dispose() { this._httpClient.Dispose(); diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs index 9855ddb313c0..dc9d98e04619 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Text; @@ -27,7 +28,8 @@ public sealed class OpenAIChatCompletionServiceTests : IDisposable { private readonly HttpMessageHandlerStub _messageHandlerStub; private readonly HttpClient _httpClient; - private readonly OpenAIFunction _timepluginDate, _timepluginNow; + private readonly KernelPlugin _plugin; + private readonly KernelFunction _timepluginDate, _timepluginNow; private readonly OpenAIPromptExecutionSettings _executionSettings; private readonly Mock _mockLoggerFactory; @@ -37,18 +39,21 @@ public OpenAIChatCompletionServiceTests() this._httpClient = new HttpClient(this._messageHandlerStub, false); this._mockLoggerFactory = new Mock(); - IList functions = KernelPluginFactory.CreateFromFunctions("TimePlugin", new[] + this._plugin = KernelPluginFactory.CreateFromFunctions("TimePlugin", new[] { KernelFunctionFactory.CreateFromMethod((string? format = null) => DateTime.Now.Date.ToString(format, CultureInfo.InvariantCulture), "Date", "TimePlugin.Date"), KernelFunctionFactory.CreateFromMethod((string? format = null) => DateTime.Now.ToString(format, CultureInfo.InvariantCulture), "Now", "TimePlugin.Now"), - }).GetFunctionsMetadata(); + }); - this._timepluginDate = functions[0].ToOpenAIFunction(); - this._timepluginNow = functions[1].ToOpenAIFunction(); + this._timepluginDate = this._plugin.ElementAt(0); + this._timepluginNow = this._plugin.ElementAt(1); this._executionSettings = new() { - ToolCallBehavior = ToolCallBehavior.EnableFunctions([this._timepluginDate, this._timepluginNow]) + ToolCallBehavior = ToolCallBehavior.EnableFunctions([ + this._timepluginDate.Metadata.ToOpenAIFunction(), + this._timepluginNow.Metadata.ToOpenAIFunction() + ]) }; } @@ -130,7 +135,7 @@ public async Task ItCreatesCorrectFunctionToolCallsWhenUsingNowAsync() var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(ChatCompletionResponse) }; - this._executionSettings.ToolCallBehavior = ToolCallBehavior.RequireFunction(this._timepluginNow); + this._executionSettings.ToolCallBehavior = ToolCallBehavior.RequireFunction(this._timepluginNow.Metadata.ToOpenAIFunction()); // Act await chatCompletion.GetChatMessageContentsAsync([], this._executionSettings); @@ -556,6 +561,95 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); } + [Fact] + public async Task ItCreatesCorrectFunctionToolCallsWhenUsingAutoFunctionChoiceBehaviorAsync() + { + // Arrange + var kernel = new Kernel(); + kernel.Plugins.Add(this._plugin); + + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(ChatCompletionResponse) + }; + + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; + + // Act + await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + Assert.Equal(2, optionsJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("TimePlugin-Date", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal("TimePlugin-Now", optionsJson.GetProperty("tools")[1].GetProperty("function").GetProperty("name").GetString()); + + Assert.Equal("auto", optionsJson.GetProperty("tool_choice").ToString()); + } + + [Fact] + public async Task ItCreatesCorrectFunctionToolCallsWhenUsingRequiredFunctionChoiceBehaviorAsync() + { + // Arrange + var kernel = new Kernel(); + kernel.Plugins.AddFromFunctions("TimePlugin", [this._timepluginDate]); + + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(ChatCompletionResponse) + }; + + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice() }; + + // Act + await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + Assert.Equal(1, optionsJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("TimePlugin-Date", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal("TimePlugin-Date", optionsJson.GetProperty("tool_choice").GetProperty("function").GetProperty("name").ToString()); + } + + [Fact] + public async Task ItCreatesCorrectFunctionToolCallsWhenUsingNoneFunctionChoiceBehaviorAsync() + { + // Arrange + var kernel = new Kernel(); + kernel.Plugins.AddFromFunctions("TimePlugin", [this._timepluginDate]); + + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(ChatCompletionResponse) + }; + + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.None }; + + // Act + await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + Assert.Equal(1, optionsJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("NonInvocableTool", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal("none", optionsJson.GetProperty("tool_choice").ToString()); + } + public void Dispose() { this._httpClient.Dispose(); diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs index 8912219a8aaf..86d70f3b4a18 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs @@ -240,6 +240,22 @@ public void FromExecutionSettingsWithDataDoesNotIncludeEmptyStopSequences() Assert.Null(executionSettingsWithData.StopSequences); } + [Fact] + public void ItRestoresOriginalFunctionChoiceBehavior() + { + // Arrange + var functionChoiceBehavior = FunctionChoiceBehavior.None; + + var originalExecutionSettings = new PromptExecutionSettings(); + originalExecutionSettings.FunctionChoiceBehavior = functionChoiceBehavior; + + // Act + var result = OpenAIPromptExecutionSettings.FromExecutionSettings(originalExecutionSettings); + + // Assert + Assert.Equal(functionChoiceBehavior, result.FunctionChoiceBehavior); + } + private static void AssertExecutionSettings(OpenAIPromptExecutionSettings executionSettings) { Assert.NotNull(executionSettings); From c9ca569c039b10039c7dc32fdcf7ab51f7021be1 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 13 May 2024 13:18:50 +0100 Subject: [PATCH 63/90] fix merge issues --- .../src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 7b5ca8fc74ec..5f2767b240fe 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1016,12 +1016,12 @@ private static ChatCompletionsOptions CreateChatCompletionsOptions( if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) { - options.Messages.AddRange(GetRequestMessages(new ChatMessageContent(AuthorRole.System, executionSettings!.ChatSystemPrompt), executionSettings)); + options.Messages.AddRange(GetRequestMessages(new ChatMessageContent(AuthorRole.System, executionSettings!.ChatSystemPrompt), executionSettings.ToolCallBehavior)); } foreach (var message in chatHistory) { - options.Messages.AddRange(GetRequestMessages(message, executionSettings)); + options.Messages.AddRange(GetRequestMessages(message, executionSettings.ToolCallBehavior)); } return options; @@ -1090,7 +1090,7 @@ private static List GetRequestMessages(ChatMessageContent me continue; } - var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, executionSettings.ToolCallBehavior); + var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); toolMessages.Add(new ChatRequestToolMessage(stringResult ?? string.Empty, resultContent.Id)); } From af08e8b9a96b92e33c5ff7b73bce82a1512acd42 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 13 May 2024 19:52:27 +0100 Subject: [PATCH 64/90] provide reason for function check in auto-invocation mode. --- .../FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index 1109a2cd2c77..4c94d3920001 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -108,7 +108,8 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho // Handle functions provided via constructor as function instances. if (this._functions is { } functions && functions.Any()) { - // Make sure that every function can be found in the kernel. + // Ensure that every function in auto-invocation mode is present in the kernel, allowing the service to look it up for invocation later. + // For manual invocation, there is no need to check if the function is available in the kernel, as the caller that registered it already has its instance. if (autoInvoke) { foreach (var function in functions) From bc146a8fb72b93c721d5188d4261f4e2523b90f8 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 14 May 2024 15:32:28 +0100 Subject: [PATCH 65/90] 1. Initialize Functions collection by functions supplied via constructor. 2. Convert prompt function name in format "plugin.function" to "plugin-function". --- .../Functions/KernelFunctionMarkdownTests.cs | 4 +- .../Yaml/Functions/KernelFunctionYamlTests.cs | 4 +- ...omptExecutionSettingsTypeConverterTests.cs | 4 +- .../FunctionChoiceBehaviorTypesConverter.cs | 82 +++++++++++++ .../PromptExecutionSettingsTypeConverter.cs | 3 +- .../AutoFunctionChoiceBehavior.cs | 65 ++++------ .../FunctionChoiceBehavior.cs | 3 - .../FunctionNameFormatJsonConverter.cs | 62 ++++++++++ .../RequiredFunctionChoiceBehavior.cs | 68 ++++------- .../AutoFunctionChoiceBehaviorTests.cs | 115 +++++++++++------- .../RequiredFunctionChoiceBehaviorTests.cs | 115 +++++++++++------- .../PromptTemplateConfigTests.cs | 4 +- 12 files changed, 346 insertions(+), 183 deletions(-) create mode 100644 dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs diff --git a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs index 8a5ab2c15f98..cb8157fb5395 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs @@ -49,7 +49,7 @@ public void ItShouldInitializeFunctionChoiceBehaviorsFromMarkdown() Assert.NotNull(autoFunctionChoiceBehavior.Functions); Assert.Single(autoFunctionChoiceBehavior.Functions); - Assert.Equal("p1.f1", autoFunctionChoiceBehavior.Functions.First()); + Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.First()); Assert.Equal(8, autoFunctionChoiceBehavior.MaximumAutoInvokeAttempts); // RequiredFunctionCallChoice for service2 @@ -60,7 +60,7 @@ public void ItShouldInitializeFunctionChoiceBehaviorsFromMarkdown() Assert.NotNull(requiredFunctionChoiceBehavior); Assert.NotNull(requiredFunctionChoiceBehavior.Functions); Assert.Single(requiredFunctionChoiceBehavior.Functions); - Assert.Equal("p1.f1", requiredFunctionChoiceBehavior.Functions.First()); + Assert.Equal("p1-f1", requiredFunctionChoiceBehavior.Functions.First()); Assert.Equal(2, requiredFunctionChoiceBehavior.MaximumUseAttempts); // NoneFunctionCallChoice for service3 diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs index f8eb74a6f722..b6cee1d523a4 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs @@ -98,7 +98,7 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; Assert.NotNull(autoFunctionChoiceBehavior?.Functions); - Assert.Equal("p1.f1", autoFunctionChoiceBehavior.Functions.Single()); + Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.Single()); Assert.Equal(9, autoFunctionChoiceBehavior.MaximumAutoInvokeAttempts); // Service with required function choice behavior @@ -106,7 +106,7 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); - Assert.Equal("p2.f2", requiredFunctionChoiceBehavior.Functions.Single()); + Assert.Equal("p2-f2", requiredFunctionChoiceBehavior.Functions.Single()); Assert.Equal(6, requiredFunctionChoiceBehavior.MaximumAutoInvokeAttempts); Assert.Equal(3, requiredFunctionChoiceBehavior.MaximumUseAttempts); diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs index ac53ff3d9e3c..44fc9be0c70a 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs @@ -52,7 +52,7 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; Assert.NotNull(autoFunctionChoiceBehavior?.Functions); - Assert.Equal("p1.f1", autoFunctionChoiceBehavior.Functions.Single()); + Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.Single()); Assert.Equal(9, autoFunctionChoiceBehavior.MaximumAutoInvokeAttempts); // Service with required function choice behavior @@ -60,7 +60,7 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); - Assert.Equal("p2.f2", requiredFunctionChoiceBehavior.Functions.Single()); + Assert.Equal("p2-f2", requiredFunctionChoiceBehavior.Functions.Single()); Assert.Equal(6, requiredFunctionChoiceBehavior.MaximumAutoInvokeAttempts); Assert.Equal(3, requiredFunctionChoiceBehavior.MaximumUseAttempts); diff --git a/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs b/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs new file mode 100644 index 000000000000..cb16aaf2e899 --- /dev/null +++ b/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using YamlDotNet.Core; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Microsoft.SemanticKernel; + +/// +/// Allows custom deserialization for derivatives of . +/// +internal sealed class FunctionChoiceBehaviorTypesConverter : IYamlTypeConverter +{ + private const char PromptFunctionNameSeparator = '.'; + + private const char FunctionNameSeparator = '-'; + + private static IDeserializer? s_deserializer; + + /// + public bool Accepts(Type type) + { +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + return + type == typeof(AutoFunctionChoiceBehavior) || + type == typeof(RequiredFunctionChoiceBehavior) || + type == typeof(NoneFunctionChoiceBehavior); +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + public object? ReadYaml(IParser parser, Type type) + { + s_deserializer ??= new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination. Otherwise, the "Property 'type' not found on type '{type.FullName}'" exception is thrown. + .Build(); + +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + if (type == typeof(AutoFunctionChoiceBehavior)) + { + var behavior = s_deserializer.Deserialize(parser); + behavior.Functions = ConvertFunctionNames(behavior.Functions); + return behavior; + } + else if (type == typeof(RequiredFunctionChoiceBehavior)) + { + var behavior = s_deserializer.Deserialize(parser); + behavior.Functions = ConvertFunctionNames(behavior.Functions); + return behavior; + } + else if (type == typeof(NoneFunctionChoiceBehavior)) + { + return s_deserializer.Deserialize(parser); + } + + throw new YamlException($"Unexpected type '{type.FullName}' for function choice behavior."); +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + /// + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + throw new NotImplementedException(); + } + + private static IList? ConvertFunctionNames(IList? functions) + { + if (functions is null) + { + return functions; + } + + return functions.Select(fqn => + { + var functionName = fqn ?? throw new YamlException("Expected a non-null YAML string."); + return functionName.Replace(PromptFunctionNameSeparator, FunctionNameSeparator); + }).ToList(); + } +} diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs index c7889c4035fa..b4c5d8569f17 100644 --- a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs @@ -27,8 +27,7 @@ public bool Accepts(Type type) { s_deserializer ??= new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) - .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination in the WithTypeDiscriminatingNodeDeserializer method below. - // Otherwise, the "Property '{name}' not found on type '{type.FullName}'" exception is thrown. + .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) .WithTypeDiscriminatingNodeDeserializer((options) => { #pragma warning disable SKEXP0001 diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index 0bf1a9a91dcd..d3f11da10235 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -19,11 +19,6 @@ public sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior /// private readonly IEnumerable? _functions; - /// - /// List of the fully qualified names of the functions that the model can choose from. - /// - private readonly IEnumerable? _functionFQNs; - /// /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. /// @@ -44,6 +39,7 @@ public AutoFunctionChoiceBehavior() public AutoFunctionChoiceBehavior(IEnumerable functions) { this._functions = functions; + this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); } /// @@ -55,25 +51,14 @@ public AutoFunctionChoiceBehavior(IEnumerable functions) /// the same function over and over. To disable auto invocation, this can be set to 0. /// [JsonPropertyName("maximumAutoInvokeAttempts")] - public int MaximumAutoInvokeAttempts { get; init; } = DefaultMaximumAutoInvokeAttempts; + public int MaximumAutoInvokeAttempts { get; set; } = DefaultMaximumAutoInvokeAttempts; /// /// Fully qualified names of subset of the 's plugins' functions information to provide to the model. /// [JsonPropertyName("functions")] - public IEnumerable? Functions - { - get => this._functionFQNs; - init - { - if (value?.Count() > 0 && this._functions?.Count() > 0) - { - throw new KernelException("Functions are already provided via the constructor."); - } - - this._functionFQNs = value; - } - } + [JsonConverter(typeof(FunctionNameFormatJsonConverter))] + public IList? Functions { get; set; } /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) @@ -93,39 +78,37 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho List? availableFunctions = null; bool allowAnyRequestedKernelFunction = false; - // Handle functions provided via constructor as function instances. - if (this._functions is { } functions && functions.Any()) - { - // Make sure that every function can be found in the kernel. - if (autoInvoke) - { - foreach (var function in functions) - { - if (!context.Kernel!.Plugins.TryGetFunction(function.PluginName, function.Name, out _)) - { - throw new KernelException($"The specified function {function.PluginName}.{function.Name} is not available in the kernel."); - } - } - } - - availableFunctions = functions.ToList(); - } // Handle functions provided via the 'Functions' property as function fully qualified names. - else if (this.Functions is { } functionFQNs && functionFQNs.Any()) + if (this.Functions is { } functionFQNs && functionFQNs.Any()) { availableFunctions = []; foreach (var functionFQN in functionFQNs) { - // Make sure that every function can be found in the kernel. - var name = FunctionName.Parse(functionFQN, FunctionNameSeparator); + var nameParts = FunctionName.Parse(functionFQN); - if (!context.Kernel!.Plugins.TryGetFunction(name.PluginName, name.Name, out var function)) + // Check if the function is available in the kernel. If it is, then connectors can find it for auto-invocation later. + if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) + { + availableFunctions.Add(function); + continue; + } + + // If auto-invocation is requested and no function is found in the kernel, fail early. + if (autoInvoke) { throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); } - availableFunctions.Add(function); + // Check if the function instance was provided via the constructor for manual-invocation. + function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); + if (function is not null) + { + availableFunctions.Add(function); + continue; + } + + throw new KernelException($"No instance of the specified function {functionFQN} is found."); } } // Provide all functions from the kernel. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index 50e64526f339..045205fe4954 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -16,9 +16,6 @@ namespace Microsoft.SemanticKernel; [Experimental("SKEXP0001")] public abstract class FunctionChoiceBehavior { - /// The separator used to separate plugin name and function name. - protected const string FunctionNameSeparator = "."; - /// /// The default maximum number of function auto-invokes that can be made in a single user request. /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs new file mode 100644 index 000000000000..ace55d5a059f --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel; + +/// +/// A custom JSON converter for converting function names in a JSON array. +/// This converter replaces dots used as a function name separator in prompts with hyphens when reading and back when writing. +/// +public sealed class FunctionNameFormatJsonConverter : JsonConverter> +{ + private const char PromptFunctionNameSeparator = '.'; + + private const char FunctionNameSeparator = '-'; + + /// + public override IList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException("Expected a JSON array."); + } + + var functionNames = new List(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException("Expected a JSON string."); + } + + var functionName = reader.GetString() ?? throw new JsonException("Expected a non-null JSON string."); + + functionNames.Add(functionName.Replace(PromptFunctionNameSeparator, FunctionNameSeparator)); + } + + return functionNames; + } + + /// + public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + + foreach (string functionName in value) + { + writer.WriteStringValue(functionName.Replace(FunctionNameSeparator, PromptFunctionNameSeparator)); + } + + writer.WriteEndArray(); + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index 4c94d3920001..eba7b4f402f7 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -19,11 +19,6 @@ public sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior /// private readonly IEnumerable? _functions; - /// - /// List of the fully qualified names of the functions that the model can choose from. - /// - private readonly IEnumerable? _functionFQNs; - /// /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. /// @@ -44,25 +39,15 @@ public RequiredFunctionChoiceBehavior() public RequiredFunctionChoiceBehavior(IEnumerable functions) { this._functions = functions; + this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); } /// /// Fully qualified names of subset of the 's plugins' functions information to provide to the model. /// [JsonPropertyName("functions")] - public IEnumerable? Functions - { - get => this._functionFQNs; - init - { - if (value?.Count() > 0 && this._functions?.Count() > 0) - { - throw new KernelException("Functions are already provided via the constructor."); - } - - this._functionFQNs = value; - } - } + [JsonConverter(typeof(FunctionNameFormatJsonConverter))] + public IList? Functions { get; set; } /// /// The maximum number of function auto-invokes that can be made in a single user request. @@ -73,7 +58,7 @@ public IEnumerable? Functions /// the same function over and over. To disable auto invocation, this can be set to 0. /// [JsonPropertyName("maximumAutoInvokeAttempts")] - public int MaximumAutoInvokeAttempts { get; init; } = DefaultMaximumAutoInvokeAttempts; + public int MaximumAutoInvokeAttempts { get; set; } = DefaultMaximumAutoInvokeAttempts; /// /// Number of requests that are part of a single user interaction that should include this functions in the request. @@ -85,7 +70,7 @@ public IEnumerable? Functions /// will not include the functions for further use. /// [JsonPropertyName("maximumUseAttempts")] - public int MaximumUseAttempts { get; init; } = 1; + public int MaximumUseAttempts { get; set; } = 1; /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) @@ -105,40 +90,37 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho List? requiredFunctions = null; bool allowAnyRequestedKernelFunction = false; - // Handle functions provided via constructor as function instances. - if (this._functions is { } functions && functions.Any()) - { - // Ensure that every function in auto-invocation mode is present in the kernel, allowing the service to look it up for invocation later. - // For manual invocation, there is no need to check if the function is available in the kernel, as the caller that registered it already has its instance. - if (autoInvoke) - { - foreach (var function in functions) - { - if (!context.Kernel!.Plugins.TryGetFunction(function.PluginName, function.Name, out _)) - { - throw new KernelException($"The specified function {function.PluginName}.{function.Name} is not available in the kernel."); - } - } - } - - requiredFunctions = functions.ToList(); - } // Handle functions provided via the 'Functions' property as function fully qualified names. - else if (this.Functions is { } functionFQNs && functionFQNs.Any()) + if (this.Functions is { } functionFQNs && functionFQNs.Any()) { requiredFunctions = []; foreach (var functionFQN in functionFQNs) { - // Make sure that every function can be found in the kernel. - var name = FunctionName.Parse(functionFQN, FunctionNameSeparator); + var nameParts = FunctionName.Parse(functionFQN); - if (!context.Kernel!.Plugins.TryGetFunction(name.PluginName, name.Name, out var function)) + // Check if the function is available in the kernel. If it is, then connectors can find it for auto-invocation later. + if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) + { + requiredFunctions.Add(function); + continue; + } + + // If auto-invocation is requested and no function is found in the kernel, fail early. + if (autoInvoke) { throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); } - requiredFunctions.Add(function); + // Check if the function instance was provided via the constructor for manual-invocation. + function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); + if (function is not null) + { + requiredFunctions.Add(function); + continue; + } + + throw new KernelException($"No instance of the specified function {functionFQN} is found."); } } // Provide all functions from the kernel. diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs index d89ab0d0f273..fa551e0cd552 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs @@ -20,7 +20,7 @@ public AutoFunctionChoiceBehaviorTests() } [Fact] - public void ItShouldAdvertiseKernelFunctionsAsAvailableOnes() + public void ItShouldAdvertiseAllKernelFunctionsAsAvailableOnes() { // Arrange var plugin = GetTestPlugin(); @@ -44,7 +44,7 @@ public void ItShouldAdvertiseKernelFunctionsAsAvailableOnes() } [Fact] - public void ItShouldAdvertiseFunctionsProvidedAsInstancesAsAvailableOnes() + public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorAsAvailableOnes() { // Arrange var plugin = GetTestPlugin(); @@ -67,7 +67,7 @@ public void ItShouldAdvertiseFunctionsProvidedAsInstancesAsAvailableOnes() } [Fact] - public void ItShouldAdvertiseFunctionsProvidedAsFunctionFQNsAsAvailableOnes() + public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsPropertyAsAvailableOnes() { // Arrange var plugin = GetTestPlugin(); @@ -76,7 +76,7 @@ public void ItShouldAdvertiseFunctionsProvidedAsFunctionFQNsAsAvailableOnes() // Act var choiceBehavior = new AutoFunctionChoiceBehavior() { - Functions = ["MyPlugin.Function1", "MyPlugin.Function2"] + Functions = ["MyPlugin-Function1", "MyPlugin-Function2"] }; var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -92,6 +92,58 @@ public void ItShouldAdvertiseFunctionsProvidedAsFunctionFQNsAsAvailableOnes() Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); } + [Fact] + public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorAsAvailableOnesForManualInvocation() + { + // Arrange + var plugin = GetTestPlugin(); + + // Act + var choiceBehavior = new AutoFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]) + { + MaximumAutoInvokeAttempts = 0 + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.RequiredFunctions); + + Assert.NotNull(config.AvailableFunctions); + Assert.Equal(2, config.AvailableFunctions.Count()); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); + } + + [Fact] + public void ItShouldAdvertiseAllKernelFunctionsAsAvailableOnesForManualInvocation() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new AutoFunctionChoiceBehavior() + { + MaximumAutoInvokeAttempts = 0 + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.RequiredFunctions); + + Assert.NotNull(config.AvailableFunctions); + Assert.Equal(3, config.AvailableFunctions.Count()); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); + Assert.Contains(config.AvailableFunctions, f => f.Name == "Function3"); + } + [Fact] public void ItShouldHaveFiveMaxAutoInvokeAttemptsByDefault() { @@ -150,22 +202,21 @@ public void ItShouldAllowManualInvocation() } [Fact] - public void ItShouldThrowExceptionIfFunctionProvidedAsInstancesAndAsFunctionFQNsAtTheSameTime() + public void ItShouldInitializeFunctionPropertyByFunctionsPassedViaConstructor() { // Arrange var plugin = GetTestPlugin(); this._kernel.Plugins.Add(plugin); // Act - var exception = Assert.Throws(() => - { - var choiceBehavior = new AutoFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]) - { - Functions = ["MyPlugin.Function1"] - }; - }); + var choiceBehavior = new AutoFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]); + + // Assert + Assert.NotNull(choiceBehavior.Functions); + Assert.Equal(2, choiceBehavior.Functions.Count()); - Assert.Equal("Functions are already provided via the constructor.", exception.Message); + Assert.Equal("MyPlugin-Function1", choiceBehavior.Functions.ElementAt(0)); + Assert.Equal("MyPlugin-Function2", choiceBehavior.Functions.ElementAt(1)); } [Fact] @@ -206,13 +257,11 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegis choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("The specified function MyPlugin.Function1 is not available in the kernel.", exception.Message); + Assert.Equal("The specified function MyPlugin-Function1 is not available in the kernel.", exception.Message); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ItShouldThrowExceptionIfFunctionProvidedAsFunctionFQNIsNotRegisteredInKernel(bool autoInvoke) + [Fact] + public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequested() { // Arrange var plugin = GetTestPlugin(); @@ -220,8 +269,8 @@ public void ItShouldThrowExceptionIfFunctionProvidedAsFunctionFQNIsNotRegistered var choiceBehavior = new AutoFunctionChoiceBehavior() { - MaximumAutoInvokeAttempts = autoInvoke ? 5 : 0, - Functions = ["MyPlugin.NonKernelFunction"] + MaximumAutoInvokeAttempts = 0, + Functions = ["MyPlugin-NonKernelFunction"] }; // Act @@ -230,11 +279,11 @@ public void ItShouldThrowExceptionIfFunctionProvidedAsFunctionFQNIsNotRegistered choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("The specified function MyPlugin.NonKernelFunction is not available in the kernel.", exception.Message); + Assert.Equal("No instance of the specified function MyPlugin-NonKernelFunction is found.", exception.Message); } [Fact] - public void ItShouldAllowToInvokeAnyRequestedKernelFunctionForKernelFunctions() + public void ItShouldAllowInvocationOfAnyRequestedKernelFunction() { // Arrange var plugin = GetTestPlugin(); @@ -251,7 +300,7 @@ public void ItShouldAllowToInvokeAnyRequestedKernelFunctionForKernelFunctions() } [Fact] - public void ItShouldNotAllowInvokingAnyRequestedKernelFunctionForProvidedAsInstancesFunctions() + public void ItShouldNotAllowInvocationOfAnyRequestedKernelFunctionIfSubsetOfFunctionsSpecified() { // Arrange var plugin = GetTestPlugin(); @@ -267,26 +316,6 @@ public void ItShouldNotAllowInvokingAnyRequestedKernelFunctionForProvidedAsInsta Assert.False(config.AllowAnyRequestedKernelFunction); } - [Fact] - public void ItShouldNotAllowInvokingAnyRequestedKernelFunctionForFunctionsProvidedAsFunctionFQNs() - { - // Arrange - var plugin = GetTestPlugin(); - this._kernel.Plugins.Add(plugin); - - // Act - var choiceBehavior = new AutoFunctionChoiceBehavior() - { - Functions = ["MyPlugin.Function2"] - }; - - var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); - - // Assert - Assert.NotNull(config); - Assert.False(config.AllowAnyRequestedKernelFunction); - } - private static KernelPlugin GetTestPlugin() { var function1 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function1"); diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs index 210eb91f7365..9a9b1eb1b3a1 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs @@ -20,7 +20,7 @@ public RequiredFunctionChoiceBehaviorTests() } [Fact] - public void ItShouldAdvertiseKernelFunctionsAsRequiredOnes() + public void ItShouldAdvertiseAllKernelFunctionsAsRequiredOnes() { // Arrange var plugin = GetTestPlugin(); @@ -44,7 +44,7 @@ public void ItShouldAdvertiseKernelFunctionsAsRequiredOnes() } [Fact] - public void ItShouldAdvertiseFunctionsProvidedAsInstancesAsRequiredOnes() + public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorAsRequiredOnes() { // Arrange var plugin = GetTestPlugin(); @@ -67,7 +67,7 @@ public void ItShouldAdvertiseFunctionsProvidedAsInstancesAsRequiredOnes() } [Fact] - public void ItShouldAdvertiseFunctionsProvidedAsFunctionFQNsAsRequiredOnes() + public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsPropertyAsRequiredOnes() { // Arrange var plugin = GetTestPlugin(); @@ -76,7 +76,7 @@ public void ItShouldAdvertiseFunctionsProvidedAsFunctionFQNsAsRequiredOnes() // Act var choiceBehavior = new RequiredFunctionChoiceBehavior() { - Functions = ["MyPlugin.Function1", "MyPlugin.Function2"] + Functions = ["MyPlugin-Function1", "MyPlugin-Function2"] }; var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -92,6 +92,58 @@ public void ItShouldAdvertiseFunctionsProvidedAsFunctionFQNsAsRequiredOnes() Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); } + [Fact] + public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorAsRequiredOnesForManualInvocation() + { + // Arrange + var plugin = GetTestPlugin(); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]) + { + MaximumAutoInvokeAttempts = 0 + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.AvailableFunctions); + + Assert.NotNull(config.RequiredFunctions); + Assert.Equal(2, config.RequiredFunctions.Count()); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); + } + + [Fact] + public void ItShouldAdvertiseAllKernelFunctionsAsRequiredOnesForManualInvocation() + { + // Arrange + var plugin = GetTestPlugin(); + this._kernel.Plugins.Add(plugin); + + // Act + var choiceBehavior = new RequiredFunctionChoiceBehavior() + { + MaximumAutoInvokeAttempts = 0 + }; + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.Null(config.AvailableFunctions); + + Assert.NotNull(config.RequiredFunctions); + Assert.Equal(3, config.RequiredFunctions.Count()); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); + Assert.Contains(config.RequiredFunctions, f => f.Name == "Function3"); + } + [Fact] public void ItShouldHaveFiveMaxAutoInvokeAttemptsByDefault() { @@ -150,22 +202,21 @@ public void ItShouldAllowManualInvocation() } [Fact] - public void ItShouldThrowExceptionIfFunctionProvidedAsInstancesAndAsFunctionFQNsAtTheSameTime() + public void ItShouldInitializeFunctionPropertyByFunctionsPassedViaConstructor() { // Arrange var plugin = GetTestPlugin(); this._kernel.Plugins.Add(plugin); // Act - var exception = Assert.Throws(() => - { - var choiceBehavior = new RequiredFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]) - { - Functions = ["MyPlugin.Function1"] - }; - }); + var choiceBehavior = new RequiredFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]); + + // Assert + Assert.NotNull(choiceBehavior.Functions); + Assert.Equal(2, choiceBehavior.Functions.Count()); - Assert.Equal("Functions are already provided via the constructor.", exception.Message); + Assert.Equal("MyPlugin-Function1", choiceBehavior.Functions.ElementAt(0)); + Assert.Equal("MyPlugin-Function2", choiceBehavior.Functions.ElementAt(1)); } [Fact] @@ -206,13 +257,11 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegis choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("The specified function MyPlugin.Function1 is not available in the kernel.", exception.Message); + Assert.Equal("The specified function MyPlugin-Function1 is not available in the kernel.", exception.Message); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ItShouldThrowExceptionIfFunctionProvidedAsFunctionFQNIsNotRegisteredInKernel(bool autoInvoke) + [Fact] + public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequested() { // Arrange var plugin = GetTestPlugin(); @@ -220,8 +269,8 @@ public void ItShouldThrowExceptionIfFunctionProvidedAsFunctionFQNIsNotRegistered var choiceBehavior = new RequiredFunctionChoiceBehavior() { - MaximumAutoInvokeAttempts = autoInvoke ? 5 : 0, - Functions = ["MyPlugin.NonKernelFunction"] + MaximumAutoInvokeAttempts = 0, + Functions = ["MyPlugin-NonKernelFunction"] }; // Act @@ -230,11 +279,11 @@ public void ItShouldThrowExceptionIfFunctionProvidedAsFunctionFQNIsNotRegistered choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("The specified function MyPlugin.NonKernelFunction is not available in the kernel.", exception.Message); + Assert.Equal("No instance of the specified function MyPlugin-NonKernelFunction is found.", exception.Message); } [Fact] - public void ItShouldAllowToInvokeAnyRequestedKernelFunctionForKernelFunctions() + public void ItShouldAllowInvocationOfAnyRequestedKernelFunction() { // Arrange var plugin = GetTestPlugin(); @@ -251,7 +300,7 @@ public void ItShouldAllowToInvokeAnyRequestedKernelFunctionForKernelFunctions() } [Fact] - public void ItShouldNotAllowInvokingAnyRequestedKernelFunctionForProvidedAsInstancesFunctions() + public void ItShouldNotAllowInvocationOfAnyRequestedKernelFunctionIfSubsetOfFunctionsSpecified() { // Arrange var plugin = GetTestPlugin(); @@ -267,26 +316,6 @@ public void ItShouldNotAllowInvokingAnyRequestedKernelFunctionForProvidedAsInsta Assert.False(config.AllowAnyRequestedKernelFunction); } - [Fact] - public void ItShouldNotAllowInvokingAnyRequestedKernelFunctionForFunctionsProvidedAsFunctionFQNs() - { - // Arrange - var plugin = GetTestPlugin(); - this._kernel.Plugins.Add(plugin); - - // Act - var choiceBehavior = new RequiredFunctionChoiceBehavior() - { - Functions = ["MyPlugin.Function2"] - }; - - var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); - - // Assert - Assert.NotNull(config); - Assert.False(config.AllowAnyRequestedKernelFunction); - } - [Fact] public void ItShouldHaveOneMaxUseAttemptsByDefault() { diff --git a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs index ebd32c22bec3..d29527ba2e87 100644 --- a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs @@ -175,7 +175,7 @@ public void DeserializingAutoFunctionCallingChoice() Assert.NotNull(autoFunctionCallChoice); Assert.NotNull(autoFunctionCallChoice.Functions); - Assert.Equal("p1.f1", autoFunctionCallChoice.Functions.Single()); + Assert.Equal("p1-f1", autoFunctionCallChoice.Functions.Single()); Assert.Equal(12, autoFunctionCallChoice.MaximumAutoInvokeAttempts); } @@ -215,7 +215,7 @@ public void DeserializingRequiredFunctionCallingChoice() Assert.NotNull(requiredFunctionCallChoice); Assert.NotNull(requiredFunctionCallChoice.Functions); - Assert.Equal("p1.f1", requiredFunctionCallChoice.Functions.Single()); + Assert.Equal("p1-f1", requiredFunctionCallChoice.Functions.Single()); Assert.Equal(11, requiredFunctionCallChoice.MaximumAutoInvokeAttempts); Assert.Equal(2, requiredFunctionCallChoice.MaximumUseAttempts); From 04005b70ffa0cddeb6b98457f5cd1542a28f06fc Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 14 May 2024 15:46:23 +0100 Subject: [PATCH 66/90] fix compilation warnings --- .../Functions/AutoFunctionChoiceBehaviorTests.cs | 2 +- .../Functions/RequiredFunctionChoiceBehaviorTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs index fa551e0cd552..5ecacda209b0 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs @@ -213,7 +213,7 @@ public void ItShouldInitializeFunctionPropertyByFunctionsPassedViaConstructor() // Assert Assert.NotNull(choiceBehavior.Functions); - Assert.Equal(2, choiceBehavior.Functions.Count()); + Assert.Equal(2, choiceBehavior.Functions.Count); Assert.Equal("MyPlugin-Function1", choiceBehavior.Functions.ElementAt(0)); Assert.Equal("MyPlugin-Function2", choiceBehavior.Functions.ElementAt(1)); diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs index 9a9b1eb1b3a1..b674c6413101 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs @@ -213,7 +213,7 @@ public void ItShouldInitializeFunctionPropertyByFunctionsPassedViaConstructor() // Assert Assert.NotNull(choiceBehavior.Functions); - Assert.Equal(2, choiceBehavior.Functions.Count()); + Assert.Equal(2, choiceBehavior.Functions.Count); Assert.Equal("MyPlugin-Function1", choiceBehavior.Functions.ElementAt(0)); Assert.Equal("MyPlugin-Function2", choiceBehavior.Functions.ElementAt(1)); From b986dfe918b6c75f2210f9e6ef4d99280ebd3fe8 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 14 May 2024 16:35:57 +0100 Subject: [PATCH 67/90] add unit tests for yaml and json converters --- ...nctionChoiceBehaviorTypesConverterTests.cs | 89 +++++++++++++++++++ .../FunctionChoiceBehaviorTypesConverter.cs | 8 +- .../AutoFunctionChoiceBehaviorTests.cs | 2 +- .../FunctionNameFormatJsonConverterTests.cs | 56 ++++++++++++ .../NoneFunctionChoiceBehaviorTests.cs | 2 +- .../RequiredFunctionChoiceBehaviorTests.cs | 2 +- 6 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs rename dotnet/src/SemanticKernel.UnitTests/{Functions => AI/FunctionChoiceBehaviors}/AutoFunctionChoiceBehaviorTests.cs (99%) create mode 100644 dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs rename dotnet/src/SemanticKernel.UnitTests/{Functions => AI/FunctionChoiceBehaviors}/NoneFunctionChoiceBehaviorTests.cs (95%) rename dotnet/src/SemanticKernel.UnitTests/{Functions => AI/FunctionChoiceBehaviors}/RequiredFunctionChoiceBehaviorTests.cs (99%) diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs new file mode 100644 index 000000000000..529b017fc4f9 --- /dev/null +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using Microsoft.SemanticKernel; +using Xunit; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace SemanticKernel.Functions.UnitTests.Yaml; + +/// +/// Tests for . +/// +public sealed class FunctionChoiceBehaviorTypesConverterTests +{ + [Fact] + public void ItShouldDeserializeAutoFunctionChoiceBehavior() + { + // Arrange + var deserializer = new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) + .Build(); + + var yaml = """ + type: auto + maximum_auto_invoke_attempts: 9 + functions: + - p1.f1 + """; + + // Act + var behavior = deserializer.Deserialize(yaml); + + // Assert + Assert.NotNull(behavior.Functions); + Assert.Single(behavior.Functions); + Assert.Equal("p1-f1", behavior.Functions.Single()); + Assert.Equal(9, behavior.MaximumAutoInvokeAttempts); + } + + [Fact] + public void ItShouldDeserializeRequiredFunctionChoiceBehavior() + { + // Arrange + var deserializer = new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) + .Build(); + + var yaml = """ + type: required + maximum_auto_invoke_attempts: 6 + maximum_use_attempts: 3 + functions: + - p2.f2 + """; + + // Act + var behavior = deserializer.Deserialize(yaml); + + // Assert + Assert.NotNull(behavior.Functions); + Assert.Single(behavior.Functions); + Assert.Equal("p2-f2", behavior.Functions.Single()); + Assert.Equal(6, behavior.MaximumAutoInvokeAttempts); + Assert.Equal(3, behavior.MaximumUseAttempts); + } + + [Fact] + public void ItShouldDeserializeNoneFunctionChoiceBehavior() + { + // Arrange + var deserializer = new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) + .Build(); + + var yaml = """ + type: none + """; + + // Act + var behavior = deserializer.Deserialize(yaml); + + // Assert + Assert.Null(behavior.Functions); + } +} diff --git a/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs b/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs index cb16aaf2e899..660c9c207d2b 100644 --- a/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs @@ -23,12 +23,12 @@ internal sealed class FunctionChoiceBehaviorTypesConverter : IYamlTypeConverter /// public bool Accepts(Type type) { -#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable SKEXP0001 return type == typeof(AutoFunctionChoiceBehavior) || type == typeof(RequiredFunctionChoiceBehavior) || type == typeof(NoneFunctionChoiceBehavior); -#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning restore SKEXP0001 } public object? ReadYaml(IParser parser, Type type) @@ -38,7 +38,7 @@ public bool Accepts(Type type) .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination. Otherwise, the "Property 'type' not found on type '{type.FullName}'" exception is thrown. .Build(); -#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable SKEXP0001 if (type == typeof(AutoFunctionChoiceBehavior)) { var behavior = s_deserializer.Deserialize(parser); @@ -57,7 +57,7 @@ public bool Accepts(Type type) } throw new YamlException($"Unexpected type '{type.FullName}' for function choice behavior."); -#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning restore SKEXP0001 } /// diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs similarity index 99% rename from dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs rename to dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs index 5ecacda209b0..8aa494bff56e 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs @@ -5,7 +5,7 @@ using Microsoft.SemanticKernel; using Xunit; -namespace SemanticKernel.UnitTests.Functions; +namespace SemanticKernel.UnitTests.AI.FunctionChoiceBehaviors; /// /// Unit tests for diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs new file mode 100644 index 000000000000..95c632edb61c --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using System.Text.Json; +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.UnitTests.AI.FunctionChoiceBehaviors; +public class FunctionNameFormatJsonConverterTests +{ + [Fact] + public void ItShouldDeserializeAutoFunctionChoiceBehavior() + { + // Arrange + var json = """ + { + "type": "auto", + "functions": ["p1.f1"], + "maximumAutoInvokeAttempts": 8 + } + """; + + // Act + var behavior = JsonSerializer.Deserialize(json); + + // Assert + Assert.NotNull(behavior?.Functions); + Assert.Single(behavior.Functions); + Assert.Equal("p1-f1", behavior.Functions.Single()); + Assert.Equal(8, behavior.MaximumAutoInvokeAttempts); + } + + [Fact] + public void ItShouldDeserializeRequiredFunctionChoiceBehavior() + { + // Arrange + var json = """ + { + "type": "required", + "functions": ["p1.f1"], + "maximumUseAttempts": 2, + "maximumAutoInvokeAttempts": 8 + } + """; + + // Act + var behavior = JsonSerializer.Deserialize(json); + + // Assert + Assert.NotNull(behavior?.Functions); + Assert.Single(behavior.Functions); + Assert.Equal("p1-f1", behavior.Functions.Single()); + Assert.Equal(8, behavior.MaximumAutoInvokeAttempts); + Assert.Equal(2, behavior.MaximumUseAttempts); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/NoneFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs similarity index 95% rename from dotnet/src/SemanticKernel.UnitTests/Functions/NoneFunctionChoiceBehaviorTests.cs rename to dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs index 1e1186be449c..c15ded1cd631 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/NoneFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs @@ -3,7 +3,7 @@ using Microsoft.SemanticKernel; using Xunit; -namespace SemanticKernel.UnitTests.Functions; +namespace SemanticKernel.UnitTests.AI.FunctionChoiceBehaviors; /// /// Unit tests for diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs similarity index 99% rename from dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs rename to dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs index b674c6413101..585311f4d388 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs @@ -5,7 +5,7 @@ using Microsoft.SemanticKernel; using Xunit; -namespace SemanticKernel.UnitTests.Functions; +namespace SemanticKernel.UnitTests.AI.FunctionChoiceBehaviors; /// /// Unit tests for From f61f00d16c2396cc91451a69713700eb3d76f18a Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 14 May 2024 21:43:42 +0100 Subject: [PATCH 68/90] 1. Add function choice to function choice behavior config 2. Use one list to send functions to connectors instead of two ones. --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 41 ++++--- .../AutoFunctionChoiceBehavior.cs | 3 +- .../FunctionChoiceBehaviors/FunctionChoice.cs | 78 +++++++++++++ .../FunctionChoiceBehaviorConfiguration.cs | 8 +- .../NoneFunctionChoiceBehavior.cs | 4 +- .../RequiredFunctionChoiceBehavior.cs | 15 +-- .../AutoFunctionChoiceBehaviorTests.cs | 64 +++++------ .../FunctionChoiceTests.cs | 106 ++++++++++++++++++ .../NoneFunctionChoiceBehaviorTests.cs | 5 +- .../RequiredFunctionChoiceBehaviorTests.cs | 64 +++++------ .../Functions/FunctionChoiceBehaviorTests.cs | 55 ++++----- 11 files changed, 303 insertions(+), 140 deletions(-) create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionChoiceTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 5f2767b240fe..c196e2425f4d 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1458,37 +1458,46 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context return result; } - // If we have a required function, it means we want to force LLM to invoke that function. - if (config.RequiredFunctions is { } requiredFunctions && requiredFunctions.Any()) + if (config.Choice == FunctionChoice.Auto) { - if (requiredFunctions.Count() > 1) + chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; + + if (config.Functions is { } functions) { - throw new KernelException("Only one required function is allowed."); + foreach (var function in functions) + { + var functionDefinition = function.Metadata.ToOpenAIFunction().ToFunctionDefinition(); + chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); + } } - var functionDefinition = requiredFunctions.First().Metadata.ToOpenAIFunction().ToFunctionDefinition(); - - chatOptions.ToolChoice = new ChatCompletionsToolChoice(functionDefinition); - chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); - return result; } - // If we have available functions, we want LLM to choose which function(s) to call. - if (config.AvailableFunctions is { } availableFunctions && availableFunctions.Any()) + if (config.Choice == FunctionChoice.Required) { - chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; - - foreach (var function in availableFunctions) + if (config.Functions is { } functions && functions.Any()) { - var functionDefinition = function.Metadata.ToOpenAIFunction().ToFunctionDefinition(); + if (functions.Count() > 1) + { + throw new KernelException("Only one required function is allowed."); + } + + var functionDefinition = functions.First().Metadata.ToOpenAIFunction().ToFunctionDefinition(); + + chatOptions.ToolChoice = new ChatCompletionsToolChoice(functionDefinition); chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); } return result; } - return result; + if (config.Choice == FunctionChoice.None) + { + return result; + } + + throw new KernelException($"Unsupported function choice '{config.Choice}'."); } private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCallingFromToolCallBehavior(Kernel? kernel, ChatCompletionsOptions chatOptions, int requestIndex, ToolCallBehavior toolCallBehavior) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index d3f11da10235..ccc6ce433c1e 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -125,7 +125,8 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho return new FunctionChoiceBehaviorConfiguration() { - AvailableFunctions = availableFunctions, + Choice = FunctionChoice.Auto, + Functions = availableFunctions, MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs new file mode 100644 index 000000000000..e6af18cbd49c --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.SemanticKernel; + +/// +/// Represents an AI model's decision-making strategy for calling functions, offering predefined choices: Auto, Required, and None. +/// Auto allows the model to decide if and which functions to call, Required enforces calling one or more functions, and None prevents any function calls, generating only a user-facing message. +/// +public readonly struct FunctionChoice : IEquatable +{ + /// + /// This choice instructs the model to decide whether to call the functions or not and, if so, which ones to call. + /// + public static FunctionChoice Auto { get; } = new("auto"); + + /// + /// This choice forces the model to always call one or more functions. The model will then select which function(s) to call. + /// + public static FunctionChoice Required { get; } = new("required"); + + /// + /// This behavior forces the model to not call any functions and only generate a user-facing message. + /// + public static FunctionChoice None { get; } = new("none"); + + /// + /// Gets the label associated with this FunctionChoice. + /// + public string Label { get; } + + /// + /// Creates a new FunctionChoice instance with the provided label. + /// + /// The label to associate with this FunctionChoice. + public FunctionChoice(string label) + { + Verify.NotNullOrWhiteSpace(label, nameof(label)); + this.Label = label!; + } + + /// + /// Returns a value indicating whether two FunctionChoice instances are equivalent, as determined by a + /// case-insensitive comparison of their labels. + /// + /// the first FunctionChoice instance to compare + /// the second FunctionChoice instance to compare + /// true if left and right are both null or have equivalent labels; false otherwise + public static bool operator ==(FunctionChoice left, FunctionChoice right) + => left.Equals(right); + + /// + /// Returns a value indicating whether two FunctionChoice instances are not equivalent, as determined by a + /// case-insensitive comparison of their labels. + /// + /// the first FunctionChoice instance to compare + /// the second FunctionChoice instance to compare + /// false if left and right are both null or have equivalent labels; true otherwise + public static bool operator !=(FunctionChoice left, FunctionChoice right) + => !(left == right); + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + => obj is FunctionChoice other && this == other; + + /// + public bool Equals(FunctionChoice other) + => string.Equals(this.Label, other.Label, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() + => StringComparer.OrdinalIgnoreCase.GetHashCode(this.Label ?? string.Empty); + + /// + public override string ToString() => this.Label ?? string.Empty; +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index 5204cc48bffb..3e5f23ed461d 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -12,14 +12,14 @@ namespace Microsoft.SemanticKernel; public sealed class FunctionChoiceBehaviorConfiguration { /// - /// The functions that are available for the model to call. + /// Represents an AI model's decision-making strategy for calling functions. /// - public IEnumerable? AvailableFunctions { get; init; } + public FunctionChoice Choice { get; init; } /// - /// The functions that the model is required to call. + /// The functions available for AI model. /// - public IEnumerable? RequiredFunctions { get; init; } + public IEnumerable? Functions { get; init; } /// /// The maximum number of function auto-invokes that can be made in a single user request. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs index 2eb8586c514c..2a1a459b7fc3 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -21,9 +21,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho { return new FunctionChoiceBehaviorConfiguration() { - // By not providing either available or required functions, we are telling the model to not call any functions. - AvailableFunctions = null, - RequiredFunctions = null, + Choice = FunctionChoice.None, }; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index eba7b4f402f7..cfafbb6934ed 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -87,13 +87,13 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho throw new KernelException("Auto-invocation for Required choice behavior is not supported when no kernel is provided."); } - List? requiredFunctions = null; + List? availableFunctions = null; bool allowAnyRequestedKernelFunction = false; // Handle functions provided via the 'Functions' property as function fully qualified names. if (this.Functions is { } functionFQNs && functionFQNs.Any()) { - requiredFunctions = []; + availableFunctions = []; foreach (var functionFQN in functionFQNs) { @@ -102,7 +102,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho // Check if the function is available in the kernel. If it is, then connectors can find it for auto-invocation later. if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) { - requiredFunctions.Add(function); + availableFunctions.Add(function); continue; } @@ -116,7 +116,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); if (function is not null) { - requiredFunctions.Add(function); + availableFunctions.Add(function); continue; } @@ -130,14 +130,15 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var plugin in context.Kernel.Plugins) { - requiredFunctions ??= []; - requiredFunctions.AddRange(plugin); + availableFunctions ??= []; + availableFunctions.AddRange(plugin); } } return new FunctionChoiceBehaviorConfiguration() { - RequiredFunctions = requiredFunctions, + Choice = FunctionChoice.Required, + Functions = availableFunctions, MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts, MaximumUseAttempts = this.MaximumUseAttempts, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs index 8aa494bff56e..0316e820328c 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs @@ -20,7 +20,7 @@ public AutoFunctionChoiceBehaviorTests() } [Fact] - public void ItShouldAdvertiseAllKernelFunctionsAsAvailableOnes() + public void ItShouldAdvertiseAllKernelFunctions() { // Arrange var plugin = GetTestPlugin(); @@ -34,17 +34,15 @@ public void ItShouldAdvertiseAllKernelFunctionsAsAvailableOnes() // Assert Assert.NotNull(config); - Assert.Null(config.RequiredFunctions); - - Assert.NotNull(config.AvailableFunctions); - Assert.Equal(3, config.AvailableFunctions.Count()); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] - public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorAsAvailableOnes() + public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructor() { // Arrange var plugin = GetTestPlugin(); @@ -58,16 +56,14 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorAsAvailableOnes( // Assert Assert.NotNull(config); - Assert.Null(config.RequiredFunctions); - - Assert.NotNull(config.AvailableFunctions); - Assert.Equal(2, config.AvailableFunctions.Count()); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] - public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsPropertyAsAvailableOnes() + public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() { // Arrange var plugin = GetTestPlugin(); @@ -84,16 +80,14 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsPropertyAsAvailable // Assert Assert.NotNull(config); - Assert.Null(config.RequiredFunctions); - - Assert.NotNull(config.AvailableFunctions); - Assert.Equal(2, config.AvailableFunctions.Count()); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] - public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorAsAvailableOnesForManualInvocation() + public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorForManualInvocation() { // Arrange var plugin = GetTestPlugin(); @@ -109,16 +103,14 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorAsAvailableOnesF // Assert Assert.NotNull(config); - Assert.Null(config.RequiredFunctions); - - Assert.NotNull(config.AvailableFunctions); - Assert.Equal(2, config.AvailableFunctions.Count()); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] - public void ItShouldAdvertiseAllKernelFunctionsAsAvailableOnesForManualInvocation() + public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() { // Arrange var plugin = GetTestPlugin(); @@ -135,13 +127,11 @@ public void ItShouldAdvertiseAllKernelFunctionsAsAvailableOnesForManualInvocatio // Assert Assert.NotNull(config); - Assert.Null(config.RequiredFunctions); - - Assert.NotNull(config.AvailableFunctions); - Assert.Equal(3, config.AvailableFunctions.Count()); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionChoiceTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionChoiceTests.cs new file mode 100644 index 000000000000..359ea11ce4de --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionChoiceTests.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.UnitTests.AI.FunctionChoiceBehaviors; + +public class FunctionChoiceTests +{ + [Fact] + public void ItShouldInitializeLabelForAutoFunctionChoice() + { + // Act + var choice = FunctionChoice.Auto; + + // Assert + Assert.Equal("auto", choice.Label); + } + + [Fact] + public void ItShouldInitializeLabelForRequiredFunctionChoice() + { + // Act + var choice = FunctionChoice.Required; + + // Assert + Assert.Equal("required", choice.Label); + } + + [Fact] + public void ItShouldInitializeLabelForNoneFunctionChoice() + { + // Act + var choice = FunctionChoice.None; + + // Assert + Assert.Equal("none", choice.Label); + } + + [Fact] + public void ItShouldCheckTwoChoicesAreEqual() + { + // Arrange + var choice1 = FunctionChoice.Auto; + var choice2 = FunctionChoice.Auto; + + // Act & Assert + Assert.True(choice1 == choice2); + } + + [Fact] + public void ItShouldCheckTwoChoicesAreNotEqual() + { + // Arrange + var choice1 = FunctionChoice.Auto; + var choice2 = FunctionChoice.Required; + + // Act & Assert + Assert.False(choice1 == choice2); + } + + [Fact] + public void ItShouldCheckChoiceIsEqualToItself() + { + // Arrange + var choice = FunctionChoice.Auto; + + // Act & Assert +#pragma warning disable CS1718 // Comparison made to same variable + Assert.True(choice == choice); +#pragma warning restore CS1718 // Comparison made to same variable + } + + [Fact] + public void ItShouldCheckChoiceIsNotEqualToDifferentType() + { + // Arrange + var choice = FunctionChoice.Auto; + + // Act & Assert + Assert.False(choice.Equals("auto")); + } + + [Fact] + public void ItShouldCheckChoiceIsNotEqualToNull() + { + // Arrange + var choice = FunctionChoice.Auto; + + // Act & Assert + Assert.False(choice.Equals(null)); + } + + [Fact] + public void ToStringShouldReturnLabel() + { + // Arrange + var choice = FunctionChoice.Auto; + + // Act + var result = choice.ToString(); + + // Assert + Assert.Equal("auto", result); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs index c15ded1cd631..99aaf151d5df 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs @@ -18,7 +18,7 @@ public NoneFunctionChoiceBehaviorTests() } [Fact] - public void ItShouldAdvertiseNeitherAvailableNorRequiredFunctions() + public void ItShouldAdvertiseNoFunctions() { // Arrange var plugin = GetTestPlugin(); @@ -32,8 +32,7 @@ public void ItShouldAdvertiseNeitherAvailableNorRequiredFunctions() // Assert Assert.NotNull(config); - Assert.Null(config.AvailableFunctions); - Assert.Null(config.RequiredFunctions); + Assert.Null(config.Functions); } private static KernelPlugin GetTestPlugin() diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs index 585311f4d388..8ca2416188f5 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs @@ -20,7 +20,7 @@ public RequiredFunctionChoiceBehaviorTests() } [Fact] - public void ItShouldAdvertiseAllKernelFunctionsAsRequiredOnes() + public void ItShouldAdvertiseAllKernelFunctions() { // Arrange var plugin = GetTestPlugin(); @@ -34,17 +34,15 @@ public void ItShouldAdvertiseAllKernelFunctionsAsRequiredOnes() // Assert Assert.NotNull(config); - Assert.Null(config.AvailableFunctions); - - Assert.NotNull(config.RequiredFunctions); - Assert.Equal(3, config.RequiredFunctions.Count()); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] - public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorAsRequiredOnes() + public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructor() { // Arrange var plugin = GetTestPlugin(); @@ -58,16 +56,14 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorAsRequiredOnes() // Assert Assert.NotNull(config); - Assert.Null(config.AvailableFunctions); - - Assert.NotNull(config.RequiredFunctions); - Assert.Equal(2, config.RequiredFunctions.Count()); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] - public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsPropertyAsRequiredOnes() + public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() { // Arrange var plugin = GetTestPlugin(); @@ -84,16 +80,14 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsPropertyAsRequiredO // Assert Assert.NotNull(config); - Assert.Null(config.AvailableFunctions); - - Assert.NotNull(config.RequiredFunctions); - Assert.Equal(2, config.RequiredFunctions.Count()); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] - public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorAsRequiredOnesForManualInvocation() + public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorForManualInvocation() { // Arrange var plugin = GetTestPlugin(); @@ -109,16 +103,14 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorAsRequiredOnesFo // Assert Assert.NotNull(config); - Assert.Null(config.AvailableFunctions); - - Assert.NotNull(config.RequiredFunctions); - Assert.Equal(2, config.RequiredFunctions.Count()); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] - public void ItShouldAdvertiseAllKernelFunctionsAsRequiredOnesForManualInvocation() + public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() { // Arrange var plugin = GetTestPlugin(); @@ -135,13 +127,11 @@ public void ItShouldAdvertiseAllKernelFunctionsAsRequiredOnesForManualInvocation // Assert Assert.NotNull(config); - Assert.Null(config.AvailableFunctions); - - Assert.NotNull(config.RequiredFunctions); - Assert.Equal(3, config.RequiredFunctions.Count()); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs index f7988c1329d0..2d13f228fc01 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs @@ -49,7 +49,7 @@ public void NoneFunctionChoiceShouldBeUsed() } [Fact] - public void AutoFunctionChoiceShouldAdvertiseKernelFunctionsAsAvailableOnes() + public void AutoFunctionChoiceShouldAdvertiseKernelFunctions() { // Arrange var plugin = GetTestPlugin(); @@ -63,17 +63,15 @@ public void AutoFunctionChoiceShouldAdvertiseKernelFunctionsAsAvailableOnes() // Assert Assert.NotNull(config); - Assert.Null(config.RequiredFunctions); - - Assert.NotNull(config.AvailableFunctions); - Assert.Equal(3, config.AvailableFunctions.Count()); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] - public void AutoFunctionChoiceShouldAdvertiseProvidedFunctionsAsAvailableOnes() + public void AutoFunctionChoiceShouldAdvertiseProvidedFunctions() { // Arrange var plugin = GetTestPlugin(); @@ -87,12 +85,10 @@ public void AutoFunctionChoiceShouldAdvertiseProvidedFunctionsAsAvailableOnes() // Assert Assert.NotNull(config); - Assert.Null(config.RequiredFunctions); - - Assert.NotNull(config.AvailableFunctions); - Assert.Equal(2, config.AvailableFunctions.Count()); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function1"); - Assert.Contains(config.AvailableFunctions, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] @@ -130,7 +126,7 @@ public void AutoFunctionChoiceShouldAllowManualInvocation() } [Fact] - public void RequiredFunctionChoiceShouldAdvertiseKernelFunctionsAsRequiredOnes() + public void RequiredFunctionChoiceShouldAdvertiseKernelFunctions() { // Arrange var plugin = GetTestPlugin(); @@ -144,17 +140,15 @@ public void RequiredFunctionChoiceShouldAdvertiseKernelFunctionsAsRequiredOnes() // Assert Assert.NotNull(config); - Assert.Null(config.AvailableFunctions); - - Assert.NotNull(config.RequiredFunctions); - Assert.Equal(3, config.RequiredFunctions.Count()); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] - public void RequiredFunctionChoiceShouldAdvertiseProvidedFunctionsAsRequiredOnes() + public void RequiredFunctionChoiceShouldAdvertiseProvidedFunctions() { // Arrange var plugin = GetTestPlugin(); @@ -168,12 +162,10 @@ public void RequiredFunctionChoiceShouldAdvertiseProvidedFunctionsAsRequiredOnes // Assert Assert.NotNull(config); - Assert.Null(config.AvailableFunctions); - - Assert.NotNull(config.RequiredFunctions); - Assert.Equal(2, config.RequiredFunctions.Count()); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function1"); - Assert.Contains(config.RequiredFunctions, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] @@ -225,8 +217,7 @@ public void NoneFunctionChoiceShouldAdvertiseNoFunctions() // Assert Assert.NotNull(config); - Assert.Null(config.AvailableFunctions); - Assert.Null(config.RequiredFunctions); + Assert.Null(config.Functions); } private static KernelPlugin GetTestPlugin() From f280a7e7abfd9d238388e94c0845c8efb9b4fb44 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 15 May 2024 17:21:13 +0100 Subject: [PATCH 69/90] add support for functions to the none funciton choice behavior --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 37 ++++++--- .../AzureOpenAIChatCompletionServiceTests.cs | 9 ++- .../OpenAIChatCompletionServiceTests.cs | 15 ++-- .../OpenAIPromptExecutionSettingsTests.cs | 2 +- .../Functions/KernelFunctionMarkdownTests.cs | 10 ++- ...nctionChoiceBehaviorTypesConverterTests.cs | 10 ++- .../Yaml/Functions/KernelFunctionYamlTests.cs | 4 + ...omptExecutionSettingsTypeConverterTests.cs | 4 + .../FunctionChoiceBehaviorTypesConverter.cs | 4 +- .../OpenAINoneFunctionChoiceBehaviorTests.cs | 5 +- .../AutoFunctionChoiceBehavior.cs | 2 +- .../FunctionChoiceBehavior.cs | 18 +++-- .../NoneFunctionChoiceBehavior.cs | 79 ++++++++++++++++++- .../RequiredFunctionChoiceBehavior.cs | 4 +- .../AI/PromptExecutionSettings.cs | 9 ++- .../FunctionNameFormatJsonConverterTests.cs | 6 +- .../NoneFunctionChoiceBehaviorTests.cs | 29 ++++++- .../AI/PromptExecutionSettingsTests.cs | 2 +- .../Functions/FunctionChoiceBehaviorTests.cs | 32 +++++++- .../PromptTemplateConfigTests.cs | 11 +-- 20 files changed, 235 insertions(+), 57 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index c196e2425f4d..0c19d3a29e69 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1400,6 +1400,15 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context /// Request sequence index of automatic function invocation process. private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(Kernel? kernel, OpenAIPromptExecutionSettings executionSettings, ChatCompletionsOptions chatOptions, int requestIndex) { + (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? result = null; + + // If neither behavior specified, we don't need to do anything. + if (executionSettings.FunctionChoiceBehavior is null && executionSettings.ToolCallBehavior is null) + { + return result; + } + + // If both behaviors are specified, we can't handle that. if (executionSettings.FunctionChoiceBehavior is not null && executionSettings.ToolCallBehavior is not null) { throw new ArgumentException($"{nameof(executionSettings.ToolCallBehavior)} and {nameof(executionSettings.FunctionChoiceBehavior)} cannot be used together."); @@ -1409,33 +1418,30 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context chatOptions.ToolChoice = ChatCompletionsToolChoice.None; chatOptions.Tools.Clear(); - (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? result = null; - // Handling new tool behavior represented by `PromptExecutionSettings.FunctionChoiceBehavior` property. if (executionSettings.FunctionChoiceBehavior is { } functionChoiceBehavior) { - result = this.ConfigureFunctionCallingFromFunctionChoiceBehavior(kernel, chatOptions, requestIndex, functionChoiceBehavior); + result = this.ConfigureFunctionCalling(kernel, chatOptions, requestIndex, functionChoiceBehavior); } // Handling old-style tool call behavior represented by `OpenAIPromptExecutionSettings.ToolCallBehavior` property. else if (executionSettings.ToolCallBehavior is { } toolCallBehavior) { - result = this.ConfigureFunctionCallingFromToolCallBehavior(kernel, chatOptions, requestIndex, toolCallBehavior); + result = this.ConfigureFunctionCalling(kernel, chatOptions, requestIndex, toolCallBehavior); } // 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. - // Similarly, if we say not to use any tool (ToolChoice = ChatCompletionsToolChoice.None) for the first request, + // Similarly, if we say not to use any tool (ToolChoice = ChatCompletionsToolChoice.None) and dont provide any for the first request, // the service fails with "'tool_choice' is only allowed when 'tools' are specified." - if (chatOptions.ToolChoice == ChatCompletionsToolChoice.None) + if (chatOptions.ToolChoice == ChatCompletionsToolChoice.None && chatOptions.Tools.Count == 0) { - Debug.Assert(chatOptions.Tools.Count == 0); chatOptions.Tools.Add(s_nonInvocableFunctionTool); } return result; } - private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCallingFromFunctionChoiceBehavior(Kernel? kernel, ChatCompletionsOptions chatOptions, int requestIndex, FunctionChoiceBehavior functionChoiceBehavior) + private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(Kernel? kernel, ChatCompletionsOptions chatOptions, int requestIndex, FunctionChoiceBehavior functionChoiceBehavior) { // Regenerate the tool list as necessary and getting other call behavior properties. The invocation of the function(s) could have augmented // what functions are available in the kernel. @@ -1494,13 +1500,24 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context if (config.Choice == FunctionChoice.None) { + chatOptions.ToolChoice = ChatCompletionsToolChoice.None; + + if (config.Functions is { } functions) + { + foreach (var function in functions) + { + var functionDefinition = function.Metadata.ToOpenAIFunction().ToFunctionDefinition(); + chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); + } + } + return result; } - throw new KernelException($"Unsupported function choice '{config.Choice}'."); + throw new NotSupportedException($"Unsupported function choice '{config.Choice}'."); } - private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCallingFromToolCallBehavior(Kernel? kernel, ChatCompletionsOptions chatOptions, int requestIndex, ToolCallBehavior toolCallBehavior) + private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(Kernel? kernel, ChatCompletionsOptions chatOptions, int requestIndex, ToolCallBehavior toolCallBehavior) { if (requestIndex >= toolCallBehavior.MaximumUseAttempts) { diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index e87564abccf0..0ce9c4e2b809 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -985,6 +985,7 @@ public async Task ItCreatesCorrectFunctionToolCallsWhenUsingNoneFunctionChoiceBe var kernel = new Kernel(); kernel.Plugins.AddFromFunctions("TimePlugin", [ KernelFunctionFactory.CreateFromMethod(() => { }, "Date"), + KernelFunctionFactory.CreateFromMethod(() => { }, "Now") ]); var chatCompletion = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); @@ -994,7 +995,7 @@ public async Task ItCreatesCorrectFunctionToolCallsWhenUsingNoneFunctionChoiceBe Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }); - var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.None }; + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice() }; // Act await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); @@ -1004,8 +1005,10 @@ public async Task ItCreatesCorrectFunctionToolCallsWhenUsingNoneFunctionChoiceBe Assert.NotNull(actualRequestContent); var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - Assert.Equal(1, optionsJson.GetProperty("tools").GetArrayLength()); - Assert.Equal("NonInvocableTool", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal(2, optionsJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("TimePlugin-Date", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal("TimePlugin-Now", optionsJson.GetProperty("tools")[1].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal("none", optionsJson.GetProperty("tool_choice").ToString()); } diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs index 14bcbb2eb037..625203e802c5 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs @@ -657,16 +657,19 @@ public async Task ItCreatesCorrectFunctionToolCallsWhenUsingNoneFunctionChoiceBe { // Arrange var kernel = new Kernel(); - kernel.Plugins.AddFromFunctions("TimePlugin", [this._timepluginDate]); + kernel.Plugins.AddFromFunctions("TimePlugin", [ + KernelFunctionFactory.CreateFromMethod(() => { }, "Date"), + KernelFunctionFactory.CreateFromMethod(() => { }, "Now") + ]); - var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + var chatCompletion = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(ChatCompletionResponse) }; - var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.None }; + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice() }; // Act await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); @@ -676,8 +679,10 @@ public async Task ItCreatesCorrectFunctionToolCallsWhenUsingNoneFunctionChoiceBe Assert.NotNull(actualRequestContent); var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - Assert.Equal(1, optionsJson.GetProperty("tools").GetArrayLength()); - Assert.Equal("NonInvocableTool", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal(2, optionsJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("TimePlugin-Date", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal("TimePlugin-Now", optionsJson.GetProperty("tools")[1].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal("none", optionsJson.GetProperty("tool_choice").ToString()); } diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs index 285d4fd6bd31..53869a7535ee 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs @@ -247,7 +247,7 @@ public void FromExecutionSettingsWithDataDoesNotIncludeEmptyStopSequences() public void ItRestoresOriginalFunctionChoiceBehavior() { // Arrange - var functionChoiceBehavior = FunctionChoiceBehavior.None; + var functionChoiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice(); var originalExecutionSettings = new PromptExecutionSettings(); originalExecutionSettings.FunctionChoiceBehavior = functionChoiceBehavior; diff --git a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs index cb8157fb5395..f7000cb9c5c0 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs @@ -69,6 +69,9 @@ public void ItShouldInitializeFunctionChoiceBehaviorsFromMarkdown() var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; Assert.NotNull(noneFunctionChoiceBehavior); + Assert.NotNull(noneFunctionChoiceBehavior.Functions); + Assert.Single(noneFunctionChoiceBehavior.Functions); + Assert.Equal("p1-f1", noneFunctionChoiceBehavior.Functions.First()); } [Fact] @@ -99,7 +102,7 @@ These are AI execution settings "function_choice_behavior": { "type": "auto", "functions": ["p1.f1"], - "maximumAutoInvokeAttempts": 8 + "maximum_auto_invoke_attempts": 8 } } } @@ -113,7 +116,7 @@ These are more AI execution settings "function_choice_behavior": { "type": "required", "functions": ["p1.f1"], - "maximumUseAttempts": 2 + "maximum_use_attempts": 2 } } } @@ -125,7 +128,8 @@ These are AI execution settings as well "model_id": "gpt3.5-turbo", "temperature": 0.8, "function_choice_behavior": { - "type": "none" + "type": "none", + "functions": ["p1.f1"] } } } diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs index 529b017fc4f9..225fcaea475f 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs @@ -77,13 +77,17 @@ public void ItShouldDeserializeNoneFunctionChoiceBehavior() .Build(); var yaml = """ - type: none + type: none, + functions: + - p1.f1 """; // Act - var behavior = deserializer.Deserialize(yaml); + var behavior = deserializer.Deserialize(yaml); // Assert - Assert.Null(behavior.Functions); + Assert.NotNull(behavior.Functions); + Assert.Single(behavior.Functions); + Assert.Equal("p1-f1", behavior.Functions.Single()); } } diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs index b6cee1d523a4..c37ef22a48ac 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs @@ -115,6 +115,8 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; Assert.NotNull(noneFunctionChoiceBehavior); + Assert.NotNull(noneFunctionChoiceBehavior?.Functions); + Assert.Equal("p3-f3", noneFunctionChoiceBehavior.Functions.Single()); } [Fact] @@ -221,6 +223,8 @@ string CreateYaml(object defaultValue) stop_sequences: [ "foo", "bar", "baz" ] function_choice_behavior: type: none + functions: + - p3.f3 """; private readonly string _yamlWithCustomSettings = """ diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs index 44fc9be0c70a..9800a2954b86 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs @@ -69,6 +69,8 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; Assert.NotNull(noneFunctionChoiceBehavior); + Assert.NotNull(noneFunctionChoiceBehavior?.Functions); + Assert.Equal("p3-f3", noneFunctionChoiceBehavior.Functions.Single()); } private readonly string _yaml = """ @@ -121,5 +123,7 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() stop_sequences: [ "foo", "bar", "baz" ] function_choice_behavior: type: none + functions: + - p3.f3 """; } diff --git a/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs b/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs index 660c9c207d2b..8f5b0f42be6d 100644 --- a/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs @@ -53,7 +53,9 @@ public bool Accepts(Type type) } else if (type == typeof(NoneFunctionChoiceBehavior)) { - return s_deserializer.Deserialize(parser); + var behavior = s_deserializer.Deserialize(parser); + behavior.Functions = ConvertFunctionNames(behavior.Functions); + return behavior; } throw new YamlException($"Unexpected type '{type.FullName}' for function choice behavior."); diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs index a70e9a4338cb..b3ad13b4b985 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs @@ -42,7 +42,7 @@ public async Task SpecifiedInCodeInstructsConnectorNotToInvokeKernelFunctionAsyn }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.None }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice() }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -74,7 +74,6 @@ public async Task SpecifiedInPromptInstructsConnectorNotToInvokeKernelFunctionAs temperature: 0.1 function_choice_behavior: type: none - maximum_auto_invoke_attempts: 3 """"; var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); @@ -103,7 +102,7 @@ public async Task SpecifiedInCodeInstructsConnectorNotToInvokeKernelFunctionForS await next(context); }); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.None }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice() }; string result = ""; diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index ccc6ce433c1e..bbec12a63e3c 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -50,7 +50,7 @@ public AutoFunctionChoiceBehavior(IEnumerable functions) /// will be disabled. This is a safeguard against possible runaway execution if the model routinely re-requests /// the same function over and over. To disable auto invocation, this can be set to 0. /// - [JsonPropertyName("maximumAutoInvokeAttempts")] + [JsonPropertyName("maximum_auto_invoke_attempts")] public int MaximumAutoInvokeAttempts { get; set; } = DefaultMaximumAutoInvokeAttempts; /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index 045205fe4954..c3d1c9f0e2c7 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -32,7 +32,7 @@ public abstract class FunctionChoiceBehavior /// /// The subset of the 's plugins' function information. /// Indicates whether the functions should be automatically invoked by the AI service/connector. - /// An instance of one of the derivatives. + /// An instance of one of the . public static FunctionChoiceBehavior AutoFunctionChoice(IEnumerable? functions = null, bool autoInvoke = true) { return new AutoFunctionChoiceBehavior(functions ?? []) @@ -47,7 +47,7 @@ public static FunctionChoiceBehavior AutoFunctionChoice(IEnumerable /// The subset of the 's plugins' function information. /// Indicates whether the functions should be automatically invoked by the AI service/connector. - /// An instance of one of the derivatives. + /// An instance of one of the . public static FunctionChoiceBehavior RequiredFunctionChoice(IEnumerable? functions = null, bool autoInvoke = true) { return new RequiredFunctionChoiceBehavior(functions ?? []) @@ -57,11 +57,19 @@ public static FunctionChoiceBehavior RequiredFunctionChoice(IEnumerable - /// Gets an instance of the that does not provides any 's plugins' function information to the model. + /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior forces the model to not call any functions and only generate a user-facing message. /// - /// An instance of one of the derivatives. - public static FunctionChoiceBehavior None { get; } = new NoneFunctionChoiceBehavior(); + /// The subset of the 's plugins' function information. + /// An instance of one of the . + /// + /// Although this behavior prevents the model from calling any functions, the model can use the provided function information + /// to describe how it would complete the prompt if it had the ability to call the functions. + /// + public static FunctionChoiceBehavior NoneFunctionChoice(IEnumerable? functions = null) + { + return new NoneFunctionChoiceBehavior(functions ?? []); + } /// Returns the configuration specified by the . /// The caller context. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs index 2a1a459b7fc3..1671904ef7de 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -1,27 +1,104 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json.Serialization; namespace Microsoft.SemanticKernel; /// -/// Represents that does not provides any 's plugins' function information to the model. +/// Represents that does not provides any 's plugins' function information to the model by default. /// This behavior forces the model to not call any functions and only generate a user-facing message. /// +/// +/// Although this behavior prevents the model from calling any functions, the model can use the provided function information +/// to describe how it would complete the prompt if it had the ability to call the functions. +/// [Experimental("SKEXP0001")] public sealed class NoneFunctionChoiceBehavior : FunctionChoiceBehavior { + /// + /// List of the functions that the model can choose from. + /// + private readonly IEnumerable? _functions; + /// /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. /// public const string TypeDiscriminator = "none"; + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + public NoneFunctionChoiceBehavior() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The subset of the 's plugins' functions information. + public NoneFunctionChoiceBehavior(IEnumerable functions) + { + this._functions = functions; + this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); + } + + /// + /// Fully qualified names of subset of the 's plugins' functions information to provide to the model. + /// + [JsonPropertyName("functions")] + [JsonConverter(typeof(FunctionNameFormatJsonConverter))] + public IList? Functions { get; set; } + /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { + List? availableFunctions = null; + + // Handle functions provided via the 'Functions' property as function fully qualified names. + if (this.Functions is { } functionFQNs && functionFQNs.Any()) + { + availableFunctions = []; + + foreach (var functionFQN in functionFQNs) + { + var nameParts = FunctionName.Parse(functionFQN); + + // Check if the function is available in the kernel. + if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) + { + availableFunctions.Add(function); + continue; + } + + // Check if a function instance that was not imported into the kernel was provided through the constructor. + function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); + if (function is not null) + { + availableFunctions.Add(function); + continue; + } + + throw new KernelException($"The specified function {functionFQN} is not available."); + } + } + // Provide all functions from the kernel. + else if (context.Kernel is not null) + { + foreach (var plugin in context.Kernel.Plugins) + { + availableFunctions ??= []; + availableFunctions.AddRange(plugin); + } + } + return new FunctionChoiceBehaviorConfiguration() { Choice = FunctionChoice.None, + Functions = availableFunctions, }; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index cfafbb6934ed..9a9e953feea7 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -57,7 +57,7 @@ public RequiredFunctionChoiceBehavior(IEnumerable functions) /// will be disabled. This is a safeguard against possible runaway execution if the model routinely re-requests /// the same function over and over. To disable auto invocation, this can be set to 0. /// - [JsonPropertyName("maximumAutoInvokeAttempts")] + [JsonPropertyName("maximum_auto_invoke_attempts")] public int MaximumAutoInvokeAttempts { get; set; } = DefaultMaximumAutoInvokeAttempts; /// @@ -69,7 +69,7 @@ public RequiredFunctionChoiceBehavior(IEnumerable functions) /// if this is 1, the first request will include the functions, but the subsequent response sending back the functions' result /// will not include the functions for further use. /// - [JsonPropertyName("maximumUseAttempts")] + [JsonPropertyName("maximum_use_attempts")] public int MaximumUseAttempts { get; set; } = 1; /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs index 0f1e0ee45571..421748f52537 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs @@ -57,17 +57,18 @@ public string? ModelId /// /// /// To force the model to always call one or more functions, set the property to an instance returned - /// from method. - /// Pass list of the functions when calling the method. + /// from method. By default, all functions in the will be available. + /// To limit the functions available, pass a list of the functions when calling the method. /// /// /// To force the model to not call any functions and only generate a user-facing message, set the property to an instance returned - /// from property. + /// from property. By default, all functions in the will be available. + /// To limit the functions available, pass a list of the functions when calling the method. /// /// /// For all the behaviors that presume the model to call functions, auto-invoke behavior may be selected. If the service /// sends a request for a function call, if auto-invoke has been requested, the client will attempt to - /// resolve that function from the functions available in the , and if found, rather + /// resolve that function from the functions available, and if found, rather /// than returning the response back to the caller, it will handle the request automatically, invoking /// the function, and sending back the result. The intermediate messages will be retained in the provided . /// diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs index 95c632edb61c..f60a5c866578 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs @@ -16,7 +16,7 @@ public void ItShouldDeserializeAutoFunctionChoiceBehavior() { "type": "auto", "functions": ["p1.f1"], - "maximumAutoInvokeAttempts": 8 + "maximum_auto_invoke_attempts": 8 } """; @@ -38,8 +38,8 @@ public void ItShouldDeserializeRequiredFunctionChoiceBehavior() { "type": "required", "functions": ["p1.f1"], - "maximumUseAttempts": 2, - "maximumAutoInvokeAttempts": 8 + "maximum_use_attempts": 2, + "maximum_auto_invoke_attempts": 8 } """; diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs index 99aaf151d5df..ef00c269e8d6 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using Microsoft.SemanticKernel; using Xunit; @@ -18,7 +19,7 @@ public NoneFunctionChoiceBehaviorTests() } [Fact] - public void ItShouldAdvertiseNoFunctions() + public void ItShouldAdvertiseKernelFunctions() { // Arrange var plugin = GetTestPlugin(); @@ -32,7 +33,31 @@ public void ItShouldAdvertiseNoFunctions() // Assert Assert.NotNull(config); - Assert.Null(config.Functions); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); + } + + [Fact] + public void ItShouldAdvertiseFunctionsIfSpecified() + { + // Arrange + var plugin = GetTestPlugin(); + + // Act + var choiceBehavior = new NoneFunctionChoiceBehavior([plugin.ElementAt(0), plugin.ElementAt(2)]); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } private static KernelPlugin GetTestPlugin() diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/PromptExecutionSettingsTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/PromptExecutionSettingsTests.cs index 6408239dd154..8dc84446039c 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/PromptExecutionSettingsTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/PromptExecutionSettingsTests.cs @@ -61,7 +61,7 @@ public void PromptExecutionSettingsFreezeWorksAsExpected() Assert.NotNull(executionSettings.ExtensionData); Assert.Throws(() => executionSettings.ExtensionData.Add("results_per_prompt", 2)); Assert.Throws(() => executionSettings.ExtensionData["temperature"] = 1); - Assert.Throws(() => executionSettings.FunctionChoiceBehavior = FunctionChoiceBehavior.None); + Assert.Throws(() => executionSettings.FunctionChoiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice()); executionSettings!.Freeze(); // idempotent Assert.True(executionSettings.IsFrozen); diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs index 2d13f228fc01..9cb03f3e81a8 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs @@ -42,7 +42,7 @@ public void RequiredFunctionChoiceShouldBeUsed() public void NoneFunctionChoiceShouldBeUsed() { // Act - var choiceBehavior = FunctionChoiceBehavior.None; + var choiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice(); // Assert Assert.IsType(choiceBehavior); @@ -203,21 +203,45 @@ public void RequiredFunctionChoiceShouldAllowManualInvocation() } [Fact] - public void NoneFunctionChoiceShouldAdvertiseNoFunctions() + public void NoneFunctionChoiceShouldAdvertiseProvidedFunctions() + { + // Arrange + var plugin = GetTestPlugin(); + + // Act + var choiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice([plugin.ElementAt(0), plugin.ElementAt(2)]); + + var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); + + // Assert + Assert.NotNull(config); + + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); + } + + [Fact] + public void NoneFunctionChoiceShouldAdvertiseAllKernelFunctions() { // Arrange var plugin = GetTestPlugin(); this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.None; + var choiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice(); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); // Assert Assert.NotNull(config); - Assert.Null(config.Functions); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } private static KernelPlugin GetTestPlugin() diff --git a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs index d29527ba2e87..0d87274b7d30 100644 --- a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs @@ -154,7 +154,7 @@ public void DeserializingAutoFunctionCallingChoice() "model_id": "gpt-4", "function_choice_behavior": { "type": "auto", - "maximumAutoInvokeAttempts": 12, + "maximum_auto_invoke_attempts": 12, "functions":["p1.f1"] } } @@ -192,8 +192,8 @@ public void DeserializingRequiredFunctionCallingChoice() "model_id": "gpt-4", "function_choice_behavior": { "type": "required", - "maximumAutoInvokeAttempts": 11, - "maximumUseAttempts": 2, + "maximum_auto_invoke_attempts": 11, + "maximum_use_attempts": 2, "functions":["p1.f1"] } } @@ -232,7 +232,7 @@ public void DeserializingNoneFunctionCallingChoice() "default": { "model_id": "gpt-4", "function_choice_behavior": { - "type": "none", + "type": "none" } } } @@ -248,7 +248,8 @@ public void DeserializingNoneFunctionCallingChoice() var executionSettings = promptTemplateConfig.ExecutionSettings.Single().Value; - Assert.IsType(executionSettings.FunctionChoiceBehavior); + var noneFunctionCallChoice = executionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; + Assert.NotNull(noneFunctionCallChoice); } [Fact] From e6e600943609c6d49d8787cf17bbdabe1c57071c Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 16 May 2024 11:46:51 +0100 Subject: [PATCH 70/90] remove MaximumAutoInvokeAttempts and MaximumUseAttempts properties from the public API serface of the *FunctionChoiceBehavior classes --- .../Functions/KernelFunctionMarkdownTests.cs | 8 +- ...nctionChoiceBehaviorTypesConverterTests.cs | 6 - .../Yaml/Functions/KernelFunctionYamlTests.cs | 6 - ...omptExecutionSettingsTypeConverterTests.cs | 6 - .../OpenAIAutoFunctionChoiceBehaviorTests.cs | 98 +--------------- ...enAIRequiredFunctionChoiceBehaviorTests.cs | 110 +----------------- .../AutoFunctionChoiceBehavior.cs | 26 ++--- .../FunctionChoiceBehavior.cs | 20 ++-- .../RequiredFunctionChoiceBehavior.cs | 40 ++----- .../AutoFunctionChoiceBehaviorTests.cs | 39 ++----- .../FunctionNameFormatJsonConverterTests.cs | 10 +- .../RequiredFunctionChoiceBehaviorTests.cs | 59 ++-------- .../Functions/FunctionChoiceBehaviorTests.cs | 4 +- .../PromptTemplateConfigTests.cs | 8 -- 14 files changed, 64 insertions(+), 376 deletions(-) diff --git a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs index f7000cb9c5c0..363b28d2f2f9 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs @@ -50,7 +50,6 @@ public void ItShouldInitializeFunctionChoiceBehaviorsFromMarkdown() Assert.NotNull(autoFunctionChoiceBehavior.Functions); Assert.Single(autoFunctionChoiceBehavior.Functions); Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.First()); - Assert.Equal(8, autoFunctionChoiceBehavior.MaximumAutoInvokeAttempts); // RequiredFunctionCallChoice for service2 var service2ExecutionSettings = function.ExecutionSettings["service2"]; @@ -61,7 +60,6 @@ public void ItShouldInitializeFunctionChoiceBehaviorsFromMarkdown() Assert.NotNull(requiredFunctionChoiceBehavior.Functions); Assert.Single(requiredFunctionChoiceBehavior.Functions); Assert.Equal("p1-f1", requiredFunctionChoiceBehavior.Functions.First()); - Assert.Equal(2, requiredFunctionChoiceBehavior.MaximumUseAttempts); // NoneFunctionCallChoice for service3 var service3ExecutionSettings = function.ExecutionSettings["service3"]; @@ -101,8 +99,7 @@ These are AI execution settings "temperature": 0.7, "function_choice_behavior": { "type": "auto", - "functions": ["p1.f1"], - "maximum_auto_invoke_attempts": 8 + "functions": ["p1.f1"] } } } @@ -115,8 +112,7 @@ These are more AI execution settings "temperature": 0.8, "function_choice_behavior": { "type": "required", - "functions": ["p1.f1"], - "maximum_use_attempts": 2 + "functions": ["p1.f1"] } } } diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs index 225fcaea475f..d84f56d03afa 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs @@ -24,7 +24,6 @@ public void ItShouldDeserializeAutoFunctionChoiceBehavior() var yaml = """ type: auto - maximum_auto_invoke_attempts: 9 functions: - p1.f1 """; @@ -36,7 +35,6 @@ public void ItShouldDeserializeAutoFunctionChoiceBehavior() Assert.NotNull(behavior.Functions); Assert.Single(behavior.Functions); Assert.Equal("p1-f1", behavior.Functions.Single()); - Assert.Equal(9, behavior.MaximumAutoInvokeAttempts); } [Fact] @@ -50,8 +48,6 @@ public void ItShouldDeserializeRequiredFunctionChoiceBehavior() var yaml = """ type: required - maximum_auto_invoke_attempts: 6 - maximum_use_attempts: 3 functions: - p2.f2 """; @@ -63,8 +59,6 @@ public void ItShouldDeserializeRequiredFunctionChoiceBehavior() Assert.NotNull(behavior.Functions); Assert.Single(behavior.Functions); Assert.Equal("p2-f2", behavior.Functions.Single()); - Assert.Equal(6, behavior.MaximumAutoInvokeAttempts); - Assert.Equal(3, behavior.MaximumUseAttempts); } [Fact] diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs index c37ef22a48ac..4db0022e2e87 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs @@ -99,7 +99,6 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; Assert.NotNull(autoFunctionChoiceBehavior?.Functions); Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.Single()); - Assert.Equal(9, autoFunctionChoiceBehavior.MaximumAutoInvokeAttempts); // Service with required function choice behavior var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; @@ -107,8 +106,6 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); Assert.Equal("p2-f2", requiredFunctionChoiceBehavior.Functions.Single()); - Assert.Equal(6, requiredFunctionChoiceBehavior.MaximumAutoInvokeAttempts); - Assert.Equal(3, requiredFunctionChoiceBehavior.MaximumUseAttempts); // Service with none function choice behavior var service3ExecutionSettings = promptTemplateConfig.ExecutionSettings["service3"]; @@ -196,7 +193,6 @@ string CreateYaml(object defaultValue) stop_sequences: [] function_choice_behavior: type: auto - maximum_auto_invoke_attempts: 9 functions: - p1.f1 service2: @@ -209,8 +205,6 @@ string CreateYaml(object defaultValue) stop_sequences: [ "foo", "bar", "baz" ] function_choice_behavior: type: required - maximum_auto_invoke_attempts: 6 - maximum_use_attempts: 3 functions: - p2.f2 service3: diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs index 9800a2954b86..fe5717f3cd68 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs @@ -53,7 +53,6 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; Assert.NotNull(autoFunctionChoiceBehavior?.Functions); Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.Single()); - Assert.Equal(9, autoFunctionChoiceBehavior.MaximumAutoInvokeAttempts); // Service with required function choice behavior var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; @@ -61,8 +60,6 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); Assert.Equal("p2-f2", requiredFunctionChoiceBehavior.Functions.Single()); - Assert.Equal(6, requiredFunctionChoiceBehavior.MaximumAutoInvokeAttempts); - Assert.Equal(3, requiredFunctionChoiceBehavior.MaximumUseAttempts); // Service with none function choice behavior var service3ExecutionSettings = promptTemplateConfig.ExecutionSettings["service3"]; @@ -96,7 +93,6 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() stop_sequences: [] function_choice_behavior: type: auto - maximum_auto_invoke_attempts: 9 functions: - p1.f1 service2: @@ -109,8 +105,6 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() stop_sequences: [ "foo", "bar", "baz" ] function_choice_behavior: type: required - maximum_auto_invoke_attempts: 6 - maximum_use_attempts: 3 functions: - p2.f2 service3: diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs index d247d500b133..ac80a8f2e946 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs @@ -77,7 +77,6 @@ public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionAutom temperature: 0.1 function_choice_behavior: type: auto - maximum_auto_invoke_attempts: 3 """"; var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); @@ -128,53 +127,6 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall Assert.Equal("GetCurrentDate", functionCall.FunctionName); } - [Fact] - public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionManuallyAsync() - { - // Arrange - this._kernel.ImportPluginFromType(); - - var invokedFunctions = new List(); - - this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => - { - invokedFunctions.Add(context.Function.Name); - await next(context); - }); - - var promptTemplate = """" - template_format: semantic-kernel - template: How many days until Christmas? - execution_settings: - default: - temperature: 0.1 - function_choice_behavior: - type: auto - maximum_auto_invoke_attempts: 0 - """"; - - var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); - - // Act - var result = await this._kernel.InvokeAsync(promptFunction); - - // Assert - Assert.NotNull(result); - - Assert.Empty(invokedFunctions); - - var responseContent = result.GetValue(); - Assert.NotNull(responseContent); - - var functionCalls = FunctionCallContent.GetFunctionCalls(responseContent); - Assert.NotNull(functionCalls); - Assert.Single(functionCalls); - - var functionCall = functionCalls.First(); - Assert.Equal("DateTimeUtils", functionCall.PluginName); - Assert.Equal("GetCurrentDate", functionCall.FunctionName); - } - [Fact] public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomaticallyForStreamingAsync() { @@ -228,7 +180,6 @@ public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionAutom temperature: 0.1 function_choice_behavior: type: auto - maximum_auto_invoke_attempts: 3 """"; var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); @@ -282,51 +233,6 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall Assert.Empty(invokedFunctions); } - [Fact] - public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionManuallyForStreamingAsync() - { - // Arrange - this._kernel.ImportPluginFromType(); - - var invokedFunctions = new List(); - - this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => - { - invokedFunctions.Add(context.Function.Name); - await next(context); - }); - - var promptTemplate = """" - template_format: semantic-kernel - template: How many days until Christmas? - execution_settings: - default: - temperature: 0.1 - function_choice_behavior: - type: auto - maximum_auto_invoke_attempts: 0 - """"; - - var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); - - var functionsForManualInvocation = new List(); - - // Act - await foreach (var content in promptFunction.InvokeStreamingAsync(this._kernel)) - { - if (content.ToolCallUpdate is StreamingFunctionToolCallUpdate functionUpdate && !string.IsNullOrEmpty(functionUpdate.Name)) - { - functionsForManualInvocation.Add(functionUpdate.Name); - } - } - - // Assert - Assert.Single(functionsForManualInvocation); - Assert.Contains("DateTimeUtils-GetCurrentDate", functionsForManualInvocation); - - Assert.Empty(invokedFunctions); - } - [Fact] public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManuallyAsync() { @@ -342,7 +248,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManu }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice([plugin.ElementAt(1)], autoInvoke: false) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(false, [plugin.ElementAt(1)]) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -379,7 +285,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManu var functionsForManualInvocation = new List(); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice([plugin.ElementAt(1)], autoInvoke: false) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(false, [plugin.ElementAt(1)]) }; // Act await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs index 98f1dda1024c..d4590f621be3 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs @@ -45,7 +45,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomat }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([plugin.ElementAt(1)], autoInvoke: true) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(true, [plugin.ElementAt(1)]) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -80,7 +80,6 @@ public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionAutom type: required functions: - DateTimeUtils.GetCurrentDate - maximum_auto_invoke_attempts: 3 """"; var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); @@ -111,7 +110,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([plugin.ElementAt(1)], autoInvoke: false) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(false, [plugin.ElementAt(1)]) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -132,55 +131,6 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall Assert.Equal("GetCurrentDate", functionCall.FunctionName); } - [Fact] - public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionManuallyAsync() - { - // Arrange - this._kernel.ImportPluginFromType(); - - var invokedFunctions = new List(); - - this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => - { - invokedFunctions.Add(context.Function.Name); - await next(context); - }); - - var promptTemplate = """" - template_format: semantic-kernel - template: How many days until Christmas? - execution_settings: - default: - temperature: 0.1 - function_choice_behavior: - type: required - functions: - - DateTimeUtils.GetCurrentDate - maximum_auto_invoke_attempts: 0 - """"; - - var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); - - // Act - var result = await this._kernel.InvokeAsync(promptFunction); - - // Assert - Assert.NotNull(result); - - Assert.Empty(invokedFunctions); - - var responseContent = result.GetValue(); - Assert.NotNull(responseContent); - - var functionCalls = FunctionCallContent.GetFunctionCalls(responseContent); - Assert.NotNull(functionCalls); - Assert.Single(functionCalls); - - var functionCall = functionCalls.First(); - Assert.Equal("DateTimeUtils", functionCall.PluginName); - Assert.Equal("GetCurrentDate", functionCall.FunctionName); - } - [Fact] public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomaticallyForStreamingAsync() { @@ -196,7 +146,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomat await next(context); }); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([plugin.ElementAt(1)], autoInvoke: true) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(true, [plugin.ElementAt(1)]) }; string result = ""; @@ -237,7 +187,6 @@ public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionAutom type: required functions: - DateTimeUtils.GetCurrentDate - maximum_auto_invoke_attempts: 3 """"; var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); @@ -274,7 +223,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall var functionsForManualInvocation = new List(); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([plugin.ElementAt(1)], autoInvoke: false) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(false, [plugin.ElementAt(1)]) }; // Act await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) @@ -292,53 +241,6 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall Assert.Empty(invokedFunctions); } - [Fact] - public async Task SpecifiedInPromptInstructsConnectorToInvokeKernelFunctionManuallyForStreamingAsync() - { - // Arrange - this._kernel.ImportPluginFromType(); - - var invokedFunctions = new List(); - - this._autoFunctionInvocationFilter.RegisterFunctionInvocationHandler(async (context, next) => - { - invokedFunctions.Add(context.Function.Name); - await next(context); - }); - - var promptTemplate = """" - template_format: semantic-kernel - template: How many days until Christmas? - execution_settings: - default: - temperature: 0.1 - function_choice_behavior: - type: required - functions: - - DateTimeUtils.GetCurrentDate - maximum_auto_invoke_attempts: 0 - """"; - - var promptFunction = KernelFunctionYaml.FromPromptYaml(promptTemplate); - - var functionsForManualInvocation = new List(); - - // Act - await foreach (var content in promptFunction.InvokeStreamingAsync(this._kernel)) - { - if (content.ToolCallUpdate is StreamingFunctionToolCallUpdate functionUpdate && !string.IsNullOrEmpty(functionUpdate.Name)) - { - functionsForManualInvocation.Add(functionUpdate.Name); - } - } - - // Assert - Assert.Single(functionsForManualInvocation); - Assert.Contains("DateTimeUtils-GetCurrentDate", functionsForManualInvocation); - - Assert.Empty(invokedFunctions); - } - [Fact] public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManuallyAsync() { @@ -354,7 +256,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManu }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([plugin.ElementAt(1)], autoInvoke: false) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(false, [plugin.ElementAt(1)]) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -391,7 +293,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManu var functionsForManualInvocation = new List(); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice([plugin.ElementAt(1)], autoInvoke: false) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(false, [plugin.ElementAt(1)]) }; // Act await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index bbec12a63e3c..7bc1d5ce6b2b 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -19,6 +19,11 @@ public sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior /// private readonly IEnumerable? _functions; + /// + /// The maximum number of function auto-invokes that can be made in a single user request. + /// + private readonly int _maximumAutoInvokeAttempts = DefaultMaximumAutoInvokeAttempts; + /// /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. /// @@ -35,24 +40,15 @@ public AutoFunctionChoiceBehavior() /// /// Initializes a new instance of the class. /// + /// Indicates whether the functions should be automatically invoked by the AI service/connector. /// The subset of the 's plugins' functions information. - public AutoFunctionChoiceBehavior(IEnumerable functions) + public AutoFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable? functions = null) { this._functions = functions; - this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); + this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); + this._maximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; } - /// - /// The maximum number of function auto-invokes that can be made in a single user request. - /// - /// - /// After this number of iterations as part of a single user request is reached, auto-invocation - /// will be disabled. This is a safeguard against possible runaway execution if the model routinely re-requests - /// the same function over and over. To disable auto invocation, this can be set to 0. - /// - [JsonPropertyName("maximum_auto_invoke_attempts")] - public int MaximumAutoInvokeAttempts { get; set; } = DefaultMaximumAutoInvokeAttempts; - /// /// Fully qualified names of subset of the 's plugins' functions information to provide to the model. /// @@ -63,7 +59,7 @@ public AutoFunctionChoiceBehavior(IEnumerable functions) /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { - bool autoInvoke = this.MaximumAutoInvokeAttempts > 0; + bool autoInvoke = this._maximumAutoInvokeAttempts > 0; // If auto-invocation is specified, we need a kernel to be able to invoke the functions. // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions @@ -127,7 +123,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho { Choice = FunctionChoice.Auto, Functions = availableFunctions, - MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts, + MaximumAutoInvokeAttempts = this._maximumAutoInvokeAttempts, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index c3d1c9f0e2c7..64f035b1e0a8 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -24,36 +24,30 @@ public abstract class FunctionChoiceBehavior /// will be disabled. This is a safeguard against possible runaway execution if the model routinely re-requests /// the same function over and over. /// - protected const int DefaultMaximumAutoInvokeAttempts = 5; + protected const int DefaultMaximumAutoInvokeAttempts = 10; /// /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior allows the model to decide whether to call the functions and, if so, which ones to call. /// - /// The subset of the 's plugins' function information. /// Indicates whether the functions should be automatically invoked by the AI service/connector. + /// The subset of the 's plugins' function information. /// An instance of one of the . - public static FunctionChoiceBehavior AutoFunctionChoice(IEnumerable? functions = null, bool autoInvoke = true) + public static FunctionChoiceBehavior AutoFunctionChoice(bool autoInvoke = true, IEnumerable? functions = null) { - return new AutoFunctionChoiceBehavior(functions ?? []) - { - MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0 - }; + return new AutoFunctionChoiceBehavior(autoInvoke, functions); } /// /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. /// - /// The subset of the 's plugins' function information. /// Indicates whether the functions should be automatically invoked by the AI service/connector. + /// The subset of the 's plugins' function information. /// An instance of one of the . - public static FunctionChoiceBehavior RequiredFunctionChoice(IEnumerable? functions = null, bool autoInvoke = true) + public static FunctionChoiceBehavior RequiredFunctionChoice(bool autoInvoke = true, IEnumerable? functions = null) { - return new RequiredFunctionChoiceBehavior(functions ?? []) - { - MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0 - }; + return new RequiredFunctionChoiceBehavior(autoInvoke, functions); } /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index 9a9e953feea7..e8ea43030bef 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -19,6 +19,11 @@ public sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior /// private readonly IEnumerable? _functions; + /// + /// The maximum number of function auto-invokes that can be made in a single user request. + /// + private readonly int _maximumAutoInvokeAttempts = DefaultMaximumAutoInvokeAttempts; + /// /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. /// @@ -35,11 +40,13 @@ public RequiredFunctionChoiceBehavior() /// /// Initializes a new instance of the class. /// + /// Indicates whether the functions should be automatically invoked by the AI service/connector. /// The subset of the 's plugins' functions information. - public RequiredFunctionChoiceBehavior(IEnumerable functions) + public RequiredFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable? functions = null) { this._functions = functions; - this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); + this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); + this._maximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; } /// @@ -49,33 +56,10 @@ public RequiredFunctionChoiceBehavior(IEnumerable functions) [JsonConverter(typeof(FunctionNameFormatJsonConverter))] public IList? Functions { get; set; } - /// - /// The maximum number of function auto-invokes that can be made in a single user request. - /// - /// - /// After this number of iterations as part of a single user request is reached, auto-invocation - /// will be disabled. This is a safeguard against possible runaway execution if the model routinely re-requests - /// the same function over and over. To disable auto invocation, this can be set to 0. - /// - [JsonPropertyName("maximum_auto_invoke_attempts")] - public int MaximumAutoInvokeAttempts { get; set; } = DefaultMaximumAutoInvokeAttempts; - - /// - /// Number of requests that are part of a single user interaction that should include this functions in the request. - /// - /// - /// This should be greater than or equal to . - /// Once this limit is reached, the functions will no longer be included in subsequent requests that are part of the user operation, e.g. - /// if this is 1, the first request will include the functions, but the subsequent response sending back the functions' result - /// will not include the functions for further use. - /// - [JsonPropertyName("maximum_use_attempts")] - public int MaximumUseAttempts { get; set; } = 1; - /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { - bool autoInvoke = this.MaximumAutoInvokeAttempts > 0; + bool autoInvoke = this._maximumAutoInvokeAttempts > 0; // If auto-invocation is specified, we need a kernel to be able to invoke the functions. // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions @@ -139,8 +123,8 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho { Choice = FunctionChoice.Required, Functions = availableFunctions, - MaximumAutoInvokeAttempts = this.MaximumAutoInvokeAttempts, - MaximumUseAttempts = this.MaximumUseAttempts, + MaximumAutoInvokeAttempts = this._maximumAutoInvokeAttempts, + MaximumUseAttempts = 1, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; } diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs index 0316e820328c..fe7bb83203e2 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs @@ -93,10 +93,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorForManualInvocat var plugin = GetTestPlugin(); // Act - var choiceBehavior = new AutoFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]) - { - MaximumAutoInvokeAttempts = 0 - }; + var choiceBehavior = new AutoFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)], autoInvoke: false); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -117,10 +114,7 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = new AutoFunctionChoiceBehavior() - { - MaximumAutoInvokeAttempts = 0 - }; + var choiceBehavior = new AutoFunctionChoiceBehavior(autoInvoke: false); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -135,7 +129,7 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() } [Fact] - public void ItShouldHaveFiveMaxAutoInvokeAttemptsByDefault() + public void ItShouldHaveDefaultMaxAutoInvokeAttempts() { // Arrange var plugin = GetTestPlugin(); @@ -148,7 +142,7 @@ public void ItShouldHaveFiveMaxAutoInvokeAttemptsByDefault() // Assert Assert.NotNull(config); - Assert.Equal(5, config.MaximumAutoInvokeAttempts); + Assert.Equal(10, config.MaximumAutoInvokeAttempts); } [Fact] @@ -159,16 +153,13 @@ public void ItShouldAllowAutoInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = new AutoFunctionChoiceBehavior() - { - MaximumAutoInvokeAttempts = 8 - }; + var choiceBehavior = new AutoFunctionChoiceBehavior(); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); // Assert Assert.NotNull(config); - Assert.Equal(8, config.MaximumAutoInvokeAttempts); + Assert.Equal(10, config.MaximumAutoInvokeAttempts); } [Fact] @@ -179,10 +170,7 @@ public void ItShouldAllowManualInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = new AutoFunctionChoiceBehavior() - { - MaximumAutoInvokeAttempts = 0 - }; + var choiceBehavior = new AutoFunctionChoiceBehavior(autoInvoke: false); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -216,10 +204,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedButNoKernelIsProvided var plugin = GetTestPlugin(); this._kernel.Plugins.Add(plugin); - var choiceBehavior = new AutoFunctionChoiceBehavior() - { - MaximumAutoInvokeAttempts = 8 - }; + var choiceBehavior = new AutoFunctionChoiceBehavior(); // Act var exception = Assert.Throws(() => @@ -236,10 +221,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegis // Arrange var plugin = GetTestPlugin(); - var choiceBehavior = new AutoFunctionChoiceBehavior([plugin.ElementAt(0)]) - { - MaximumAutoInvokeAttempts = 5 - }; + var choiceBehavior = new AutoFunctionChoiceBehavior(functions: [plugin.ElementAt(0)]); // Act var exception = Assert.Throws(() => @@ -257,9 +239,8 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste var plugin = GetTestPlugin(); this._kernel.Plugins.Add(plugin); - var choiceBehavior = new AutoFunctionChoiceBehavior() + var choiceBehavior = new AutoFunctionChoiceBehavior(autoInvoke: false) { - MaximumAutoInvokeAttempts = 0, Functions = ["MyPlugin-NonKernelFunction"] }; diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs index f60a5c866578..959a76bbf328 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs @@ -15,8 +15,7 @@ public void ItShouldDeserializeAutoFunctionChoiceBehavior() var json = """ { "type": "auto", - "functions": ["p1.f1"], - "maximum_auto_invoke_attempts": 8 + "functions": ["p1.f1"] } """; @@ -27,7 +26,6 @@ public void ItShouldDeserializeAutoFunctionChoiceBehavior() Assert.NotNull(behavior?.Functions); Assert.Single(behavior.Functions); Assert.Equal("p1-f1", behavior.Functions.Single()); - Assert.Equal(8, behavior.MaximumAutoInvokeAttempts); } [Fact] @@ -37,9 +35,7 @@ public void ItShouldDeserializeRequiredFunctionChoiceBehavior() var json = """ { "type": "required", - "functions": ["p1.f1"], - "maximum_use_attempts": 2, - "maximum_auto_invoke_attempts": 8 + "functions": ["p1.f1"] } """; @@ -50,7 +46,5 @@ public void ItShouldDeserializeRequiredFunctionChoiceBehavior() Assert.NotNull(behavior?.Functions); Assert.Single(behavior.Functions); Assert.Equal("p1-f1", behavior.Functions.Single()); - Assert.Equal(8, behavior.MaximumAutoInvokeAttempts); - Assert.Equal(2, behavior.MaximumUseAttempts); } } diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs index 8ca2416188f5..5d2df5ab1fef 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs @@ -93,10 +93,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorForManualInvocat var plugin = GetTestPlugin(); // Act - var choiceBehavior = new RequiredFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]) - { - MaximumAutoInvokeAttempts = 0 - }; + var choiceBehavior = new RequiredFunctionChoiceBehavior(false, [plugin.ElementAt(0), plugin.ElementAt(1)]); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -117,10 +114,7 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = new RequiredFunctionChoiceBehavior() - { - MaximumAutoInvokeAttempts = 0 - }; + var choiceBehavior = new RequiredFunctionChoiceBehavior(false); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -135,7 +129,7 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() } [Fact] - public void ItShouldHaveFiveMaxAutoInvokeAttemptsByDefault() + public void ItShouldHaveDefaultMaxAutoInvokeAttempts() { // Arrange var plugin = GetTestPlugin(); @@ -148,7 +142,7 @@ public void ItShouldHaveFiveMaxAutoInvokeAttemptsByDefault() // Assert Assert.NotNull(config); - Assert.Equal(5, config.MaximumAutoInvokeAttempts); + Assert.Equal(10, config.MaximumAutoInvokeAttempts); } [Fact] @@ -159,16 +153,13 @@ public void ItShouldAllowAutoInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = new RequiredFunctionChoiceBehavior() - { - MaximumAutoInvokeAttempts = 8 - }; + var choiceBehavior = new RequiredFunctionChoiceBehavior(); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); // Assert Assert.NotNull(config); - Assert.Equal(8, config.MaximumAutoInvokeAttempts); + Assert.Equal(10, config.MaximumAutoInvokeAttempts); } [Fact] @@ -179,10 +170,7 @@ public void ItShouldAllowManualInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = new RequiredFunctionChoiceBehavior() - { - MaximumAutoInvokeAttempts = 0 - }; + var choiceBehavior = new RequiredFunctionChoiceBehavior(false); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -216,10 +204,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedButNoKernelIsProvided var plugin = GetTestPlugin(); this._kernel.Plugins.Add(plugin); - var choiceBehavior = new RequiredFunctionChoiceBehavior() - { - MaximumAutoInvokeAttempts = 8 - }; + var choiceBehavior = new RequiredFunctionChoiceBehavior(); // Act var exception = Assert.Throws(() => @@ -236,10 +221,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegis // Arrange var plugin = GetTestPlugin(); - var choiceBehavior = new RequiredFunctionChoiceBehavior([plugin.ElementAt(0)]) - { - MaximumAutoInvokeAttempts = 5 - }; + var choiceBehavior = new RequiredFunctionChoiceBehavior(true, [plugin.ElementAt(0)]); // Act var exception = Assert.Throws(() => @@ -257,9 +239,8 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste var plugin = GetTestPlugin(); this._kernel.Plugins.Add(plugin); - var choiceBehavior = new RequiredFunctionChoiceBehavior() + var choiceBehavior = new RequiredFunctionChoiceBehavior(false) { - MaximumAutoInvokeAttempts = 0, Functions = ["MyPlugin-NonKernelFunction"] }; @@ -323,26 +304,6 @@ public void ItShouldHaveOneMaxUseAttemptsByDefault() Assert.Equal(1, config.MaximumUseAttempts); } - [Fact] - public void ItShouldAllowChangingMaxUseAttempts() - { - // Arrange - var plugin = GetTestPlugin(); - this._kernel.Plugins.Add(plugin); - - // Act - var choiceBehavior = new RequiredFunctionChoiceBehavior() - { - MaximumUseAttempts = 2 - }; - - var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); - - // Assert - Assert.NotNull(config); - Assert.Equal(2, config.MaximumUseAttempts); - } - private static KernelPlugin GetTestPlugin() { var function1 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function1"); diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs index 9cb03f3e81a8..57e03a60ffa3 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs @@ -105,7 +105,7 @@ public void AutoFunctionChoiceShouldAllowAutoInvocation() // Assert Assert.NotNull(config); - Assert.Equal(5, config.MaximumAutoInvokeAttempts); + Assert.Equal(10, config.MaximumAutoInvokeAttempts); } [Fact] @@ -182,7 +182,7 @@ public void RequiredFunctionChoiceShouldAllowAutoInvocation() // Assert Assert.NotNull(config); - Assert.Equal(5, config.MaximumAutoInvokeAttempts); + Assert.Equal(10, config.MaximumAutoInvokeAttempts); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs index 0d87274b7d30..ef230e17a9e8 100644 --- a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs @@ -154,7 +154,6 @@ public void DeserializingAutoFunctionCallingChoice() "model_id": "gpt-4", "function_choice_behavior": { "type": "auto", - "maximum_auto_invoke_attempts": 12, "functions":["p1.f1"] } } @@ -176,8 +175,6 @@ public void DeserializingAutoFunctionCallingChoice() Assert.NotNull(autoFunctionCallChoice.Functions); Assert.Equal("p1-f1", autoFunctionCallChoice.Functions.Single()); - - Assert.Equal(12, autoFunctionCallChoice.MaximumAutoInvokeAttempts); } [Fact] @@ -192,8 +189,6 @@ public void DeserializingRequiredFunctionCallingChoice() "model_id": "gpt-4", "function_choice_behavior": { "type": "required", - "maximum_auto_invoke_attempts": 11, - "maximum_use_attempts": 2, "functions":["p1.f1"] } } @@ -216,9 +211,6 @@ public void DeserializingRequiredFunctionCallingChoice() Assert.NotNull(requiredFunctionCallChoice.Functions); Assert.Equal("p1-f1", requiredFunctionCallChoice.Functions.Single()); - - Assert.Equal(11, requiredFunctionCallChoice.MaximumAutoInvokeAttempts); - Assert.Equal(2, requiredFunctionCallChoice.MaximumUseAttempts); } [Fact] From 73e5bc53347b64b2a72e901decc45de25711a068 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 16 May 2024 13:23:17 +0100 Subject: [PATCH 71/90] remove FunctionChoiceBehaviorConfiguration.MaximumAutoInvokeAttempts property. --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 12 +++++++++- .../AutoFunctionChoiceBehavior.cs | 14 +++++------ .../FunctionChoiceBehaviors/FunctionChoice.cs | 1 + .../FunctionChoiceBehavior.cs | 10 -------- .../FunctionChoiceBehaviorConfiguration.cs | 10 ++------ .../FunctionNameFormatJsonConverter.cs | 2 ++ .../RequiredFunctionChoiceBehavior.cs | 14 +++++------ .../AutoFunctionChoiceBehaviorTests.cs | 23 +++---------------- .../RequiredFunctionChoiceBehaviorTests.cs | 23 +++---------------- .../Functions/FunctionChoiceBehaviorTests.cs | 8 +++---- 10 files changed, 38 insertions(+), 79 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index d852399cdf90..e6042c90d43f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -51,6 +51,16 @@ internal abstract class ClientCore /// private const int MaxInflightAutoInvokes = 5; + /// + /// The maximum number of function auto-invokes that can be made in a single user request. + /// + /// + /// After this number of iterations as part of a single user request is reached, auto-invocation + /// will be disabled. This is a safeguard against possible runaway execution if the model routinely re-requests + /// the same function over and over. + /// + private const int MaximumAutoInvokeAttempts = 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" }; @@ -1527,7 +1537,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts) result = new() { AllowAnyRequestedKernelFunction = config.AllowAnyRequestedKernelFunction, - MaximumAutoInvokeAttempts = config.MaximumAutoInvokeAttempts, + MaximumAutoInvokeAttempts = config.AutoInvoke ? MaximumAutoInvokeAttempts : 0, }; if (requestIndex >= config.MaximumUseAttempts) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index 7bc1d5ce6b2b..c3877d862cc0 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -20,9 +20,9 @@ public sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior private readonly IEnumerable? _functions; /// - /// The maximum number of function auto-invokes that can be made in a single user request. + /// Indicates whether the functions should be automatically invoked by the AI service/connector. /// - private readonly int _maximumAutoInvokeAttempts = DefaultMaximumAutoInvokeAttempts; + private readonly bool _autoInvoke = true; /// /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. @@ -44,9 +44,9 @@ public AutoFunctionChoiceBehavior() /// The subset of the 's plugins' functions information. public AutoFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable? functions = null) { + this._autoInvoke = autoInvoke; this._functions = functions; this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); - this._maximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; } /// @@ -59,14 +59,12 @@ public AutoFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { - bool autoInvoke = this._maximumAutoInvokeAttempts > 0; - // If auto-invocation is specified, we need a kernel to be able to invoke the functions. // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions // and then fail to do so, so we fail before we get to that point. This is an error // on the consumers behalf: if they specify auto-invocation with any functions, they must // specify the kernel and the kernel must contain those functions. - if (autoInvoke && context.Kernel is null) + if (this._autoInvoke && context.Kernel is null) { throw new KernelException("Auto-invocation for Auto choice behavior is not supported when no kernel is provided."); } @@ -91,7 +89,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho } // If auto-invocation is requested and no function is found in the kernel, fail early. - if (autoInvoke) + if (this._autoInvoke) { throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); } @@ -123,7 +121,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho { Choice = FunctionChoice.Auto, Functions = availableFunctions, - MaximumAutoInvokeAttempts = this._maximumAutoInvokeAttempts, + AutoInvoke = this._autoInvoke, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs index e6af18cbd49c..51bc301f23ad 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs @@ -9,6 +9,7 @@ namespace Microsoft.SemanticKernel; /// Represents an AI model's decision-making strategy for calling functions, offering predefined choices: Auto, Required, and None. /// Auto allows the model to decide if and which functions to call, Required enforces calling one or more functions, and None prevents any function calls, generating only a user-facing message. /// +[Experimental("SKEXP0001")] public readonly struct FunctionChoice : IEquatable { /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index 64f035b1e0a8..88d8b7d4930a 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -16,16 +16,6 @@ namespace Microsoft.SemanticKernel; [Experimental("SKEXP0001")] public abstract class FunctionChoiceBehavior { - /// - /// The default maximum number of function auto-invokes that can be made in a single user request. - /// - /// - /// After this number of iterations as part of a single user request is reached, auto-invocation - /// will be disabled. This is a safeguard against possible runaway execution if the model routinely re-requests - /// the same function over and over. - /// - protected const int DefaultMaximumAutoInvokeAttempts = 10; - /// /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior allows the model to decide whether to call the functions and, if so, which ones to call. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index 3e5f23ed461d..dbc71c21b3d7 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -22,20 +22,14 @@ public sealed class FunctionChoiceBehaviorConfiguration public IEnumerable? Functions { get; init; } /// - /// The maximum number of function auto-invokes that can be made in a single user request. + /// Indicates whether the functions should be automatically invoked by the AI service/connector. /// - /// - /// After this number of iterations as part of a single user request is reached, auto-invocation - /// will be disabled. This is a safeguard against possible runaway execution if the model routinely re-requests - /// the same function over and over. To disable auto invocation, this can be set to 0. - /// - public int? MaximumAutoInvokeAttempts { get; init; } + public bool AutoInvoke { get; init; } = true; /// /// Number of requests that are part of a single user interaction that should include this functions in the request. /// /// - /// This should be greater than or equal to . /// Once this limit is reached, the functions will no longer be included in subsequent requests that are part of the user operation, e.g. /// if this is 1, the first request will include the functions, but the subsequent response sending back the functions' result /// will not include the functions for further use. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs index ace55d5a059f..b828fa2f2c9a 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; @@ -11,6 +12,7 @@ namespace Microsoft.SemanticKernel; /// A custom JSON converter for converting function names in a JSON array. /// This converter replaces dots used as a function name separator in prompts with hyphens when reading and back when writing. /// +[Experimental("SKEXP0001")] public sealed class FunctionNameFormatJsonConverter : JsonConverter> { private const char PromptFunctionNameSeparator = '.'; diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index e8ea43030bef..6d97b9acadcb 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -20,9 +20,9 @@ public sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior private readonly IEnumerable? _functions; /// - /// The maximum number of function auto-invokes that can be made in a single user request. + /// Indicates whether the functions should be automatically invoked by the AI service/connector. /// - private readonly int _maximumAutoInvokeAttempts = DefaultMaximumAutoInvokeAttempts; + private readonly bool _autoInvoke = true; /// /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. @@ -44,9 +44,9 @@ public RequiredFunctionChoiceBehavior() /// The subset of the 's plugins' functions information. public RequiredFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable? functions = null) { + this._autoInvoke = autoInvoke; this._functions = functions; this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); - this._maximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; } /// @@ -59,14 +59,12 @@ public RequiredFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { - bool autoInvoke = this._maximumAutoInvokeAttempts > 0; - // If auto-invocation is specified, we need a kernel to be able to invoke the functions. // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions // and then fail to do so, so we fail before we get to that point. This is an error // on the consumers behalf: if they specify auto-invocation with any functions, they must // specify the kernel and the kernel must contain those functions. - if (autoInvoke && context.Kernel is null) + if (this._autoInvoke && context.Kernel is null) { throw new KernelException("Auto-invocation for Required choice behavior is not supported when no kernel is provided."); } @@ -91,7 +89,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho } // If auto-invocation is requested and no function is found in the kernel, fail early. - if (autoInvoke) + if (this._autoInvoke) { throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); } @@ -123,7 +121,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho { Choice = FunctionChoice.Required, Functions = availableFunctions, - MaximumAutoInvokeAttempts = this._maximumAutoInvokeAttempts, + AutoInvoke = this._autoInvoke, MaximumUseAttempts = 1, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs index fe7bb83203e2..bbcb3fb808f5 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs @@ -129,7 +129,7 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() } [Fact] - public void ItShouldHaveDefaultMaxAutoInvokeAttempts() + public void ItShouldAllowAutoInvocationByDefault() { // Arrange var plugin = GetTestPlugin(); @@ -142,24 +142,7 @@ public void ItShouldHaveDefaultMaxAutoInvokeAttempts() // Assert Assert.NotNull(config); - Assert.Equal(10, config.MaximumAutoInvokeAttempts); - } - - [Fact] - public void ItShouldAllowAutoInvocation() - { - // Arrange - var plugin = GetTestPlugin(); - this._kernel.Plugins.Add(plugin); - - // Act - var choiceBehavior = new AutoFunctionChoiceBehavior(); - - var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); - - // Assert - Assert.NotNull(config); - Assert.Equal(10, config.MaximumAutoInvokeAttempts); + Assert.True(config.AutoInvoke); } [Fact] @@ -176,7 +159,7 @@ public void ItShouldAllowManualInvocation() // Assert Assert.NotNull(config); - Assert.Equal(0, config.MaximumAutoInvokeAttempts); + Assert.False(config.AutoInvoke); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs index 5d2df5ab1fef..ee3db7369f49 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs @@ -129,7 +129,7 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() } [Fact] - public void ItShouldHaveDefaultMaxAutoInvokeAttempts() + public void ItShouldAllowAutoInvocationByDefault() { // Arrange var plugin = GetTestPlugin(); @@ -142,24 +142,7 @@ public void ItShouldHaveDefaultMaxAutoInvokeAttempts() // Assert Assert.NotNull(config); - Assert.Equal(10, config.MaximumAutoInvokeAttempts); - } - - [Fact] - public void ItShouldAllowAutoInvocation() - { - // Arrange - var plugin = GetTestPlugin(); - this._kernel.Plugins.Add(plugin); - - // Act - var choiceBehavior = new RequiredFunctionChoiceBehavior(); - - var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); - - // Assert - Assert.NotNull(config); - Assert.Equal(10, config.MaximumAutoInvokeAttempts); + Assert.True(config.AutoInvoke); } [Fact] @@ -176,7 +159,7 @@ public void ItShouldAllowManualInvocation() // Assert Assert.NotNull(config); - Assert.Equal(0, config.MaximumAutoInvokeAttempts); + Assert.False(config.AutoInvoke); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs index 57e03a60ffa3..9c1cbce1de38 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs @@ -105,7 +105,7 @@ public void AutoFunctionChoiceShouldAllowAutoInvocation() // Assert Assert.NotNull(config); - Assert.Equal(10, config.MaximumAutoInvokeAttempts); + Assert.True(config.AutoInvoke); } [Fact] @@ -122,7 +122,7 @@ public void AutoFunctionChoiceShouldAllowManualInvocation() // Assert Assert.NotNull(config); - Assert.Equal(0, config.MaximumAutoInvokeAttempts); + Assert.False(config.AutoInvoke); } [Fact] @@ -182,7 +182,7 @@ public void RequiredFunctionChoiceShouldAllowAutoInvocation() // Assert Assert.NotNull(config); - Assert.Equal(10, config.MaximumAutoInvokeAttempts); + Assert.True(config.AutoInvoke); } [Fact] @@ -199,7 +199,7 @@ public void RequiredFunctionChoiceShouldAllowManualInvocation() // Assert Assert.NotNull(config); - Assert.Equal(0, config.MaximumAutoInvokeAttempts); + Assert.False(config.AutoInvoke); } [Fact] From 4284e262dd5d4ec9cfa1ef2d0e109405a433023a Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 16 May 2024 13:55:24 +0100 Subject: [PATCH 72/90] remove FunctionChoiceBehaviorConfiguration.MaximumUseAttempts property --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 14 ++++++++++++-- .../FunctionChoiceBehaviorConfiguration.cs | 10 ---------- .../RequiredFunctionChoiceBehavior.cs | 1 - .../RequiredFunctionChoiceBehaviorTests.cs | 17 ----------------- 4 files changed, 12 insertions(+), 30 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index e6042c90d43f..231930471ec7 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -61,6 +61,16 @@ internal abstract class ClientCore /// private const int MaximumAutoInvokeAttempts = 128; + /// + /// Number of requests that are part of a single user interaction that should include this functions in the request. + /// + /// + /// Once this limit is reached, the functions will no longer be included in subsequent requests that are part of the user operation, e.g. + /// if this is 1, the first request will include the functions, but the subsequent response sending back the functions' result + /// will not include the functions for further use. + /// + private const int MaximumUseAttempts = 1; + /// 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" }; @@ -1540,12 +1550,12 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context MaximumAutoInvokeAttempts = config.AutoInvoke ? MaximumAutoInvokeAttempts : 0, }; - if (requestIndex >= config.MaximumUseAttempts) + if (config.Choice == FunctionChoice.Required && requestIndex >= MaximumUseAttempts) { // Don't add any tools as we've reached the maximum use attempts limit. if (this.Logger.IsEnabled(LogLevel.Debug)) { - this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the functions.", config.MaximumUseAttempts); + this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the functions.", MaximumUseAttempts); } return result; diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index dbc71c21b3d7..80175a293f77 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -26,16 +26,6 @@ public sealed class FunctionChoiceBehaviorConfiguration /// public bool AutoInvoke { get; init; } = true; - /// - /// Number of requests that are part of a single user interaction that should include this functions in the request. - /// - /// - /// Once this limit is reached, the functions will no longer be included in subsequent requests that are part of the user operation, e.g. - /// if this is 1, the first request will include the functions, but the subsequent response sending back the functions' result - /// will not include the functions for further use. - /// - public int? MaximumUseAttempts { get; init; } - /// /// Specifies whether validation against a specified list of functions is required before allowing the model to request a function from the kernel. /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index 6d97b9acadcb..9aa63c906772 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -122,7 +122,6 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho Choice = FunctionChoice.Required, Functions = availableFunctions, AutoInvoke = this._autoInvoke, - MaximumUseAttempts = 1, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; } diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs index ee3db7369f49..ece75a9e8e10 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs @@ -270,23 +270,6 @@ public void ItShouldNotAllowInvocationOfAnyRequestedKernelFunctionIfSubsetOfFunc Assert.False(config.AllowAnyRequestedKernelFunction); } - [Fact] - public void ItShouldHaveOneMaxUseAttemptsByDefault() - { - // Arrange - var plugin = GetTestPlugin(); - this._kernel.Plugins.Add(plugin); - - // Act - var choiceBehavior = new RequiredFunctionChoiceBehavior(); - - var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); - - // Assert - Assert.NotNull(config); - Assert.Equal(1, config.MaximumUseAttempts); - } - private static KernelPlugin GetTestPlugin() { var function1 = KernelFunctionFactory.CreateFromMethod(() => { }, "Function1"); From 2bbfaf1bc97edb080f37509a287174089ca08687 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 30 May 2024 14:05:18 +0100 Subject: [PATCH 73/90] fix formatting warning --- dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index d05898c53cfe..cd864a0007d0 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -634,7 +634,7 @@ internal async IAsyncEnumerable GetStreamingC OpenAIPromptExecutionSettings chatExecutionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); ValidateMaxTokens(chatExecutionSettings.MaxTokens); - var chatOptions = CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); + var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); var functionCallConfiguration = this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, 0); From 162a948c84ed4516c23385b8be2be7b4da8f2a78 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 31 May 2024 21:33:59 +0100 Subject: [PATCH 74/90] Update dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs Co-authored-by: Stephen Toub --- .../FunctionChoiceBehaviorConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index 80175a293f77..e1bbba27b365 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -6,7 +6,7 @@ namespace Microsoft.SemanticKernel; /// -/// Represent function choice behavior configuration. +/// Represent function choice behavior configuration produced by a . /// [Experimental("SKEXP0001")] public sealed class FunctionChoiceBehaviorConfiguration From 63619a86880c1553d5329cb1d820850c9488d226 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 31 May 2024 22:24:12 +0100 Subject: [PATCH 75/90] address PR comments --- .../AutoFunctionChoiceBehavior.cs | 8 +++++--- .../FunctionChoiceBehavior.cs | 16 +++++++++++++--- .../FunctionChoiceBehaviorConfiguration.cs | 17 ++++++++++++----- .../NoneFunctionChoiceBehavior.cs | 8 +++++--- .../RequiredFunctionChoiceBehavior.cs | 8 +++++--- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index c3877d862cc0..e9fe65991461 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel; /// -/// Represent that provides either all of the 's plugins' function information to the model or a specified subset. +/// Represents that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior allows the model to decide whether to call the functions and, if so, which ones to call. /// [Experimental("SKEXP0001")] @@ -41,7 +41,8 @@ public AutoFunctionChoiceBehavior() /// Initializes a new instance of the class. /// /// Indicates whether the functions should be automatically invoked by the AI service/connector. - /// The subset of the 's plugins' functions information. + /// The subset of the 's plugins' functions to provide to the model. + /// If null or empty, all 's plugins' functions are provided to the model. public AutoFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable? functions = null) { this._autoInvoke = autoInvoke; @@ -50,7 +51,8 @@ public AutoFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable - /// Fully qualified names of subset of the 's plugins' functions information to provide to the model. + /// Fully qualified names of subset of the 's plugins' functions to provide to the model. + /// If null or empty, all 's plugins' functions are provided to the model. /// [JsonPropertyName("functions")] [JsonConverter(typeof(FunctionNameFormatJsonConverter))] diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index 88d8b7d4930a..4054e77bc4a3 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -16,12 +16,20 @@ namespace Microsoft.SemanticKernel; [Experimental("SKEXP0001")] public abstract class FunctionChoiceBehavior { + /// + /// Creates a new instance of the class. + /// + internal FunctionChoiceBehavior() + { + } + /// /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior allows the model to decide whether to call the functions and, if so, which ones to call. /// /// Indicates whether the functions should be automatically invoked by the AI service/connector. - /// The subset of the 's plugins' function information. + /// The subset of the 's plugins' functions to provide to the model. + /// If null or empty, all 's plugins' functions are provided to the model. /// An instance of one of the . public static FunctionChoiceBehavior AutoFunctionChoice(bool autoInvoke = true, IEnumerable? functions = null) { @@ -33,7 +41,8 @@ public static FunctionChoiceBehavior AutoFunctionChoice(bool autoInvoke = true, /// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. /// /// Indicates whether the functions should be automatically invoked by the AI service/connector. - /// The subset of the 's plugins' function information. + /// The subset of the 's plugins' functions to provide to the model. + /// If null or empty, all 's plugins' functions are provided to the model. /// An instance of one of the . public static FunctionChoiceBehavior RequiredFunctionChoice(bool autoInvoke = true, IEnumerable? functions = null) { @@ -44,7 +53,8 @@ public static FunctionChoiceBehavior RequiredFunctionChoice(bool autoInvoke = tr /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior forces the model to not call any functions and only generate a user-facing message. /// - /// The subset of the 's plugins' function information. + /// The subset of the 's plugins' functions to provide to the model. + /// If null or empty, all 's plugins' functions are provided to the model. /// An instance of one of the . /// /// Although this behavior prevents the model from calling any functions, the model can use the provided function information diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index 80175a293f77..7b33afb3f24f 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -6,28 +6,35 @@ namespace Microsoft.SemanticKernel; /// -/// Represent function choice behavior configuration. +/// Represents function choice behavior configuration. /// [Experimental("SKEXP0001")] public sealed class FunctionChoiceBehaviorConfiguration { + /// + /// Creates a new instance of the class. + /// + internal FunctionChoiceBehaviorConfiguration() + { + } + /// /// Represents an AI model's decision-making strategy for calling functions. /// - public FunctionChoice Choice { get; init; } + public FunctionChoice Choice { get; internal set; } /// /// The functions available for AI model. /// - public IEnumerable? Functions { get; init; } + public IEnumerable? Functions { get; internal set; } /// /// Indicates whether the functions should be automatically invoked by the AI service/connector. /// - public bool AutoInvoke { get; init; } = true; + public bool AutoInvoke { get; internal set; } = true; /// /// Specifies whether validation against a specified list of functions is required before allowing the model to request a function from the kernel. /// - public bool? AllowAnyRequestedKernelFunction { get; init; } + public bool? AllowAnyRequestedKernelFunction { get; internal set; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs index 1671904ef7de..88e850aa99c9 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel; /// -/// Represents that does not provides any 's plugins' function information to the model by default. +/// Represents that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior forces the model to not call any functions and only generate a user-facing message. /// /// @@ -39,7 +39,8 @@ public NoneFunctionChoiceBehavior() /// /// Initializes a new instance of the class. /// - /// The subset of the 's plugins' functions information. + /// The subset of the 's plugins' functions to provide to the model. + /// If null or empty, all 's plugins' functions are provided to the model. public NoneFunctionChoiceBehavior(IEnumerable functions) { this._functions = functions; @@ -47,7 +48,8 @@ public NoneFunctionChoiceBehavior(IEnumerable functions) } /// - /// Fully qualified names of subset of the 's plugins' functions information to provide to the model. + /// Fully qualified names of subset of the 's plugins' functions to provide to the model. + /// If null or empty, all 's plugins' functions are provided to the model. /// [JsonPropertyName("functions")] [JsonConverter(typeof(FunctionNameFormatJsonConverter))] diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index 9aa63c906772..c417a14393d5 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel; /// -/// Represent that provides either all of the 's plugins' function information to the model or a specified subset. +/// Represents that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. /// [Experimental("SKEXP0001")] @@ -41,7 +41,8 @@ public RequiredFunctionChoiceBehavior() /// Initializes a new instance of the class. /// /// Indicates whether the functions should be automatically invoked by the AI service/connector. - /// The subset of the 's plugins' functions information. + /// The subset of the 's plugins' functions to provide to the model. + /// If null or empty, all 's plugins' functions are provided to the model. public RequiredFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable? functions = null) { this._autoInvoke = autoInvoke; @@ -50,7 +51,8 @@ public RequiredFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable - /// Fully qualified names of subset of the 's plugins' functions information to provide to the model. + /// Fully qualified names of subset of the 's plugins' functions to provide to the model. + /// If null or empty, all 's plugins' functions are provided to the model. /// [JsonPropertyName("functions")] [JsonConverter(typeof(FunctionNameFormatJsonConverter))] From 960176b687a2a6f939ba0c812d87767134ea0047 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 5 Jun 2024 10:13:18 +0100 Subject: [PATCH 76/90] fix: put more detailed failure response to code comment. --- dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index cd864a0007d0..c144682eb2d7 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1552,7 +1552,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context result = this.ConfigureFunctionCalling(kernel, chatOptions, requestIndex, toolCallBehavior); } - // Having already sent tools and with tool call information in history, the service can become unhappy ("[] is too short - 'tools'") + // Having already sent tools and with tool call information in history, the service can become unhappy "Invalid 'tools': empty array. Expected an array with minimum length 1, but got an empty array instead." // if we don't send any tools in subsequent requests, even if we say not to use any. // Similarly, if we say not to use any tool (ToolChoice = ChatCompletionsToolChoice.None) and dont provide any for the first request, // the service fails with "'tool_choice' is only allowed when 'tools' are specified." From 21e2f49f471f95b6453979e9602e8b8d9796ab0e Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 5 Jun 2024 10:28:02 +0100 Subject: [PATCH 77/90] fix: remove the "FunctionChoice" suffix from the `FunctionChoiceBehavior` static factory methods. --- .../AzureOpenAIChatCompletionServiceTests.cs | 6 ++--- .../OpenAIChatCompletionServiceTests.cs | 6 ++--- .../OpenAIPromptExecutionSettingsTests.cs | 2 +- .../OpenAIAutoFunctionChoiceBehaviorTests.cs | 12 ++++----- .../OpenAINoneFunctionChoiceBehaviorTests.cs | 4 +-- ...enAIRequiredFunctionChoiceBehaviorTests.cs | 12 ++++----- .../FunctionChoiceBehavior.cs | 6 ++--- .../AI/PromptExecutionSettings.cs | 6 ++--- .../AI/PromptExecutionSettingsTests.cs | 2 +- .../Functions/FunctionChoiceBehaviorTests.cs | 26 +++++++++---------- 10 files changed, 41 insertions(+), 41 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index 29c931bbd777..625b9f30f228 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -953,7 +953,7 @@ public async Task ItCreatesCorrectFunctionToolCallsWhenUsingAutoFunctionChoiceBe Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }); - var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; // Act await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); @@ -986,7 +986,7 @@ public async Task ItCreatesCorrectFunctionToolCallsWhenUsingRequiredFunctionChoi Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }); - var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice() }; + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Required() }; // Act await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); @@ -1018,7 +1018,7 @@ public async Task ItCreatesCorrectFunctionToolCallsWhenUsingNoneFunctionChoiceBe Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }); - var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice() }; + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() }; // Act await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs index 625203e802c5..3aa34d54af45 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs @@ -606,7 +606,7 @@ public async Task ItCreatesCorrectFunctionToolCallsWhenUsingAutoFunctionChoiceBe Content = new StringContent(ChatCompletionResponse) }; - var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice() }; + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; // Act await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); @@ -637,7 +637,7 @@ public async Task ItCreatesCorrectFunctionToolCallsWhenUsingRequiredFunctionChoi Content = new StringContent(ChatCompletionResponse) }; - var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice() }; + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Required() }; // Act await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); @@ -669,7 +669,7 @@ public async Task ItCreatesCorrectFunctionToolCallsWhenUsingNoneFunctionChoiceBe Content = new StringContent(ChatCompletionResponse) }; - var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice() }; + var executionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() }; // Act await chatCompletion.GetChatMessageContentsAsync([], executionSettings, kernel); diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs index 92341fcb4018..c29d060b5117 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs @@ -260,7 +260,7 @@ public void FromExecutionSettingsWithDataDoesNotIncludeEmptyStopSequences() public void ItRestoresOriginalFunctionChoiceBehavior() { // Arrange - var functionChoiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice(); + var functionChoiceBehavior = FunctionChoiceBehavior.None(); var originalExecutionSettings = new PromptExecutionSettings(); originalExecutionSettings.FunctionChoiceBehavior = functionChoiceBehavior; diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs index ac80a8f2e946..e2955da2bf64 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs @@ -44,7 +44,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomat }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: true) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: true) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -106,7 +106,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -141,7 +141,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomat await next(context); }); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: true) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: true) }; string result = ""; @@ -215,7 +215,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall var functionsForManualInvocation = new List(); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false) }; // Act await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) @@ -248,7 +248,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManu }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(false, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(false, [plugin.ElementAt(1)]) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -285,7 +285,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManu var functionsForManualInvocation = new List(); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(false, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(false, [plugin.ElementAt(1)]) }; // Act await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs index b3ad13b4b985..557072f69ba8 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAINoneFunctionChoiceBehaviorTests.cs @@ -42,7 +42,7 @@ public async Task SpecifiedInCodeInstructsConnectorNotToInvokeKernelFunctionAsyn }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice() }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -102,7 +102,7 @@ public async Task SpecifiedInCodeInstructsConnectorNotToInvokeKernelFunctionForS await next(context); }); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice() }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.None() }; string result = ""; diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs index d4590f621be3..ebaae89f3b04 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs @@ -45,7 +45,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomat }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(true, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Required(true, [plugin.ElementAt(1)]) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -110,7 +110,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(false, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Required(false, [plugin.ElementAt(1)]) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -146,7 +146,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomat await next(context); }); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(true, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Required(true, [plugin.ElementAt(1)]) }; string result = ""; @@ -223,7 +223,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall var functionsForManualInvocation = new List(); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(false, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Required(false, [plugin.ElementAt(1)]) }; // Act await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) @@ -256,7 +256,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManu }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(false, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Required(false, [plugin.ElementAt(1)]) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -293,7 +293,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManu var functionsForManualInvocation = new List(); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(false, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Required(false, [plugin.ElementAt(1)]) }; // Act await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index 4054e77bc4a3..19e789beb61d 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -31,7 +31,7 @@ internal FunctionChoiceBehavior() /// The subset of the 's plugins' functions to provide to the model. /// If null or empty, all 's plugins' functions are provided to the model. /// An instance of one of the . - public static FunctionChoiceBehavior AutoFunctionChoice(bool autoInvoke = true, IEnumerable? functions = null) + public static FunctionChoiceBehavior Auto(bool autoInvoke = true, IEnumerable? functions = null) { return new AutoFunctionChoiceBehavior(autoInvoke, functions); } @@ -44,7 +44,7 @@ public static FunctionChoiceBehavior AutoFunctionChoice(bool autoInvoke = true, /// The subset of the 's plugins' functions to provide to the model. /// If null or empty, all 's plugins' functions are provided to the model. /// An instance of one of the . - public static FunctionChoiceBehavior RequiredFunctionChoice(bool autoInvoke = true, IEnumerable? functions = null) + public static FunctionChoiceBehavior Required(bool autoInvoke = true, IEnumerable? functions = null) { return new RequiredFunctionChoiceBehavior(autoInvoke, functions); } @@ -60,7 +60,7 @@ public static FunctionChoiceBehavior RequiredFunctionChoice(bool autoInvoke = tr /// Although this behavior prevents the model from calling any functions, the model can use the provided function information /// to describe how it would complete the prompt if it had the ability to call the functions. /// - public static FunctionChoiceBehavior NoneFunctionChoice(IEnumerable? functions = null) + public static FunctionChoiceBehavior None(IEnumerable? functions = null) { return new NoneFunctionChoiceBehavior(functions ?? []); } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs index 3243de48979d..2e5bd8c69a98 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs @@ -52,17 +52,17 @@ public string? ModelId /// To disable function calling, and have the model only generate a user-facing message, set the property to null (the default). /// /// To allow the model to decide whether to call the functions and, if so, which ones to call, set the property to an instance returned - /// from method. By default, all functions in the will be available. + /// from method. By default, all functions in the will be available. /// To limit the functions available, pass a list of the functions when calling the method. /// /// /// To force the model to always call one or more functions, set the property to an instance returned - /// from method. By default, all functions in the will be available. + /// from method. By default, all functions in the will be available. /// To limit the functions available, pass a list of the functions when calling the method. /// /// /// To force the model to not call any functions and only generate a user-facing message, set the property to an instance returned - /// from property. By default, all functions in the will be available. + /// from property. By default, all functions in the will be available. /// To limit the functions available, pass a list of the functions when calling the method. /// /// diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/PromptExecutionSettingsTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/PromptExecutionSettingsTests.cs index 8dc84446039c..91ea2b7137c2 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/PromptExecutionSettingsTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/PromptExecutionSettingsTests.cs @@ -61,7 +61,7 @@ public void PromptExecutionSettingsFreezeWorksAsExpected() Assert.NotNull(executionSettings.ExtensionData); Assert.Throws(() => executionSettings.ExtensionData.Add("results_per_prompt", 2)); Assert.Throws(() => executionSettings.ExtensionData["temperature"] = 1); - Assert.Throws(() => executionSettings.FunctionChoiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice()); + Assert.Throws(() => executionSettings.FunctionChoiceBehavior = FunctionChoiceBehavior.None()); executionSettings!.Freeze(); // idempotent Assert.True(executionSettings.IsFrozen); diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs index 9c1cbce1de38..196ca39d6495 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs @@ -22,7 +22,7 @@ public FunctionChoiceBehaviorTests() public void AutoFunctionChoiceShouldBeUsed() { // Act - var choiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(); + var choiceBehavior = FunctionChoiceBehavior.Auto(); // Assert Assert.IsType(choiceBehavior); @@ -32,7 +32,7 @@ public void AutoFunctionChoiceShouldBeUsed() public void RequiredFunctionChoiceShouldBeUsed() { // Act - var choiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(); + var choiceBehavior = FunctionChoiceBehavior.Required(); // Assert Assert.IsType(choiceBehavior); @@ -42,7 +42,7 @@ public void RequiredFunctionChoiceShouldBeUsed() public void NoneFunctionChoiceShouldBeUsed() { // Act - var choiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice(); + var choiceBehavior = FunctionChoiceBehavior.None(); // Assert Assert.IsType(choiceBehavior); @@ -56,7 +56,7 @@ public void AutoFunctionChoiceShouldAdvertiseKernelFunctions() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(functions: null); + var choiceBehavior = FunctionChoiceBehavior.Auto(functions: null); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -78,7 +78,7 @@ public void AutoFunctionChoiceShouldAdvertiseProvidedFunctions() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]); + var choiceBehavior = FunctionChoiceBehavior.Auto(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -99,7 +99,7 @@ public void AutoFunctionChoiceShouldAllowAutoInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: true); + var choiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: true); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -116,7 +116,7 @@ public void AutoFunctionChoiceShouldAllowManualInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.AutoFunctionChoice(autoInvoke: false); + var choiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -133,7 +133,7 @@ public void RequiredFunctionChoiceShouldAdvertiseKernelFunctions() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(functions: null); + var choiceBehavior = FunctionChoiceBehavior.Required(functions: null); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -155,7 +155,7 @@ public void RequiredFunctionChoiceShouldAdvertiseProvidedFunctions() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]); + var choiceBehavior = FunctionChoiceBehavior.Required(functions: [plugin.ElementAt(0), plugin.ElementAt(1)]); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -176,7 +176,7 @@ public void RequiredFunctionChoiceShouldAllowAutoInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(autoInvoke: true); + var choiceBehavior = FunctionChoiceBehavior.Required(autoInvoke: true); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -193,7 +193,7 @@ public void RequiredFunctionChoiceShouldAllowManualInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.RequiredFunctionChoice(autoInvoke: false); + var choiceBehavior = FunctionChoiceBehavior.Required(autoInvoke: false); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -209,7 +209,7 @@ public void NoneFunctionChoiceShouldAdvertiseProvidedFunctions() var plugin = GetTestPlugin(); // Act - var choiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice([plugin.ElementAt(0), plugin.ElementAt(2)]); + var choiceBehavior = FunctionChoiceBehavior.None([plugin.ElementAt(0), plugin.ElementAt(2)]); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -230,7 +230,7 @@ public void NoneFunctionChoiceShouldAdvertiseAllKernelFunctions() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.NoneFunctionChoice(); + var choiceBehavior = FunctionChoiceBehavior.None(); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); From 1f13f3515ae233bcf2b4a43b87e09ca302be4a10 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 10 Jun 2024 13:18:04 +0100 Subject: [PATCH 78/90] fix: address PR comments --- .../OpenAI_FunctionCalling.cs | 2 +- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 62 +++--------- .../AzureSdk/OpenAIFunction.cs | 18 +++- .../OpenAIKernelFunctionMetadataExtensions.cs | 38 ++++++-- .../OpenAIPromptExecutionSettings.cs | 6 +- .../Connectors.OpenAI/ToolCallBehavior.cs | 95 ++++++++++--------- .../KernelFunctionMetadataExtensionsTests.cs | 8 +- .../FunctionCalling/OpenAIFunctionTests.cs | 49 +++++++++- .../OpenAI/ToolCallBehaviorTests.cs | 77 ++++++++------- .../Functions/KernelFunctionMarkdownTests.cs | 49 +++++----- ...nctionChoiceBehaviorTypesConverterTests.cs | 87 ----------------- .../Yaml/Functions/KernelFunctionYamlTests.cs | 36 +++++-- ...omptExecutionSettingsTypeConverterTests.cs | 37 ++++++-- .../FunctionChoiceBehaviorTypesConverter.cs | 84 ---------------- .../PromptExecutionSettingsTypeConverter.cs | 50 +++++++--- .../Connectors/OpenAI/OpenAIToolsTests.cs | 2 +- .../src/Functions/FunctionName.cs | 6 +- .../AutoFunctionChoiceBehavior.cs | 24 ++--- .../FunctionChoiceBehavior.cs | 16 ++-- .../FunctionChoiceBehaviorConfiguration.cs | 15 +-- .../FunctionNameFormatJsonConverter.cs | 64 ------------- .../NoneFunctionChoiceBehavior.cs | 25 ++--- .../RequiredFunctionChoiceBehavior.cs | 27 ++---- .../AutoFunctionChoiceBehaviorTests.cs | 56 +++++------ .../FunctionNameFormatJsonConverterTests.cs | 24 ++++- .../NoneFunctionChoiceBehaviorTests.cs | 18 ++-- .../RequiredFunctionChoiceBehaviorTests.cs | 56 +++++------ .../Functions/FunctionChoiceBehaviorTests.cs | 54 +++++------ .../PromptTemplateConfigTests.cs | 4 +- 29 files changed, 472 insertions(+), 617 deletions(-) delete mode 100644 dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs delete mode 100644 dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs delete mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs diff --git a/dotnet/samples/Concepts/AutoFunctionCalling/OpenAI_FunctionCalling.cs b/dotnet/samples/Concepts/AutoFunctionCalling/OpenAI_FunctionCalling.cs index bc985e885916..d2d7f6d84a50 100644 --- a/dotnet/samples/Concepts/AutoFunctionCalling/OpenAI_FunctionCalling.cs +++ b/dotnet/samples/Concepts/AutoFunctionCalling/OpenAI_FunctionCalling.cs @@ -139,7 +139,7 @@ public async Task RunAsync() } // Adding a simulated function call to the connector response message - var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); + var simulatedFunctionCall = new FunctionCallContent("weather_alert", id: "call_123"); result.Items.Add(simulatedFunctionCall); // Adding a simulated function result to chat history diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index e905e25f2b63..c630e91fe541 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1547,26 +1547,14 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context return result; } - // If both behaviors are specified, we can't handle that. - if (executionSettings.FunctionChoiceBehavior is not null && executionSettings.ToolCallBehavior is not null) - { - throw new ArgumentException($"{nameof(executionSettings.ToolCallBehavior)} and {nameof(executionSettings.FunctionChoiceBehavior)} cannot be used together."); - } - // Set the tool choice to none. If we end up wanting to use tools, we'll set it to the desired value. chatOptions.ToolChoice = ChatCompletionsToolChoice.None; chatOptions.Tools.Clear(); - // Handling new tool behavior represented by `PromptExecutionSettings.FunctionChoiceBehavior` property. if (executionSettings.FunctionChoiceBehavior is { } functionChoiceBehavior) { result = this.ConfigureFunctionCalling(kernel, chatOptions, requestIndex, functionChoiceBehavior); } - // Handling old-style tool call behavior represented by `OpenAIPromptExecutionSettings.ToolCallBehavior` property. - else if (executionSettings.ToolCallBehavior is { } toolCallBehavior) - { - result = this.ConfigureFunctionCalling(kernel, chatOptions, requestIndex, toolCallBehavior); - } // Having already sent tools and with tool call information in history, the service can become unhappy "Invalid 'tools': empty array. Expected an array with minimum length 1, but got an empty array instead." // if we don't send any tools in subsequent requests, even if we say not to use any. @@ -1605,13 +1593,13 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context if (config.Choice == FunctionChoice.Auto) { - chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; - - if (config.Functions is { } functions) + if (config.FunctionsMetadata is { } functionsMetadata && functionsMetadata.Any()) { - foreach (var function in functions) + chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; + + foreach (var functionMetadata in functionsMetadata) { - var functionDefinition = function.Metadata.ToOpenAIFunction().ToFunctionDefinition(); + var functionDefinition = functionMetadata.ToOpenAIFunction().ToFunctionDefinition(); chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); } } @@ -1621,14 +1609,14 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context if (config.Choice == FunctionChoice.Required) { - if (config.Functions is { } functions && functions.Any()) + if (config.FunctionsMetadata is { } functionsMetadata && functionsMetadata.Any()) { - if (functions.Count() > 1) + if (functionsMetadata.Count() > 1) { throw new KernelException("Only one required function is allowed."); } - var functionDefinition = functions.First().Metadata.ToOpenAIFunction().ToFunctionDefinition(); + var functionDefinition = functionsMetadata.First().ToOpenAIFunction().ToFunctionDefinition(); chatOptions.ToolChoice = new ChatCompletionsToolChoice(functionDefinition); chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); @@ -1639,13 +1627,13 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context if (config.Choice == FunctionChoice.None) { - chatOptions.ToolChoice = ChatCompletionsToolChoice.None; - - if (config.Functions is { } functions) + if (config.FunctionsMetadata is { } functionsMetadata && functionsMetadata.Any()) { - foreach (var function in functions) + chatOptions.ToolChoice = ChatCompletionsToolChoice.None; + + foreach (var functionMetadata in functionsMetadata) { - var functionDefinition = function.Metadata.ToOpenAIFunction().ToFunctionDefinition(); + var functionDefinition = functionMetadata.ToOpenAIFunction().ToFunctionDefinition(); chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); } } @@ -1655,28 +1643,4 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context throw new NotSupportedException($"Unsupported function choice '{config.Choice}'."); } - - private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(Kernel? kernel, ChatCompletionsOptions chatOptions, int requestIndex, ToolCallBehavior toolCallBehavior) - { - if (requestIndex >= 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 tools.", 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. - toolCallBehavior.ConfigureOptions(kernel, chatOptions); - } - - return new() - { - AllowAnyRequestedKernelFunction = toolCallBehavior.AllowAnyRequestedKernelFunction, - MaximumAutoInvokeAttempts = toolCallBehavior.MaximumAutoInvokeAttempts, - }; - } } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunction.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunction.cs index b51faa59c359..68abbd62588f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunction.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunction.cs @@ -15,13 +15,14 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// public sealed class OpenAIFunctionParameter { - internal OpenAIFunctionParameter(string? name, string? description, bool isRequired, Type? parameterType, KernelJsonSchema? schema) + internal OpenAIFunctionParameter(string? name, string? description, bool isRequired, Type? parameterType, KernelJsonSchema? schema, object? defaultValue = null) { this.Name = name ?? string.Empty; this.Description = description ?? string.Empty; this.IsRequired = isRequired; this.ParameterType = parameterType; this.Schema = schema; + this.DefaultValue = defaultValue; } /// Gets the name of the parameter. @@ -38,6 +39,9 @@ internal OpenAIFunctionParameter(string? name, string? description, bool isRequi /// Gets a JSON schema for the parameter, if known. public KernelJsonSchema? Schema { get; } + + /// Gets the default value of the parameter. + public object? DefaultValue { get; } } /// @@ -144,7 +148,7 @@ public FunctionDefinition ToFunctionDefinition() for (int i = 0; i < parameters.Count; i++) { var parameter = parameters[i]; - properties.Add(parameter.Name, parameter.Schema ?? GetDefaultSchemaForTypelessParameter(parameter.Description)); + properties.Add(parameter.Name, parameter.Schema ?? GetDefaultSchemaForTypelessParameter(GetDescription(parameter))); if (parameter.IsRequired) { required.Add(parameter.Name); @@ -165,6 +169,16 @@ public FunctionDefinition ToFunctionDefinition() Description = this.Description, Parameters = resultParameters, }; + + static string GetDescription(OpenAIFunctionParameter param) + { + if (InternalTypeConverter.ConvertToString(param.DefaultValue) is string stringValue && !string.IsNullOrEmpty(stringValue)) + { + return $"{param.Description} (default value: {stringValue})"; + } + + return param.Description; + } } /// Gets a for a typeless parameter with the specified description, defaulting to typeof(string) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIKernelFunctionMetadataExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIKernelFunctionMetadataExtensions.cs index 6859e1225dd6..adf1db651066 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIKernelFunctionMetadataExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIKernelFunctionMetadataExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Linq; namespace Microsoft.SemanticKernel.Connectors.OpenAI; @@ -25,10 +26,11 @@ public static OpenAIFunction ToOpenAIFunction(this KernelFunctionMetadata metada openAIParams[i] = new OpenAIFunctionParameter( param.Name, - GetDescription(param), + param.Description, param.IsRequired, param.ParameterType, - param.Schema); + param.Schema, + param.DefaultValue); } return new OpenAIFunction( @@ -40,15 +42,33 @@ public static OpenAIFunction ToOpenAIFunction(this KernelFunctionMetadata metada metadata.ReturnParameter.Description, metadata.ReturnParameter.ParameterType, metadata.ReturnParameter.Schema)); + } - static string GetDescription(KernelParameterMetadata param) + /// + /// Convert an to a . + /// + /// The object to convert. + /// An object. + public static KernelFunctionMetadata ToKernelFunctionMetadata(this OpenAIFunction function) + { + return new KernelFunctionMetadata(function.FunctionName) { - if (InternalTypeConverter.ConvertToString(param.DefaultValue) is string stringValue && !string.IsNullOrEmpty(stringValue)) + PluginName = function.PluginName, + Description = function.Description, + Parameters = function.Parameters?.Select(p => new KernelParameterMetadata(p.Name) { - return $"{param.Description} (default value: {stringValue})"; - } - - return param.Description; - } + Description = p.Description, + DefaultValue = p.DefaultValue, + IsRequired = p.IsRequired, + ParameterType = p.ParameterType, + Schema = p.Schema, + }).ToList() ?? [], + ReturnParameter = function.ReturnParameter is null ? new() : new KernelReturnParameterMetadata() + { + Description = function.ReturnParameter.Description, + ParameterType = function.ReturnParameter.ParameterType, + Schema = function.ReturnParameter.Schema, + }, + }; } } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs index 8707adde442c..deed6568f392 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs @@ -231,12 +231,11 @@ public IDictionary? TokenSelectionBiases /// public ToolCallBehavior? ToolCallBehavior { - get => this._toolCallBehavior; - + get => base.FunctionChoiceBehavior as ToolCallBehavior; set { this.ThrowIfFrozen(); - this._toolCallBehavior = value; + base.FunctionChoiceBehavior = value; } } @@ -424,7 +423,6 @@ public static OpenAIPromptExecutionSettings FromExecutionSettingsWithData(Prompt private long? _seed; private object? _responseFormat; private IDictionary? _tokenSelectionBiases; - private ToolCallBehavior? _toolCallBehavior; private string? _user; private string? _chatSystemPrompt; private bool? _logprobs; diff --git a/dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs b/dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs index 7a5490c736ea..d38ede8bd1c5 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs @@ -6,12 +6,11 @@ using System.Diagnostics; using System.Linq; using System.Text.Json; -using Azure.AI.OpenAI; namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// Represents a behavior for OpenAI tool calls. -public abstract class ToolCallBehavior +public abstract class ToolCallBehavior : FunctionChoiceBehavior { // 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: @@ -118,11 +117,6 @@ private ToolCallBehavior(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); - /// /// 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. @@ -133,22 +127,31 @@ internal KernelFunctions(bool autoInvoke) : base(autoInvoke) { } public override string ToString() => $"{nameof(KernelFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0})"; - internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) + public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { + List? functionsMetadata = null; + // If no kernel is provided, we don't have any tools to provide. - if (kernel is not null) + if (context.Kernel is not null) { // Provide all functions from the kernel. - IList functions = kernel.Plugins.GetFunctionsMetadata(); + IList functions = context.Kernel.Plugins.GetFunctionsMetadata(); if (functions.Count > 0) { - options.ToolChoice = ChatCompletionsToolChoice.Auto; for (int i = 0; i < functions.Count; i++) { - options.Tools.Add(new ChatCompletionsFunctionToolDefinition(functions[i].ToOpenAIFunction().ToFunctionDefinition())); + (functionsMetadata ??= []).Add(functions[i]); } } } + + return new FunctionChoiceBehaviorConfiguration() + { + Choice = FunctionChoice.Auto, + FunctionsMetadata = functionsMetadata, + AutoInvoke = this.MaximumAutoInvokeAttempts > 0, + AllowAnyRequestedKernelFunction = this.AllowAnyRequestedKernelFunction, + }; } internal override bool AllowAnyRequestedKernelFunction => true; @@ -160,29 +163,19 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o internal sealed class EnabledFunctions : ToolCallBehavior { private readonly OpenAIFunction[] _openAIFunctions; - private readonly ChatCompletionsFunctionToolDefinition[] _functions; public EnabledFunctions(IEnumerable functions, bool autoInvoke) : base(autoInvoke) { this._openAIFunctions = functions.ToArray(); - - var defs = new ChatCompletionsFunctionToolDefinition[this._openAIFunctions.Length]; - for (int i = 0; i < defs.Length; i++) - { - defs[i] = new ChatCompletionsFunctionToolDefinition(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._openAIFunctions.Select(f => f.FunctionName))}"; - internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) + public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { - OpenAIFunction[] openAIFunctions = this._openAIFunctions; - ChatCompletionsFunctionToolDefinition[] functions = this._functions; - Debug.Assert(openAIFunctions.Length == functions.Length); + List? functionsMetadata = null; - if (openAIFunctions.Length > 0) + if (this._openAIFunctions.Length > 0) { bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; @@ -191,29 +184,40 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o // and then fail to do so, so we fail before we get to that point. This is an error // on the consumers behalf: if they specify auto-invocation with any functions, they must // specify the kernel and the kernel must contain those functions. - if (autoInvoke && kernel is null) + if (autoInvoke && context.Kernel is null) { throw new KernelException($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided."); } - options.ToolChoice = ChatCompletionsToolChoice.Auto; - for (int i = 0; i < openAIFunctions.Length; i++) + for (int i = 0; i < this._openAIFunctions.Length; i++) { + functionsMetadata ??= []; + // Make sure that if auto-invocation is specified, every enabled function can be found in the kernel. if (autoInvoke) { - Debug.Assert(kernel is not null); - OpenAIFunction f = openAIFunctions[i]; - if (!kernel!.Plugins.TryGetFunction(f.PluginName, f.FunctionName, out _)) + Debug.Assert(context.Kernel is not null); + OpenAIFunction f = this._openAIFunctions[i]; + if (!context.Kernel!.Plugins.TryGetFunction(f.PluginName, f.FunctionName, out var func)) { throw new KernelException($"The specified {nameof(EnabledFunctions)} function {f.FullyQualifiedName} is not available in the kernel."); } + functionsMetadata.Add(func.Metadata); + } + else + { + functionsMetadata.Add(this._openAIFunctions[i].ToKernelFunctionMetadata()); } - - // Add the function. - options.Tools.Add(functions[i]); } } + + return new FunctionChoiceBehaviorConfiguration() + { + Choice = FunctionChoice.Auto, + FunctionsMetadata = functionsMetadata, + AutoInvoke = this.MaximumAutoInvokeAttempts > 0, + AllowAnyRequestedKernelFunction = this.AllowAnyRequestedKernelFunction, + }; } } @@ -221,19 +225,15 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o internal sealed class RequiredFunction : ToolCallBehavior { private readonly OpenAIFunction _function; - private readonly ChatCompletionsFunctionToolDefinition _tool; - private readonly ChatCompletionsToolChoice _choice; public RequiredFunction(OpenAIFunction function, bool autoInvoke) : base(autoInvoke) { this._function = function; - this._tool = new ChatCompletionsFunctionToolDefinition(function.ToFunctionDefinition()); - this._choice = new ChatCompletionsToolChoice(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._function.FunctionName}"; - internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) + public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; @@ -242,19 +242,24 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o // and then fail to do so, so we fail before we get to that point. This is an error // on the consumers behalf: if they specify auto-invocation with any functions, they must // specify the kernel and the kernel must contain those functions. - if (autoInvoke && kernel is null) + if (autoInvoke && context.Kernel is null) { throw new KernelException($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided."); } // Make sure that if auto-invocation is specified, the required function can be found in the kernel. - if (autoInvoke && !kernel!.Plugins.TryGetFunction(this._function.PluginName, this._function.FunctionName, out _)) + if (autoInvoke && !context.Kernel!.Plugins.TryGetFunction(this._function.PluginName, this._function.FunctionName, out _)) { 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 new FunctionChoiceBehaviorConfiguration() + { + Choice = FunctionChoice.Required, + FunctionsMetadata = [this._function.ToKernelFunctionMetadata()], + AutoInvoke = autoInvoke, + AllowAnyRequestedKernelFunction = this.AllowAnyRequestedKernelFunction, + }; } /// Gets how many requests are part of a single interaction should include this tool in the request. diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs index b45fc64b60ba..dd601c813673 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs @@ -106,7 +106,7 @@ public void ItCanConvertToOpenAIFunctionWithParameter(bool withSchema) // Assert Assert.Equal(param1.Name, outputParam.Name); - Assert.Equal("This is param1 (default value: 1)", outputParam.Description); + Assert.Equal("This is param1", outputParam.Description); Assert.Equal(param1.IsRequired, outputParam.IsRequired); Assert.NotNull(outputParam.Schema); Assert.Equal("integer", outputParam.Schema.RootElement.GetProperty("type").GetString()); @@ -196,7 +196,7 @@ public void ItCanCreateValidOpenAIFunctionManualForPlugin() // Assert 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"}}}""", + """{"type":"object","required":["parameter1"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"type":"string","enum":["Value1","Value2"],"description":"Enum parameter (default value: Value2)"},"parameter3":{"type":["string","null"],"format":"date-time","description":"DateTime parameter"}}}""", result.Parameters.ToString() ); } @@ -247,8 +247,8 @@ private sealed class MyPlugin [KernelFunction, Description("My sample function.")] public string MyFunction( [Description("String parameter")] string parameter1, - [Description("Enum parameter")] MyEnum parameter2, - [Description("DateTime parameter")] DateTime parameter3 + [Description("Enum parameter")] MyEnum parameter2 = MyEnum.Value2, + [Description("DateTime parameter")] DateTime? parameter3 = null ) { return "return"; diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs index a9f94d81a673..dcf33c8cebff 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs @@ -92,12 +92,12 @@ public void ItCanConvertToFunctionDefinitionWithPluginName() [Fact] public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParameterType() { - string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; + string expectedParameterSchema = """{"type":"object","required":["param1","param2"],"properties":{"param1":{"type":"string","description":"String param 1"},"param2":{"type":"integer","description":"Int param 2"},"param3":{"type":"number","description":"double param 2 (default value: 34.8)"}}}"""; KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] { KernelFunctionFactory.CreateFromMethod( - [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => "", + [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2, [Description("double param 2")] double param3 = 34.8) => "", "TestFunction", "My test function") }); @@ -118,12 +118,12 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParamete [Fact] public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParameterType() { - string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; + string expectedParameterSchema = """{"type":"object","required":["param1","param2"],"properties":{"param1":{"type":"string","description":"String param 1"},"param2":{"type":"integer","description":"Int param 2"},"param3":{"type":"number","description":"double param 2 (default value: 34.8)"}}}"""; KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] { KernelFunctionFactory.CreateFromMethod( - [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => { }, + [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2, [Description("double param 2")] double param3 = 34.8) => { }, "TestFunction", "My test function") }); @@ -178,6 +178,47 @@ public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescript JsonSerializer.Serialize(pd.properties.First().Value.RootElement)); } + [Fact] + public void ItCanConvertToFunctionMetadata() + { + // Arrange + OpenAIFunction f = new("p1", "f1", "description", new[] + { + new OpenAIFunctionParameter("param1", "param1 description", true, typeof(string), KernelJsonSchema.Parse("""{ "type":"string" }""")), + new OpenAIFunctionParameter("param2", "param2 description", false, typeof(int), KernelJsonSchema.Parse("""{ "type":"integer" }""")), + }, + new OpenAIFunctionReturnParameter("return description", typeof(string), KernelJsonSchema.Parse("""{ "type":"string" }"""))); + + // Act + KernelFunctionMetadata result = f.ToKernelFunctionMetadata(); + + // Assert + Assert.Equal("p1", result.PluginName); + Assert.Equal("f1", result.Name); + Assert.Equal("description", result.Description); + + Assert.Equal(2, result.Parameters.Count); + + var param1 = result.Parameters[0]; + Assert.Equal("param1", param1.Name); + Assert.Equal("param1 description", param1.Description); + Assert.True(param1.IsRequired); + Assert.Equal(typeof(string), param1.ParameterType); + Assert.Equal("string", param1.Schema?.RootElement.GetProperty("type").GetString()); + + var param2 = result.Parameters[1]; + Assert.Equal("param2", param2.Name); + Assert.Equal("param2 description", param2.Description); + Assert.False(param2.IsRequired); + Assert.Equal(typeof(int), param2.ParameterType); + Assert.Equal("integer", param2.Schema?.RootElement.GetProperty("type").GetString()); + + Assert.NotNull(result.ReturnParameter); + Assert.Equal("return description", result.ReturnParameter.Description); + Assert.Equal(typeof(string), result.ReturnParameter.ParameterType); + Assert.Equal("string", result.ReturnParameter.Schema?.RootElement.GetProperty("type").GetString()); + } + #pragma warning disable CA1812 // uninstantiated internal class private sealed class ParametersData { diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs index d39480ebfe8d..8c96601371b0 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; using Xunit; @@ -64,13 +63,12 @@ public void KernelFunctionsConfigureOptionsWithNullKernelDoesNotAddTools() { // Arrange var kernelFunctions = new KernelFunctions(autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); // Act - kernelFunctions.ConfigureOptions(null, chatCompletionsOptions); + var config = kernelFunctions.GetConfiguration(new()); // Assert - Assert.Empty(chatCompletionsOptions.Tools); + Assert.Null(config.FunctionsMetadata); } [Fact] @@ -78,15 +76,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 config = kernelFunctions.GetConfiguration(new() { Kernel = kernel }); // Assert - Assert.Null(chatCompletionsOptions.ToolChoice); - Assert.Empty(chatCompletionsOptions.Tools); + Assert.Equal(FunctionChoice.Auto, config.Choice); + Assert.Null(config.FunctionsMetadata); } [Fact] @@ -94,7 +91,6 @@ public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() { // Arrange var kernelFunctions = new KernelFunctions(autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); var plugin = this.GetTestPlugin(); @@ -102,12 +98,12 @@ public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() kernel.Plugins.Add(plugin); // Act - kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + var config = kernelFunctions.GetConfiguration(new() { Kernel = kernel }); // Assert - Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); + Assert.Equal(FunctionChoice.Auto, config.Choice); - this.AssertTools(chatCompletionsOptions); + this.AssertFunctions(config.FunctionsMetadata); } [Fact] @@ -115,14 +111,13 @@ public void EnabledFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() { // Arrange var enabledFunctions = new EnabledFunctions([], autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); // Act - enabledFunctions.ConfigureOptions(null, chatCompletionsOptions); + var config = enabledFunctions.GetConfiguration(new()); // Assert - Assert.Null(chatCompletionsOptions.ToolChoice); - Assert.Empty(chatCompletionsOptions.Tools); + Assert.Equal(FunctionChoice.Auto, config.Choice); + Assert.Null(config.FunctionsMetadata); } [Fact] @@ -131,10 +126,9 @@ public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsExc // Arrange var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); 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.GetConfiguration(new())); Assert.Equal($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided.", exception.Message); } @@ -144,11 +138,10 @@ public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsEx // Arrange var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); 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.GetConfiguration(new() { Kernel = kernel })); Assert.Equal($"The specified {nameof(EnabledFunctions)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); } @@ -161,18 +154,16 @@ public void EnabledFunctionsConfigureOptionsWithKernelAndPluginsAddsTools(bool a var plugin = this.GetTestPlugin(); var functions = plugin.GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); 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 config = enabledFunctions.GetConfiguration(new() { Kernel = kernel }); // Assert - Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); - - this.AssertTools(chatCompletionsOptions); + Assert.Equal(FunctionChoice.Auto, config.Choice); + this.AssertFunctions(config.FunctionsMetadata); } [Fact] @@ -181,10 +172,9 @@ public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsEx // Arrange var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()).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.GetConfiguration(new())); Assert.Equal($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided.", exception.Message); } @@ -194,11 +184,10 @@ public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsE // Arrange var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()).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.GetConfiguration(new() { Kernel = kernel })); Assert.Equal($"The specified {nameof(RequiredFunction)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); } @@ -208,18 +197,17 @@ public void RequiredFunctionConfigureOptionsAddsTools() // Arrange var plugin = this.GetTestPlugin(); var function = plugin.GetFunctionsMetadata()[0].ToOpenAIFunction(); - 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 config = requiredFunction.GetConfiguration(new() { Kernel = kernel }); // Assert - Assert.NotNull(chatCompletionsOptions.ToolChoice); + Assert.NotNull(config.FunctionsMetadata); - this.AssertTools(chatCompletionsOptions); + this.AssertFunctions(config.FunctionsMetadata); } private KernelPlugin GetTestPlugin() @@ -234,16 +222,25 @@ private KernelPlugin GetTestPlugin() return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); } - private void AssertTools(ChatCompletionsOptions chatCompletionsOptions) + private void AssertFunctions(IEnumerable? functions) { - Assert.Single(chatCompletionsOptions.Tools); + Assert.NotNull(functions); + + var function = Assert.Single(functions); + + Assert.NotNull(function); + + Assert.Equal("MyPlugin", function.PluginName); + Assert.Equal("MyFunction", function.Name); + Assert.Equal("Test Function", function.Description); - var tool = chatCompletionsOptions.Tools[0] as ChatCompletionsFunctionToolDefinition; + Assert.NotNull(function.Parameters); + Assert.Equal(2, function.Parameters.Count); - Assert.NotNull(tool); + Assert.Equal("parameter1", function.Parameters[0].Name); + Assert.Equal("parameter2", function.Parameters[1].Name); - 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.NotNull(function.ReturnParameter); + Assert.Equal("Function Result", function.ReturnParameter.Description); } } diff --git a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs index 363b28d2f2f9..e6df9363248b 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs @@ -30,6 +30,9 @@ public void ItShouldInitializeFunctionChoiceBehaviorsFromMarkdown() { // Arrange var kernel = new Kernel(); + kernel.Plugins.AddFromFunctions("p1", [KernelFunctionFactory.CreateFromMethod(() => { }, "f1")]); + kernel.Plugins.AddFromFunctions("p2", [KernelFunctionFactory.CreateFromMethod(() => { }, "f2")]); + kernel.Plugins.AddFromFunctions("p3", [KernelFunctionFactory.CreateFromMethod(() => { }, "f3")]); // Act var function = KernelFunctionMarkdown.CreateFromPromptMarkdown(Markdown, "TellMeAbout"); @@ -42,34 +45,36 @@ public void ItShouldInitializeFunctionChoiceBehaviorsFromMarkdown() // AutoFunctionCallChoice for service1 var service1ExecutionSettings = function.ExecutionSettings["service1"]; - Assert.NotNull(service1ExecutionSettings); + Assert.NotNull(service1ExecutionSettings?.FunctionChoiceBehavior); - var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; - Assert.NotNull(autoFunctionChoiceBehavior); - - Assert.NotNull(autoFunctionChoiceBehavior.Functions); - Assert.Single(autoFunctionChoiceBehavior.Functions); - Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.First()); + var autoConfig = service1ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(autoConfig); + Assert.Equal(FunctionChoice.Auto, autoConfig.Choice); + Assert.NotNull(autoConfig.FunctionsMetadata); + Assert.Equal("p1", autoConfig.FunctionsMetadata.Single().PluginName); + Assert.Equal("f1", autoConfig.FunctionsMetadata.Single().Name); // RequiredFunctionCallChoice for service2 var service2ExecutionSettings = function.ExecutionSettings["service2"]; - Assert.NotNull(service2ExecutionSettings); + Assert.NotNull(service2ExecutionSettings?.FunctionChoiceBehavior); - var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; - Assert.NotNull(requiredFunctionChoiceBehavior); - Assert.NotNull(requiredFunctionChoiceBehavior.Functions); - Assert.Single(requiredFunctionChoiceBehavior.Functions); - Assert.Equal("p1-f1", requiredFunctionChoiceBehavior.Functions.First()); + var requiredConfig = service2ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(requiredConfig); + Assert.Equal(FunctionChoice.Required, requiredConfig.Choice); + Assert.NotNull(requiredConfig.FunctionsMetadata); + Assert.Equal("p2", requiredConfig.FunctionsMetadata.Single().PluginName); + Assert.Equal("f2", requiredConfig.FunctionsMetadata.Single().Name); // NoneFunctionCallChoice for service3 var service3ExecutionSettings = function.ExecutionSettings["service3"]; - Assert.NotNull(service3ExecutionSettings); - - var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; - Assert.NotNull(noneFunctionChoiceBehavior); - Assert.NotNull(noneFunctionChoiceBehavior.Functions); - Assert.Single(noneFunctionChoiceBehavior.Functions); - Assert.Equal("p1-f1", noneFunctionChoiceBehavior.Functions.First()); + Assert.NotNull(service3ExecutionSettings?.FunctionChoiceBehavior); + + var noneConfig = service3ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(noneConfig); + Assert.Equal(FunctionChoice.None, noneConfig.Choice); + Assert.NotNull(noneConfig.FunctionsMetadata); + Assert.Equal("p3", noneConfig.FunctionsMetadata.Single().PluginName); + Assert.Equal("f3", noneConfig.FunctionsMetadata.Single().Name); } [Fact] @@ -112,7 +117,7 @@ These are more AI execution settings "temperature": 0.8, "function_choice_behavior": { "type": "required", - "functions": ["p1.f1"] + "functions": ["p2.f2"] } } } @@ -125,7 +130,7 @@ These are AI execution settings as well "temperature": 0.8, "function_choice_behavior": { "type": "none", - "functions": ["p1.f1"] + "functions": ["p3.f3"] } } } diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs deleted file mode 100644 index d84f56d03afa..000000000000 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Linq; -using Microsoft.SemanticKernel; -using Xunit; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace SemanticKernel.Functions.UnitTests.Yaml; - -/// -/// Tests for . -/// -public sealed class FunctionChoiceBehaviorTypesConverterTests -{ - [Fact] - public void ItShouldDeserializeAutoFunctionChoiceBehavior() - { - // Arrange - var deserializer = new DeserializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) - .Build(); - - var yaml = """ - type: auto - functions: - - p1.f1 - """; - - // Act - var behavior = deserializer.Deserialize(yaml); - - // Assert - Assert.NotNull(behavior.Functions); - Assert.Single(behavior.Functions); - Assert.Equal("p1-f1", behavior.Functions.Single()); - } - - [Fact] - public void ItShouldDeserializeRequiredFunctionChoiceBehavior() - { - // Arrange - var deserializer = new DeserializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) - .Build(); - - var yaml = """ - type: required - functions: - - p2.f2 - """; - - // Act - var behavior = deserializer.Deserialize(yaml); - - // Assert - Assert.NotNull(behavior.Functions); - Assert.Single(behavior.Functions); - Assert.Equal("p2-f2", behavior.Functions.Single()); - } - - [Fact] - public void ItShouldDeserializeNoneFunctionChoiceBehavior() - { - // Arrange - var deserializer = new DeserializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) - .Build(); - - var yaml = """ - type: none, - functions: - - p1.f1 - """; - - // Act - var behavior = deserializer.Deserialize(yaml); - - // Assert - Assert.NotNull(behavior.Functions); - Assert.Single(behavior.Functions); - Assert.Equal("p1-f1", behavior.Functions.Single()); - } -} diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs index 4db0022e2e87..11eac6a0e80c 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs @@ -89,31 +89,47 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() // Act var promptTemplateConfig = KernelFunctionYaml.ToPromptTemplateConfig(this._yaml); + var kernel = new Kernel(); + kernel.Plugins.AddFromFunctions("p1", [KernelFunctionFactory.CreateFromMethod(() => { }, "f1")]); + kernel.Plugins.AddFromFunctions("p2", [KernelFunctionFactory.CreateFromMethod(() => { }, "f2")]); + kernel.Plugins.AddFromFunctions("p3", [KernelFunctionFactory.CreateFromMethod(() => { }, "f3")]); + // Assert Assert.NotNull(promptTemplateConfig?.ExecutionSettings); Assert.Equal(3, promptTemplateConfig.ExecutionSettings.Count); // Service with auto function choice behavior var service1ExecutionSettings = promptTemplateConfig.ExecutionSettings["service1"]; + Assert.NotNull(service1ExecutionSettings?.FunctionChoiceBehavior); - var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; - Assert.NotNull(autoFunctionChoiceBehavior?.Functions); - Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.Single()); + var autoConfig = service1ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(autoConfig); + Assert.Equal(FunctionChoice.Auto, autoConfig.Choice); + Assert.NotNull(autoConfig.FunctionsMetadata); + Assert.Equal("p1", autoConfig.FunctionsMetadata.Single().PluginName); + Assert.Equal("f1", autoConfig.FunctionsMetadata.Single().Name); // Service with required function choice behavior var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; + Assert.NotNull(service2ExecutionSettings?.FunctionChoiceBehavior); - var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; - Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); - Assert.Equal("p2-f2", requiredFunctionChoiceBehavior.Functions.Single()); + var requiredConfig = service2ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(requiredConfig); + Assert.Equal(FunctionChoice.Required, requiredConfig.Choice); + Assert.NotNull(requiredConfig.FunctionsMetadata); + Assert.Equal("p2", requiredConfig.FunctionsMetadata.Single().PluginName); + Assert.Equal("f2", requiredConfig.FunctionsMetadata.Single().Name); // Service with none function choice behavior var service3ExecutionSettings = promptTemplateConfig.ExecutionSettings["service3"]; + Assert.NotNull(service3ExecutionSettings?.FunctionChoiceBehavior); - var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; - Assert.NotNull(noneFunctionChoiceBehavior); - Assert.NotNull(noneFunctionChoiceBehavior?.Functions); - Assert.Equal("p3-f3", noneFunctionChoiceBehavior.Functions.Single()); + var noneConfig = service3ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(noneConfig); + Assert.Equal(FunctionChoice.None, noneConfig.Choice); + Assert.NotNull(noneConfig.FunctionsMetadata); + Assert.Equal("p3", noneConfig.FunctionsMetadata.Single().PluginName); + Assert.Equal("f3", noneConfig.FunctionsMetadata.Single().Name); } [Fact] diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs index fe5717f3cd68..91a142d8c0dd 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs @@ -40,6 +40,12 @@ public void ItShouldCreatePromptFunctionFromYamlWithCustomModelSettings() [Fact] public void ItShouldDeserializeFunctionChoiceBehaviors() { + // Arrange + var kernel = new Kernel(); + kernel.Plugins.AddFromFunctions("p1", [KernelFunctionFactory.CreateFromMethod(() => { }, "f1")]); + kernel.Plugins.AddFromFunctions("p2", [KernelFunctionFactory.CreateFromMethod(() => { }, "f2")]); + kernel.Plugins.AddFromFunctions("p3", [KernelFunctionFactory.CreateFromMethod(() => { }, "f3")]); + // Act var promptTemplateConfig = KernelFunctionYaml.ToPromptTemplateConfig(this._yaml); @@ -49,25 +55,36 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() // Service with auto function choice behavior var service1ExecutionSettings = promptTemplateConfig.ExecutionSettings["service1"]; + Assert.NotNull(service1ExecutionSettings?.FunctionChoiceBehavior); - var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; - Assert.NotNull(autoFunctionChoiceBehavior?.Functions); - Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.Single()); + var autoConfig = service1ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(autoConfig); + Assert.Equal(FunctionChoice.Auto, autoConfig.Choice); + Assert.NotNull(autoConfig.FunctionsMetadata); + Assert.Equal("p1", autoConfig.FunctionsMetadata.Single().PluginName); + Assert.Equal("f1", autoConfig.FunctionsMetadata.Single().Name); // Service with required function choice behavior var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; + Assert.NotNull(service2ExecutionSettings?.FunctionChoiceBehavior); - var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; - Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); - Assert.Equal("p2-f2", requiredFunctionChoiceBehavior.Functions.Single()); + var requiredConfig = service2ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(requiredConfig); + Assert.Equal(FunctionChoice.Required, requiredConfig.Choice); + Assert.NotNull(requiredConfig.FunctionsMetadata); + Assert.Equal("p2", requiredConfig.FunctionsMetadata.Single().PluginName); + Assert.Equal("f2", requiredConfig.FunctionsMetadata.Single().Name); // Service with none function choice behavior var service3ExecutionSettings = promptTemplateConfig.ExecutionSettings["service3"]; + Assert.NotNull(service3ExecutionSettings?.FunctionChoiceBehavior); - var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; - Assert.NotNull(noneFunctionChoiceBehavior); - Assert.NotNull(noneFunctionChoiceBehavior?.Functions); - Assert.Equal("p3-f3", noneFunctionChoiceBehavior.Functions.Single()); + var noneConfig = service3ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(noneConfig); + Assert.Equal(FunctionChoice.None, noneConfig.Choice); + Assert.NotNull(noneConfig.FunctionsMetadata); + Assert.Equal("p3", noneConfig.FunctionsMetadata.Single().PluginName); + Assert.Equal("f3", noneConfig.FunctionsMetadata.Single().Name); } private readonly string _yaml = """ diff --git a/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs b/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs deleted file mode 100644 index 8f5b0f42be6d..000000000000 --- a/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using YamlDotNet.Core; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace Microsoft.SemanticKernel; - -/// -/// Allows custom deserialization for derivatives of . -/// -internal sealed class FunctionChoiceBehaviorTypesConverter : IYamlTypeConverter -{ - private const char PromptFunctionNameSeparator = '.'; - - private const char FunctionNameSeparator = '-'; - - private static IDeserializer? s_deserializer; - - /// - public bool Accepts(Type type) - { -#pragma warning disable SKEXP0001 - return - type == typeof(AutoFunctionChoiceBehavior) || - type == typeof(RequiredFunctionChoiceBehavior) || - type == typeof(NoneFunctionChoiceBehavior); -#pragma warning restore SKEXP0001 - } - - public object? ReadYaml(IParser parser, Type type) - { - s_deserializer ??= new DeserializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination. Otherwise, the "Property 'type' not found on type '{type.FullName}'" exception is thrown. - .Build(); - -#pragma warning disable SKEXP0001 - if (type == typeof(AutoFunctionChoiceBehavior)) - { - var behavior = s_deserializer.Deserialize(parser); - behavior.Functions = ConvertFunctionNames(behavior.Functions); - return behavior; - } - else if (type == typeof(RequiredFunctionChoiceBehavior)) - { - var behavior = s_deserializer.Deserialize(parser); - behavior.Functions = ConvertFunctionNames(behavior.Functions); - return behavior; - } - else if (type == typeof(NoneFunctionChoiceBehavior)) - { - var behavior = s_deserializer.Deserialize(parser); - behavior.Functions = ConvertFunctionNames(behavior.Functions); - return behavior; - } - - throw new YamlException($"Unexpected type '{type.FullName}' for function choice behavior."); -#pragma warning restore SKEXP0001 - } - - /// - public void WriteYaml(IEmitter emitter, object? value, Type type) - { - throw new NotImplementedException(); - } - - private static IList? ConvertFunctionNames(IList? functions) - { - if (functions is null) - { - return functions; - } - - return functions.Select(fqn => - { - var functionName = fqn ?? throw new YamlException("Expected a non-null YAML string."); - return functionName.Replace(PromptFunctionNameSeparator, FunctionNameSeparator); - }).ToList(); - } -} diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs index b4c5d8569f17..8edf6bf31e10 100644 --- a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs @@ -2,9 +2,12 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Linq; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; +using YamlDotNet.Serialization.BufferedDeserialization; using YamlDotNet.Serialization.NamingConventions; namespace Microsoft.SemanticKernel; @@ -27,18 +30,8 @@ public bool Accepts(Type type) { s_deserializer ??= new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) - .WithTypeDiscriminatingNodeDeserializer((options) => - { -#pragma warning disable SKEXP0001 - options.AddKeyValueTypeDiscriminator("type", new Dictionary - { - { AutoFunctionChoiceBehavior.TypeDiscriminator, typeof(AutoFunctionChoiceBehavior) }, - { RequiredFunctionChoiceBehavior.TypeDiscriminator, typeof(RequiredFunctionChoiceBehavior) }, - { NoneFunctionChoiceBehavior.TypeDiscriminator, typeof(NoneFunctionChoiceBehavior) } - }); -#pragma warning restore SKEXP0010 - }) + .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination. Otherwise, the "Property 'type' not found on type '{type.FullName}'" exception is thrown. + .WithTypeDiscriminatingNodeDeserializer(ConfigureTypeDiscriminatingNodeDeserializer) .Build(); parser.MoveNext(); // Move to the first property @@ -53,7 +46,9 @@ public bool Accepts(Type type) executionSettings.ModelId = s_deserializer.Deserialize(parser); break; case "function_choice_behavior": +#pragma warning disable SKEXP0001 executionSettings.FunctionChoiceBehavior = s_deserializer.Deserialize(parser); +#pragma warning restore SKEXP0010 break; default: (executionSettings.ExtensionData ??= new Dictionary()).Add(propertyName, s_deserializer.Deserialize(parser)); @@ -69,4 +64,35 @@ public void WriteYaml(IEmitter emitter, object? value, Type type) { throw new NotImplementedException(); } + + private static void ConfigureTypeDiscriminatingNodeDeserializer(ITypeDiscriminatingNodeDeserializerOptions options) + { +#pragma warning disable SKEXP0001 + var attributes = typeof(FunctionChoiceBehavior).GetCustomAttributes(false); + + // Getting the type discriminator property name - "type" from the JsonPolymorphicAttribute. + var discriminatorKey = attributes.OfType().Single().TypeDiscriminatorPropertyName; + if (string.IsNullOrEmpty(discriminatorKey)) + { + throw new InvalidOperationException("Type discriminator property name is not specified."); + } + + var discriminatorTypeMapping = new Dictionary(); + + // Getting FunctionChoiceBehavior subtypes and their type discriminators registered for polymorphic deserialization. + var derivedTypeAttributes = attributes.OfType(); + foreach (var derivedTypeAttribute in derivedTypeAttributes) + { + var discriminator = derivedTypeAttribute.TypeDiscriminator?.ToString(); + if (string.IsNullOrEmpty(discriminator)) + { + throw new InvalidOperationException($"Type discriminator is not specified for the {derivedTypeAttribute.DerivedType} type."); + } + + discriminatorTypeMapping.Add(discriminator!, derivedTypeAttribute.DerivedType); + } + + options.AddKeyValueTypeDiscriminator(discriminatorKey!, discriminatorTypeMapping); +#pragma warning restore SKEXP0010 + } } diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index 1512530fe681..eeeb7baf20db 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -357,7 +357,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu } // Adding a simulated function call to the connector response message - var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); + var simulatedFunctionCall = new FunctionCallContent("weather_alert", id: "call_123"); messageContent.Items.Add(simulatedFunctionCall); // Adding a simulated function result to chat history diff --git a/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs b/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs index 76f54de92a56..02d7e67f56d9 100644 --- a/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs +++ b/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs @@ -28,7 +28,8 @@ internal sealed class FunctionName /// The plugin name. public FunctionName(string name, string? pluginName = null) { - Verify.NotNull(name); + Verify.ValidFunctionName(name); + if (pluginName is not null) { Verify.ValidPluginName(pluginName); } this.Name = name; this.PluginName = pluginName; @@ -43,6 +44,9 @@ public FunctionName(string name, string? pluginName = null) /// Fully-qualified name of the function. public static string ToFullyQualifiedName(string functionName, string? pluginName = null, string functionNameSeparator = "-") { + Verify.ValidFunctionName(functionName); + if (pluginName is not null) { Verify.ValidPluginName(pluginName); } + return string.IsNullOrEmpty(pluginName) ? functionName : $"{pluginName}{functionNameSeparator}{functionName}"; } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index e9fe65991461..9bb0e9d2d54a 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -11,8 +10,7 @@ namespace Microsoft.SemanticKernel; /// Represents that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior allows the model to decide whether to call the functions and, if so, which ones to call. /// -[Experimental("SKEXP0001")] -public sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior +internal sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior { /// /// List of the functions that the model can choose from. @@ -24,11 +22,6 @@ public sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior /// private readonly bool _autoInvoke = true; - /// - /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. - /// - public const string TypeDiscriminator = "auto"; - /// /// Initializes a new instance of the class. /// @@ -47,7 +40,7 @@ public AutoFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); + this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); } /// @@ -55,7 +48,6 @@ public AutoFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable's plugins' functions are provided to the model. /// [JsonPropertyName("functions")] - [JsonConverter(typeof(FunctionNameFormatJsonConverter))] public IList? Functions { get; set; } /// @@ -71,7 +63,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho throw new KernelException("Auto-invocation for Auto choice behavior is not supported when no kernel is provided."); } - List? availableFunctions = null; + List? availableFunctions = null; bool allowAnyRequestedKernelFunction = false; // Handle functions provided via the 'Functions' property as function fully qualified names. @@ -81,12 +73,12 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var functionFQN in functionFQNs) { - var nameParts = FunctionName.Parse(functionFQN); + var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); // Check if the function is available in the kernel. If it is, then connectors can find it for auto-invocation later. if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) { - availableFunctions.Add(function); + availableFunctions.Add(function.Metadata); continue; } @@ -100,7 +92,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); if (function is not null) { - availableFunctions.Add(function); + availableFunctions.Add(function.Metadata); continue; } @@ -115,14 +107,14 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var plugin in context.Kernel.Plugins) { availableFunctions ??= []; - availableFunctions.AddRange(plugin); + availableFunctions.AddRange(plugin.Select(p => p.Metadata)); } } return new FunctionChoiceBehaviorConfiguration() { Choice = FunctionChoice.Auto, - Functions = availableFunctions, + FunctionsMetadata = availableFunctions, AutoInvoke = this._autoInvoke, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index 19e789beb61d..bf4c13aeef92 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -9,19 +9,15 @@ namespace Microsoft.SemanticKernel; /// /// Represents the base class for different function choice behaviors. /// -[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] -[JsonDerivedType(typeof(AutoFunctionChoiceBehavior), typeDiscriminator: AutoFunctionChoiceBehavior.TypeDiscriminator)] -[JsonDerivedType(typeof(RequiredFunctionChoiceBehavior), typeDiscriminator: RequiredFunctionChoiceBehavior.TypeDiscriminator)] -[JsonDerivedType(typeof(NoneFunctionChoiceBehavior), typeDiscriminator: NoneFunctionChoiceBehavior.TypeDiscriminator)] +[JsonPolymorphic(TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)] +[JsonDerivedType(typeof(AutoFunctionChoiceBehavior), typeDiscriminator: "auto")] +[JsonDerivedType(typeof(RequiredFunctionChoiceBehavior), typeDiscriminator: "required")] +[JsonDerivedType(typeof(NoneFunctionChoiceBehavior), typeDiscriminator: "none")] [Experimental("SKEXP0001")] public abstract class FunctionChoiceBehavior { - /// - /// Creates a new instance of the class. - /// - internal FunctionChoiceBehavior() - { - } + /// The separator used to separate plugin name and function name. + protected const string FunctionNameSeparator = "."; /// /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index 228281a3945a..ad5e1f3b9417 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -11,30 +11,23 @@ namespace Microsoft.SemanticKernel; [Experimental("SKEXP0001")] public sealed class FunctionChoiceBehaviorConfiguration { - /// - /// Creates a new instance of the class. - /// - internal FunctionChoiceBehaviorConfiguration() - { - } - /// /// Represents an AI model's decision-making strategy for calling functions. /// - public FunctionChoice Choice { get; internal set; } + public FunctionChoice Choice { get; init; } /// /// The functions available for AI model. /// - public IEnumerable? Functions { get; internal set; } + public IEnumerable? FunctionsMetadata { get; init; } /// /// Indicates whether the functions should be automatically invoked by the AI service/connector. /// - public bool AutoInvoke { get; internal set; } = true; + public bool AutoInvoke { get; init; } = true; /// /// Specifies whether validation against a specified list of functions is required before allowing the model to request a function from the kernel. /// - public bool? AllowAnyRequestedKernelFunction { get; internal set; } + public bool? AllowAnyRequestedKernelFunction { get; init; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs deleted file mode 100644 index b828fa2f2c9a..000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel; - -/// -/// A custom JSON converter for converting function names in a JSON array. -/// This converter replaces dots used as a function name separator in prompts with hyphens when reading and back when writing. -/// -[Experimental("SKEXP0001")] -public sealed class FunctionNameFormatJsonConverter : JsonConverter> -{ - private const char PromptFunctionNameSeparator = '.'; - - private const char FunctionNameSeparator = '-'; - - /// - public override IList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartArray) - { - throw new JsonException("Expected a JSON array."); - } - - var functionNames = new List(); - - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndArray) - { - break; - } - - if (reader.TokenType != JsonTokenType.String) - { - throw new JsonException("Expected a JSON string."); - } - - var functionName = reader.GetString() ?? throw new JsonException("Expected a non-null JSON string."); - - functionNames.Add(functionName.Replace(PromptFunctionNameSeparator, FunctionNameSeparator)); - } - - return functionNames; - } - - /// - public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) - { - writer.WriteStartArray(); - - foreach (string functionName in value) - { - writer.WriteStringValue(functionName.Replace(FunctionNameSeparator, PromptFunctionNameSeparator)); - } - - writer.WriteEndArray(); - } -} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs index 88e850aa99c9..d0b99acab812 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -15,19 +14,13 @@ namespace Microsoft.SemanticKernel; /// Although this behavior prevents the model from calling any functions, the model can use the provided function information /// to describe how it would complete the prompt if it had the ability to call the functions. /// -[Experimental("SKEXP0001")] -public sealed class NoneFunctionChoiceBehavior : FunctionChoiceBehavior +internal sealed class NoneFunctionChoiceBehavior : FunctionChoiceBehavior { /// /// List of the functions that the model can choose from. /// private readonly IEnumerable? _functions; - /// - /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. - /// - public const string TypeDiscriminator = "none"; - /// /// Initializes a new instance of the class. /// @@ -44,7 +37,7 @@ public NoneFunctionChoiceBehavior() public NoneFunctionChoiceBehavior(IEnumerable functions) { this._functions = functions; - this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); + this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); } /// @@ -52,13 +45,12 @@ public NoneFunctionChoiceBehavior(IEnumerable functions) /// If null or empty, all 's plugins' functions are provided to the model. /// [JsonPropertyName("functions")] - [JsonConverter(typeof(FunctionNameFormatJsonConverter))] public IList? Functions { get; set; } /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { - List? availableFunctions = null; + List? availableFunctions = null; // Handle functions provided via the 'Functions' property as function fully qualified names. if (this.Functions is { } functionFQNs && functionFQNs.Any()) @@ -67,12 +59,12 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var functionFQN in functionFQNs) { - var nameParts = FunctionName.Parse(functionFQN); + var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); // Check if the function is available in the kernel. if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) { - availableFunctions.Add(function); + availableFunctions.Add(function.Metadata); continue; } @@ -80,7 +72,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); if (function is not null) { - availableFunctions.Add(function); + availableFunctions.Add(function.Metadata); continue; } @@ -92,15 +84,14 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho { foreach (var plugin in context.Kernel.Plugins) { - availableFunctions ??= []; - availableFunctions.AddRange(plugin); + (availableFunctions ??= []).AddRange(plugin.Select(p => p.Metadata)); } } return new FunctionChoiceBehaviorConfiguration() { Choice = FunctionChoice.None, - Functions = availableFunctions, + FunctionsMetadata = availableFunctions, }; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index c417a14393d5..55101fb838f2 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -11,8 +10,7 @@ namespace Microsoft.SemanticKernel; /// Represents that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. /// -[Experimental("SKEXP0001")] -public sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior +internal sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior { /// /// List of the functions that the model can choose from. @@ -24,11 +22,6 @@ public sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior /// private readonly bool _autoInvoke = true; - /// - /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. - /// - public const string TypeDiscriminator = "required"; - /// /// Initializes a new instance of the class. /// @@ -47,7 +40,7 @@ public RequiredFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); + this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); } /// @@ -55,7 +48,6 @@ public RequiredFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable's plugins' functions are provided to the model. /// [JsonPropertyName("functions")] - [JsonConverter(typeof(FunctionNameFormatJsonConverter))] public IList? Functions { get; set; } /// @@ -71,7 +63,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho throw new KernelException("Auto-invocation for Required choice behavior is not supported when no kernel is provided."); } - List? availableFunctions = null; + List? availableFunctions = null; bool allowAnyRequestedKernelFunction = false; // Handle functions provided via the 'Functions' property as function fully qualified names. @@ -81,12 +73,12 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var functionFQN in functionFQNs) { - var nameParts = FunctionName.Parse(functionFQN); + var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); // Check if the function is available in the kernel. If it is, then connectors can find it for auto-invocation later. if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) { - availableFunctions.Add(function); + availableFunctions.Add(function.Metadata); continue; } @@ -100,7 +92,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); if (function is not null) { - availableFunctions.Add(function); + availableFunctions.Add(function.Metadata); continue; } @@ -114,17 +106,16 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var plugin in context.Kernel.Plugins) { - availableFunctions ??= []; - availableFunctions.AddRange(plugin); + (availableFunctions ??= []).AddRange(plugin.Select(p => p.Metadata)); } } return new FunctionChoiceBehaviorConfiguration() { Choice = FunctionChoice.Required, - Functions = availableFunctions, + FunctionsMetadata = availableFunctions, AutoInvoke = this._autoInvoke, - AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction + AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction, }; } } diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs index bbcb3fb808f5..6d3c2935ba0e 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs @@ -34,11 +34,11 @@ public void ItShouldAdvertiseAllKernelFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); - Assert.Contains(config.Functions, f => f.Name == "Function3"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(3, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); } [Fact] @@ -56,10 +56,10 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructor() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(2, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); } [Fact] @@ -72,7 +72,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() // Act var choiceBehavior = new AutoFunctionChoiceBehavior() { - Functions = ["MyPlugin-Function1", "MyPlugin-Function2"] + Functions = ["MyPlugin.Function1", "MyPlugin.Function2"] }; var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -80,10 +80,10 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(2, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); } [Fact] @@ -100,10 +100,10 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorForManualInvocat // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(2, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); } [Fact] @@ -121,11 +121,11 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); - Assert.Contains(config.Functions, f => f.Name == "Function3"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(3, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); } [Fact] @@ -176,8 +176,8 @@ public void ItShouldInitializeFunctionPropertyByFunctionsPassedViaConstructor() Assert.NotNull(choiceBehavior.Functions); Assert.Equal(2, choiceBehavior.Functions.Count); - Assert.Equal("MyPlugin-Function1", choiceBehavior.Functions.ElementAt(0)); - Assert.Equal("MyPlugin-Function2", choiceBehavior.Functions.ElementAt(1)); + Assert.Equal("MyPlugin.Function1", choiceBehavior.Functions.ElementAt(0)); + Assert.Equal("MyPlugin.Function2", choiceBehavior.Functions.ElementAt(1)); } [Fact] @@ -212,7 +212,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegis choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("The specified function MyPlugin-Function1 is not available in the kernel.", exception.Message); + Assert.Equal("The specified function MyPlugin.Function1 is not available in the kernel.", exception.Message); } [Fact] @@ -224,7 +224,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste var choiceBehavior = new AutoFunctionChoiceBehavior(autoInvoke: false) { - Functions = ["MyPlugin-NonKernelFunction"] + Functions = ["MyPlugin.NonKernelFunction"] }; // Act @@ -233,7 +233,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("No instance of the specified function MyPlugin-NonKernelFunction is found.", exception.Message); + Assert.Equal("No instance of the specified function MyPlugin.NonKernelFunction is found.", exception.Message); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs index 959a76bbf328..6d7f0e1ec13c 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs @@ -25,7 +25,7 @@ public void ItShouldDeserializeAutoFunctionChoiceBehavior() // Assert Assert.NotNull(behavior?.Functions); Assert.Single(behavior.Functions); - Assert.Equal("p1-f1", behavior.Functions.Single()); + Assert.Equal("p1.f1", behavior.Functions.Single()); } [Fact] @@ -45,6 +45,26 @@ public void ItShouldDeserializeRequiredFunctionChoiceBehavior() // Assert Assert.NotNull(behavior?.Functions); Assert.Single(behavior.Functions); - Assert.Equal("p1-f1", behavior.Functions.Single()); + Assert.Equal("p1.f1", behavior.Functions.Single()); + } + + [Fact] + public void ItShouldDeserializeNoneFunctionChoiceBehavior() + { + // Arrange + var json = """ + { + "type": "none", + "functions": ["p1.f1"] + } + """; + + // Act + var behavior = JsonSerializer.Deserialize(json); + + // Assert + Assert.NotNull(behavior?.Functions); + Assert.Single(behavior.Functions); + Assert.Equal("p1.f1", behavior.Functions.Single()); } } diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs index ef00c269e8d6..84f3ff5052ef 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs @@ -33,11 +33,11 @@ public void ItShouldAdvertiseKernelFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); - Assert.Contains(config.Functions, f => f.Name == "Function3"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(3, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); } [Fact] @@ -54,10 +54,10 @@ public void ItShouldAdvertiseFunctionsIfSpecified() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function3"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(2, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); } private static KernelPlugin GetTestPlugin() diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs index ece75a9e8e10..512379090bee 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs @@ -34,11 +34,11 @@ public void ItShouldAdvertiseAllKernelFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); - Assert.Contains(config.Functions, f => f.Name == "Function3"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(3, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); } [Fact] @@ -56,10 +56,10 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructor() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(2, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); } [Fact] @@ -72,7 +72,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() // Act var choiceBehavior = new RequiredFunctionChoiceBehavior() { - Functions = ["MyPlugin-Function1", "MyPlugin-Function2"] + Functions = ["MyPlugin.Function1", "MyPlugin.Function2"] }; var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -80,10 +80,10 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(2, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); } [Fact] @@ -100,10 +100,10 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorForManualInvocat // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(2, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); } [Fact] @@ -121,11 +121,11 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); - Assert.Contains(config.Functions, f => f.Name == "Function3"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(3, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); } [Fact] @@ -176,8 +176,8 @@ public void ItShouldInitializeFunctionPropertyByFunctionsPassedViaConstructor() Assert.NotNull(choiceBehavior.Functions); Assert.Equal(2, choiceBehavior.Functions.Count); - Assert.Equal("MyPlugin-Function1", choiceBehavior.Functions.ElementAt(0)); - Assert.Equal("MyPlugin-Function2", choiceBehavior.Functions.ElementAt(1)); + Assert.Equal("MyPlugin.Function1", choiceBehavior.Functions.ElementAt(0)); + Assert.Equal("MyPlugin.Function2", choiceBehavior.Functions.ElementAt(1)); } [Fact] @@ -212,7 +212,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegis choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("The specified function MyPlugin-Function1 is not available in the kernel.", exception.Message); + Assert.Equal("The specified function MyPlugin.Function1 is not available in the kernel.", exception.Message); } [Fact] @@ -224,7 +224,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste var choiceBehavior = new RequiredFunctionChoiceBehavior(false) { - Functions = ["MyPlugin-NonKernelFunction"] + Functions = ["MyPlugin.NonKernelFunction"] }; // Act @@ -233,7 +233,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("No instance of the specified function MyPlugin-NonKernelFunction is found.", exception.Message); + Assert.Equal("No instance of the specified function MyPlugin.NonKernelFunction is found.", exception.Message); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs index 196ca39d6495..69382383843d 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs @@ -63,11 +63,11 @@ public void AutoFunctionChoiceShouldAdvertiseKernelFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); - Assert.Contains(config.Functions, f => f.Name == "Function3"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(3, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); } [Fact] @@ -85,10 +85,10 @@ public void AutoFunctionChoiceShouldAdvertiseProvidedFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(2, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); } [Fact] @@ -140,11 +140,11 @@ public void RequiredFunctionChoiceShouldAdvertiseKernelFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); - Assert.Contains(config.Functions, f => f.Name == "Function3"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(3, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); } [Fact] @@ -162,10 +162,10 @@ public void RequiredFunctionChoiceShouldAdvertiseProvidedFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(2, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); } [Fact] @@ -216,10 +216,10 @@ public void NoneFunctionChoiceShouldAdvertiseProvidedFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function3"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(2, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); } [Fact] @@ -237,11 +237,11 @@ public void NoneFunctionChoiceShouldAdvertiseAllKernelFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); - Assert.Contains(config.Functions, f => f.Name == "Function1"); - Assert.Contains(config.Functions, f => f.Name == "Function2"); - Assert.Contains(config.Functions, f => f.Name == "Function3"); + Assert.NotNull(config.FunctionsMetadata); + Assert.Equal(3, config.FunctionsMetadata.Count()); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); } private static KernelPlugin GetTestPlugin() diff --git a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs index ef230e17a9e8..0133b515d8cc 100644 --- a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs @@ -174,7 +174,7 @@ public void DeserializingAutoFunctionCallingChoice() Assert.NotNull(autoFunctionCallChoice); Assert.NotNull(autoFunctionCallChoice.Functions); - Assert.Equal("p1-f1", autoFunctionCallChoice.Functions.Single()); + Assert.Equal("p1.f1", autoFunctionCallChoice.Functions.Single()); } [Fact] @@ -210,7 +210,7 @@ public void DeserializingRequiredFunctionCallingChoice() Assert.NotNull(requiredFunctionCallChoice); Assert.NotNull(requiredFunctionCallChoice.Functions); - Assert.Equal("p1-f1", requiredFunctionCallChoice.Functions.Single()); + Assert.Equal("p1.f1", requiredFunctionCallChoice.Functions.Single()); } [Fact] From b4fc6ed8627758517d02b307b7f5c5f9966cfa94 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 11 Jun 2024 10:19:00 +0100 Subject: [PATCH 79/90] Revert "fix: address PR comments" This reverts commit 1f13f3515ae233bcf2b4a43b87e09ca302be4a10. --- .../OpenAI_FunctionCalling.cs | 2 +- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 62 +++++++++--- .../AzureSdk/OpenAIFunction.cs | 18 +--- .../OpenAIKernelFunctionMetadataExtensions.cs | 38 ++------ .../OpenAIPromptExecutionSettings.cs | 6 +- .../Connectors.OpenAI/ToolCallBehavior.cs | 95 +++++++++---------- .../KernelFunctionMetadataExtensionsTests.cs | 8 +- .../FunctionCalling/OpenAIFunctionTests.cs | 49 +--------- .../OpenAI/ToolCallBehaviorTests.cs | 77 +++++++-------- .../Functions/KernelFunctionMarkdownTests.cs | 49 +++++----- ...nctionChoiceBehaviorTypesConverterTests.cs | 87 +++++++++++++++++ .../Yaml/Functions/KernelFunctionYamlTests.cs | 36 ++----- ...omptExecutionSettingsTypeConverterTests.cs | 37 ++------ .../FunctionChoiceBehaviorTypesConverter.cs | 84 ++++++++++++++++ .../PromptExecutionSettingsTypeConverter.cs | 50 +++------- .../Connectors/OpenAI/OpenAIToolsTests.cs | 2 +- .../src/Functions/FunctionName.cs | 6 +- .../AutoFunctionChoiceBehavior.cs | 24 +++-- .../FunctionChoiceBehavior.cs | 16 ++-- .../FunctionChoiceBehaviorConfiguration.cs | 15 ++- .../FunctionNameFormatJsonConverter.cs | 64 +++++++++++++ .../NoneFunctionChoiceBehavior.cs | 25 +++-- .../RequiredFunctionChoiceBehavior.cs | 27 ++++-- .../AutoFunctionChoiceBehaviorTests.cs | 56 +++++------ .../FunctionNameFormatJsonConverterTests.cs | 24 +---- .../NoneFunctionChoiceBehaviorTests.cs | 18 ++-- .../RequiredFunctionChoiceBehaviorTests.cs | 56 +++++------ .../Functions/FunctionChoiceBehaviorTests.cs | 54 +++++------ .../PromptTemplateConfigTests.cs | 4 +- 29 files changed, 617 insertions(+), 472 deletions(-) create mode 100644 dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs create mode 100644 dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs diff --git a/dotnet/samples/Concepts/AutoFunctionCalling/OpenAI_FunctionCalling.cs b/dotnet/samples/Concepts/AutoFunctionCalling/OpenAI_FunctionCalling.cs index d2d7f6d84a50..bc985e885916 100644 --- a/dotnet/samples/Concepts/AutoFunctionCalling/OpenAI_FunctionCalling.cs +++ b/dotnet/samples/Concepts/AutoFunctionCalling/OpenAI_FunctionCalling.cs @@ -139,7 +139,7 @@ public async Task RunAsync() } // Adding a simulated function call to the connector response message - var simulatedFunctionCall = new FunctionCallContent("weather_alert", id: "call_123"); + var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); result.Items.Add(simulatedFunctionCall); // Adding a simulated function result to chat history diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index c630e91fe541..e905e25f2b63 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1547,14 +1547,26 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context return result; } + // If both behaviors are specified, we can't handle that. + if (executionSettings.FunctionChoiceBehavior is not null && executionSettings.ToolCallBehavior is not null) + { + throw new ArgumentException($"{nameof(executionSettings.ToolCallBehavior)} and {nameof(executionSettings.FunctionChoiceBehavior)} cannot be used together."); + } + // Set the tool choice to none. If we end up wanting to use tools, we'll set it to the desired value. chatOptions.ToolChoice = ChatCompletionsToolChoice.None; chatOptions.Tools.Clear(); + // Handling new tool behavior represented by `PromptExecutionSettings.FunctionChoiceBehavior` property. if (executionSettings.FunctionChoiceBehavior is { } functionChoiceBehavior) { result = this.ConfigureFunctionCalling(kernel, chatOptions, requestIndex, functionChoiceBehavior); } + // Handling old-style tool call behavior represented by `OpenAIPromptExecutionSettings.ToolCallBehavior` property. + else if (executionSettings.ToolCallBehavior is { } toolCallBehavior) + { + result = this.ConfigureFunctionCalling(kernel, chatOptions, requestIndex, toolCallBehavior); + } // Having already sent tools and with tool call information in history, the service can become unhappy "Invalid 'tools': empty array. Expected an array with minimum length 1, but got an empty array instead." // if we don't send any tools in subsequent requests, even if we say not to use any. @@ -1593,13 +1605,13 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context if (config.Choice == FunctionChoice.Auto) { - if (config.FunctionsMetadata is { } functionsMetadata && functionsMetadata.Any()) - { - chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; + chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; - foreach (var functionMetadata in functionsMetadata) + if (config.Functions is { } functions) + { + foreach (var function in functions) { - var functionDefinition = functionMetadata.ToOpenAIFunction().ToFunctionDefinition(); + var functionDefinition = function.Metadata.ToOpenAIFunction().ToFunctionDefinition(); chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); } } @@ -1609,14 +1621,14 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context if (config.Choice == FunctionChoice.Required) { - if (config.FunctionsMetadata is { } functionsMetadata && functionsMetadata.Any()) + if (config.Functions is { } functions && functions.Any()) { - if (functionsMetadata.Count() > 1) + if (functions.Count() > 1) { throw new KernelException("Only one required function is allowed."); } - var functionDefinition = functionsMetadata.First().ToOpenAIFunction().ToFunctionDefinition(); + var functionDefinition = functions.First().Metadata.ToOpenAIFunction().ToFunctionDefinition(); chatOptions.ToolChoice = new ChatCompletionsToolChoice(functionDefinition); chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); @@ -1627,13 +1639,13 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context if (config.Choice == FunctionChoice.None) { - if (config.FunctionsMetadata is { } functionsMetadata && functionsMetadata.Any()) - { - chatOptions.ToolChoice = ChatCompletionsToolChoice.None; + chatOptions.ToolChoice = ChatCompletionsToolChoice.None; - foreach (var functionMetadata in functionsMetadata) + if (config.Functions is { } functions) + { + foreach (var function in functions) { - var functionDefinition = functionMetadata.ToOpenAIFunction().ToFunctionDefinition(); + var functionDefinition = function.Metadata.ToOpenAIFunction().ToFunctionDefinition(); chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); } } @@ -1643,4 +1655,28 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context throw new NotSupportedException($"Unsupported function choice '{config.Choice}'."); } + + private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(Kernel? kernel, ChatCompletionsOptions chatOptions, int requestIndex, ToolCallBehavior toolCallBehavior) + { + if (requestIndex >= 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 tools.", 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. + toolCallBehavior.ConfigureOptions(kernel, chatOptions); + } + + return new() + { + AllowAnyRequestedKernelFunction = toolCallBehavior.AllowAnyRequestedKernelFunction, + MaximumAutoInvokeAttempts = toolCallBehavior.MaximumAutoInvokeAttempts, + }; + } } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunction.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunction.cs index 68abbd62588f..b51faa59c359 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunction.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunction.cs @@ -15,14 +15,13 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// public sealed class OpenAIFunctionParameter { - internal OpenAIFunctionParameter(string? name, string? description, bool isRequired, Type? parameterType, KernelJsonSchema? schema, object? defaultValue = null) + internal OpenAIFunctionParameter(string? name, string? description, bool isRequired, Type? parameterType, KernelJsonSchema? schema) { this.Name = name ?? string.Empty; this.Description = description ?? string.Empty; this.IsRequired = isRequired; this.ParameterType = parameterType; this.Schema = schema; - this.DefaultValue = defaultValue; } /// Gets the name of the parameter. @@ -39,9 +38,6 @@ internal OpenAIFunctionParameter(string? name, string? description, bool isRequi /// Gets a JSON schema for the parameter, if known. public KernelJsonSchema? Schema { get; } - - /// Gets the default value of the parameter. - public object? DefaultValue { get; } } /// @@ -148,7 +144,7 @@ public FunctionDefinition ToFunctionDefinition() for (int i = 0; i < parameters.Count; i++) { var parameter = parameters[i]; - properties.Add(parameter.Name, parameter.Schema ?? GetDefaultSchemaForTypelessParameter(GetDescription(parameter))); + properties.Add(parameter.Name, parameter.Schema ?? GetDefaultSchemaForTypelessParameter(parameter.Description)); if (parameter.IsRequired) { required.Add(parameter.Name); @@ -169,16 +165,6 @@ public FunctionDefinition ToFunctionDefinition() Description = this.Description, Parameters = resultParameters, }; - - static string GetDescription(OpenAIFunctionParameter param) - { - if (InternalTypeConverter.ConvertToString(param.DefaultValue) is string stringValue && !string.IsNullOrEmpty(stringValue)) - { - return $"{param.Description} (default value: {stringValue})"; - } - - return param.Description; - } } /// Gets a for a typeless parameter with the specified description, defaulting to typeof(string) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIKernelFunctionMetadataExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIKernelFunctionMetadataExtensions.cs index adf1db651066..6859e1225dd6 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIKernelFunctionMetadataExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIKernelFunctionMetadataExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Linq; namespace Microsoft.SemanticKernel.Connectors.OpenAI; @@ -26,11 +25,10 @@ public static OpenAIFunction ToOpenAIFunction(this KernelFunctionMetadata metada openAIParams[i] = new OpenAIFunctionParameter( param.Name, - param.Description, + GetDescription(param), param.IsRequired, param.ParameterType, - param.Schema, - param.DefaultValue); + param.Schema); } return new OpenAIFunction( @@ -42,33 +40,15 @@ public static OpenAIFunction ToOpenAIFunction(this KernelFunctionMetadata metada metadata.ReturnParameter.Description, metadata.ReturnParameter.ParameterType, metadata.ReturnParameter.Schema)); - } - /// - /// Convert an to a . - /// - /// The object to convert. - /// An object. - public static KernelFunctionMetadata ToKernelFunctionMetadata(this OpenAIFunction function) - { - return new KernelFunctionMetadata(function.FunctionName) + static string GetDescription(KernelParameterMetadata param) { - PluginName = function.PluginName, - Description = function.Description, - Parameters = function.Parameters?.Select(p => new KernelParameterMetadata(p.Name) + if (InternalTypeConverter.ConvertToString(param.DefaultValue) is string stringValue && !string.IsNullOrEmpty(stringValue)) { - Description = p.Description, - DefaultValue = p.DefaultValue, - IsRequired = p.IsRequired, - ParameterType = p.ParameterType, - Schema = p.Schema, - }).ToList() ?? [], - ReturnParameter = function.ReturnParameter is null ? new() : new KernelReturnParameterMetadata() - { - Description = function.ReturnParameter.Description, - ParameterType = function.ReturnParameter.ParameterType, - Schema = function.ReturnParameter.Schema, - }, - }; + return $"{param.Description} (default value: {stringValue})"; + } + + return param.Description; + } } } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs index deed6568f392..8707adde442c 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs @@ -231,11 +231,12 @@ public IDictionary? TokenSelectionBiases /// public ToolCallBehavior? ToolCallBehavior { - get => base.FunctionChoiceBehavior as ToolCallBehavior; + get => this._toolCallBehavior; + set { this.ThrowIfFrozen(); - base.FunctionChoiceBehavior = value; + this._toolCallBehavior = value; } } @@ -423,6 +424,7 @@ public static OpenAIPromptExecutionSettings FromExecutionSettingsWithData(Prompt private long? _seed; private object? _responseFormat; private IDictionary? _tokenSelectionBiases; + private ToolCallBehavior? _toolCallBehavior; private string? _user; private string? _chatSystemPrompt; private bool? _logprobs; diff --git a/dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs b/dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs index d38ede8bd1c5..7a5490c736ea 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs @@ -6,11 +6,12 @@ using System.Diagnostics; using System.Linq; using System.Text.Json; +using Azure.AI.OpenAI; namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// Represents a behavior for OpenAI tool calls. -public abstract class ToolCallBehavior : FunctionChoiceBehavior +public abstract class ToolCallBehavior { // 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: @@ -117,6 +118,11 @@ private ToolCallBehavior(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); + /// /// 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. @@ -127,31 +133,22 @@ internal KernelFunctions(bool autoInvoke) : base(autoInvoke) { } public override string ToString() => $"{nameof(KernelFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0})"; - public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) + internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) { - List? functionsMetadata = null; - // If no kernel is provided, we don't have any tools to provide. - if (context.Kernel is not null) + if (kernel is not null) { // Provide all functions from the kernel. - IList functions = context.Kernel.Plugins.GetFunctionsMetadata(); + IList functions = kernel.Plugins.GetFunctionsMetadata(); if (functions.Count > 0) { + options.ToolChoice = ChatCompletionsToolChoice.Auto; for (int i = 0; i < functions.Count; i++) { - (functionsMetadata ??= []).Add(functions[i]); + options.Tools.Add(new ChatCompletionsFunctionToolDefinition(functions[i].ToOpenAIFunction().ToFunctionDefinition())); } } } - - return new FunctionChoiceBehaviorConfiguration() - { - Choice = FunctionChoice.Auto, - FunctionsMetadata = functionsMetadata, - AutoInvoke = this.MaximumAutoInvokeAttempts > 0, - AllowAnyRequestedKernelFunction = this.AllowAnyRequestedKernelFunction, - }; } internal override bool AllowAnyRequestedKernelFunction => true; @@ -163,19 +160,29 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho internal sealed class EnabledFunctions : ToolCallBehavior { private readonly OpenAIFunction[] _openAIFunctions; + private readonly ChatCompletionsFunctionToolDefinition[] _functions; public EnabledFunctions(IEnumerable functions, bool autoInvoke) : base(autoInvoke) { this._openAIFunctions = functions.ToArray(); + + var defs = new ChatCompletionsFunctionToolDefinition[this._openAIFunctions.Length]; + for (int i = 0; i < defs.Length; i++) + { + defs[i] = new ChatCompletionsFunctionToolDefinition(this._openAIFunctions[i].ToFunctionDefinition()); + } + this._functions = defs; } - public override string ToString() => $"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {string.Join(", ", this._openAIFunctions.Select(f => f.FunctionName))}"; + public override string ToString() => $"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {string.Join(", ", this._functions.Select(f => f.Name))}"; - public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) + internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) { - List? functionsMetadata = null; + OpenAIFunction[] openAIFunctions = this._openAIFunctions; + ChatCompletionsFunctionToolDefinition[] functions = this._functions; + Debug.Assert(openAIFunctions.Length == functions.Length); - if (this._openAIFunctions.Length > 0) + if (openAIFunctions.Length > 0) { bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; @@ -184,40 +191,29 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho // and then fail to do so, so we fail before we get to that point. This is an error // on the consumers behalf: if they specify auto-invocation with any functions, they must // specify the kernel and the kernel must contain those functions. - if (autoInvoke && context.Kernel is null) + if (autoInvoke && kernel is null) { throw new KernelException($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided."); } - for (int i = 0; i < this._openAIFunctions.Length; i++) + options.ToolChoice = ChatCompletionsToolChoice.Auto; + for (int i = 0; i < openAIFunctions.Length; i++) { - functionsMetadata ??= []; - // Make sure that if auto-invocation is specified, every enabled function can be found in the kernel. if (autoInvoke) { - Debug.Assert(context.Kernel is not null); - OpenAIFunction f = this._openAIFunctions[i]; - if (!context.Kernel!.Plugins.TryGetFunction(f.PluginName, f.FunctionName, out var func)) + Debug.Assert(kernel is not null); + OpenAIFunction f = openAIFunctions[i]; + if (!kernel!.Plugins.TryGetFunction(f.PluginName, f.FunctionName, out _)) { throw new KernelException($"The specified {nameof(EnabledFunctions)} function {f.FullyQualifiedName} is not available in the kernel."); } - functionsMetadata.Add(func.Metadata); - } - else - { - functionsMetadata.Add(this._openAIFunctions[i].ToKernelFunctionMetadata()); } + + // Add the function. + options.Tools.Add(functions[i]); } } - - return new FunctionChoiceBehaviorConfiguration() - { - Choice = FunctionChoice.Auto, - FunctionsMetadata = functionsMetadata, - AutoInvoke = this.MaximumAutoInvokeAttempts > 0, - AllowAnyRequestedKernelFunction = this.AllowAnyRequestedKernelFunction, - }; } } @@ -225,15 +221,19 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho internal sealed class RequiredFunction : ToolCallBehavior { private readonly OpenAIFunction _function; + private readonly ChatCompletionsFunctionToolDefinition _tool; + private readonly ChatCompletionsToolChoice _choice; public RequiredFunction(OpenAIFunction function, bool autoInvoke) : base(autoInvoke) { this._function = function; + this._tool = new ChatCompletionsFunctionToolDefinition(function.ToFunctionDefinition()); + this._choice = new ChatCompletionsToolChoice(this._tool); } - public override string ToString() => $"{nameof(RequiredFunction)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {this._function.FunctionName}"; + public override string ToString() => $"{nameof(RequiredFunction)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {this._tool.Name}"; - public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) + internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) { bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; @@ -242,24 +242,19 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho // and then fail to do so, so we fail before we get to that point. This is an error // on the consumers behalf: if they specify auto-invocation with any functions, they must // specify the kernel and the kernel must contain those functions. - if (autoInvoke && context.Kernel is null) + if (autoInvoke && kernel is null) { throw new KernelException($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided."); } // Make sure that if auto-invocation is specified, the required function can be found in the kernel. - if (autoInvoke && !context.Kernel!.Plugins.TryGetFunction(this._function.PluginName, this._function.FunctionName, out _)) + if (autoInvoke && !kernel!.Plugins.TryGetFunction(this._function.PluginName, this._function.FunctionName, out _)) { throw new KernelException($"The specified {nameof(RequiredFunction)} function {this._function.FullyQualifiedName} is not available in the kernel."); } - return new FunctionChoiceBehaviorConfiguration() - { - Choice = FunctionChoice.Required, - FunctionsMetadata = [this._function.ToKernelFunctionMetadata()], - AutoInvoke = autoInvoke, - AllowAnyRequestedKernelFunction = this.AllowAnyRequestedKernelFunction, - }; + options.ToolChoice = this._choice; + options.Tools.Add(this._tool); } /// Gets how many requests are part of a single interaction should include this tool in the request. diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs index dd601c813673..b45fc64b60ba 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs @@ -106,7 +106,7 @@ public void ItCanConvertToOpenAIFunctionWithParameter(bool withSchema) // Assert Assert.Equal(param1.Name, outputParam.Name); - Assert.Equal("This is param1", outputParam.Description); + Assert.Equal("This is param1 (default value: 1)", outputParam.Description); Assert.Equal(param1.IsRequired, outputParam.IsRequired); Assert.NotNull(outputParam.Schema); Assert.Equal("integer", outputParam.Schema.RootElement.GetProperty("type").GetString()); @@ -196,7 +196,7 @@ public void ItCanCreateValidOpenAIFunctionManualForPlugin() // Assert Assert.NotNull(result); Assert.Equal( - """{"type":"object","required":["parameter1"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"type":"string","enum":["Value1","Value2"],"description":"Enum parameter (default value: Value2)"},"parameter3":{"type":["string","null"],"format":"date-time","description":"DateTime parameter"}}}""", + """{"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() ); } @@ -247,8 +247,8 @@ private sealed class MyPlugin [KernelFunction, Description("My sample function.")] public string MyFunction( [Description("String parameter")] string parameter1, - [Description("Enum parameter")] MyEnum parameter2 = MyEnum.Value2, - [Description("DateTime parameter")] DateTime? parameter3 = null + [Description("Enum parameter")] MyEnum parameter2, + [Description("DateTime parameter")] DateTime parameter3 ) { return "return"; diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs index dcf33c8cebff..a9f94d81a673 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs @@ -92,12 +92,12 @@ public void ItCanConvertToFunctionDefinitionWithPluginName() [Fact] public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParameterType() { - string expectedParameterSchema = """{"type":"object","required":["param1","param2"],"properties":{"param1":{"type":"string","description":"String param 1"},"param2":{"type":"integer","description":"Int param 2"},"param3":{"type":"number","description":"double param 2 (default value: 34.8)"}}}"""; + string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] { KernelFunctionFactory.CreateFromMethod( - [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2, [Description("double param 2")] double param3 = 34.8) => "", + [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => "", "TestFunction", "My test function") }); @@ -118,12 +118,12 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParamete [Fact] public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParameterType() { - string expectedParameterSchema = """{"type":"object","required":["param1","param2"],"properties":{"param1":{"type":"string","description":"String param 1"},"param2":{"type":"integer","description":"Int param 2"},"param3":{"type":"number","description":"double param 2 (default value: 34.8)"}}}"""; + string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] { KernelFunctionFactory.CreateFromMethod( - [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2, [Description("double param 2")] double param3 = 34.8) => { }, + [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => { }, "TestFunction", "My test function") }); @@ -178,47 +178,6 @@ public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescript JsonSerializer.Serialize(pd.properties.First().Value.RootElement)); } - [Fact] - public void ItCanConvertToFunctionMetadata() - { - // Arrange - OpenAIFunction f = new("p1", "f1", "description", new[] - { - new OpenAIFunctionParameter("param1", "param1 description", true, typeof(string), KernelJsonSchema.Parse("""{ "type":"string" }""")), - new OpenAIFunctionParameter("param2", "param2 description", false, typeof(int), KernelJsonSchema.Parse("""{ "type":"integer" }""")), - }, - new OpenAIFunctionReturnParameter("return description", typeof(string), KernelJsonSchema.Parse("""{ "type":"string" }"""))); - - // Act - KernelFunctionMetadata result = f.ToKernelFunctionMetadata(); - - // Assert - Assert.Equal("p1", result.PluginName); - Assert.Equal("f1", result.Name); - Assert.Equal("description", result.Description); - - Assert.Equal(2, result.Parameters.Count); - - var param1 = result.Parameters[0]; - Assert.Equal("param1", param1.Name); - Assert.Equal("param1 description", param1.Description); - Assert.True(param1.IsRequired); - Assert.Equal(typeof(string), param1.ParameterType); - Assert.Equal("string", param1.Schema?.RootElement.GetProperty("type").GetString()); - - var param2 = result.Parameters[1]; - Assert.Equal("param2", param2.Name); - Assert.Equal("param2 description", param2.Description); - Assert.False(param2.IsRequired); - Assert.Equal(typeof(int), param2.ParameterType); - Assert.Equal("integer", param2.Schema?.RootElement.GetProperty("type").GetString()); - - Assert.NotNull(result.ReturnParameter); - Assert.Equal("return description", result.ReturnParameter.Description); - Assert.Equal(typeof(string), result.ReturnParameter.ParameterType); - Assert.Equal("string", result.ReturnParameter.Schema?.RootElement.GetProperty("type").GetString()); - } - #pragma warning disable CA1812 // uninstantiated internal class private sealed class ParametersData { diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs index 8c96601371b0..d39480ebfe8d 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs +++ b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; +using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; using Xunit; @@ -63,12 +64,13 @@ public void KernelFunctionsConfigureOptionsWithNullKernelDoesNotAddTools() { // Arrange var kernelFunctions = new KernelFunctions(autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); // Act - var config = kernelFunctions.GetConfiguration(new()); + kernelFunctions.ConfigureOptions(null, chatCompletionsOptions); // Assert - Assert.Null(config.FunctionsMetadata); + Assert.Empty(chatCompletionsOptions.Tools); } [Fact] @@ -76,14 +78,15 @@ public void KernelFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() { // Arrange var kernelFunctions = new KernelFunctions(autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); // Act - var config = kernelFunctions.GetConfiguration(new() { Kernel = kernel }); + kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); // Assert - Assert.Equal(FunctionChoice.Auto, config.Choice); - Assert.Null(config.FunctionsMetadata); + Assert.Null(chatCompletionsOptions.ToolChoice); + Assert.Empty(chatCompletionsOptions.Tools); } [Fact] @@ -91,6 +94,7 @@ public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() { // Arrange var kernelFunctions = new KernelFunctions(autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); var plugin = this.GetTestPlugin(); @@ -98,12 +102,12 @@ public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() kernel.Plugins.Add(plugin); // Act - var config = kernelFunctions.GetConfiguration(new() { Kernel = kernel }); + kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); // Assert - Assert.Equal(FunctionChoice.Auto, config.Choice); + Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); - this.AssertFunctions(config.FunctionsMetadata); + this.AssertTools(chatCompletionsOptions); } [Fact] @@ -111,13 +115,14 @@ public void EnabledFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() { // Arrange var enabledFunctions = new EnabledFunctions([], autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); // Act - var config = enabledFunctions.GetConfiguration(new()); + enabledFunctions.ConfigureOptions(null, chatCompletionsOptions); // Assert - Assert.Equal(FunctionChoice.Auto, config.Choice); - Assert.Null(config.FunctionsMetadata); + Assert.Null(chatCompletionsOptions.ToolChoice); + Assert.Empty(chatCompletionsOptions.Tools); } [Fact] @@ -126,9 +131,10 @@ public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsExc // Arrange var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); + var chatCompletionsOptions = new ChatCompletionsOptions(); // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.GetConfiguration(new())); + var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(null, chatCompletionsOptions)); Assert.Equal($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided.", exception.Message); } @@ -138,10 +144,11 @@ public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsEx // Arrange var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); + var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.GetConfiguration(new() { Kernel = kernel })); + var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(kernel, chatCompletionsOptions)); Assert.Equal($"The specified {nameof(EnabledFunctions)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); } @@ -154,16 +161,18 @@ public void EnabledFunctionsConfigureOptionsWithKernelAndPluginsAddsTools(bool a var plugin = this.GetTestPlugin(); var functions = plugin.GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); var enabledFunctions = new EnabledFunctions(functions, autoInvoke); + var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); kernel.Plugins.Add(plugin); // Act - var config = enabledFunctions.GetConfiguration(new() { Kernel = kernel }); + enabledFunctions.ConfigureOptions(kernel, chatCompletionsOptions); // Assert - Assert.Equal(FunctionChoice.Auto, config.Choice); - this.AssertFunctions(config.FunctionsMetadata); + Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); + + this.AssertTools(chatCompletionsOptions); } [Fact] @@ -172,9 +181,10 @@ public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsEx // Arrange var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()).First(); var requiredFunction = new RequiredFunction(function, autoInvoke: true); + var chatCompletionsOptions = new ChatCompletionsOptions(); // Act & Assert - var exception = Assert.Throws(() => requiredFunction.GetConfiguration(new())); + var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(null, chatCompletionsOptions)); Assert.Equal($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided.", exception.Message); } @@ -184,10 +194,11 @@ public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsE // Arrange var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()).First(); var requiredFunction = new RequiredFunction(function, autoInvoke: true); + var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); // Act & Assert - var exception = Assert.Throws(() => requiredFunction.GetConfiguration(new() { Kernel = kernel })); + var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(kernel, chatCompletionsOptions)); Assert.Equal($"The specified {nameof(RequiredFunction)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); } @@ -197,17 +208,18 @@ public void RequiredFunctionConfigureOptionsAddsTools() // Arrange var plugin = this.GetTestPlugin(); var function = plugin.GetFunctionsMetadata()[0].ToOpenAIFunction(); + var chatCompletionsOptions = new ChatCompletionsOptions(); var requiredFunction = new RequiredFunction(function, autoInvoke: true); var kernel = new Kernel(); kernel.Plugins.Add(plugin); // Act - var config = requiredFunction.GetConfiguration(new() { Kernel = kernel }); + requiredFunction.ConfigureOptions(kernel, chatCompletionsOptions); // Assert - Assert.NotNull(config.FunctionsMetadata); + Assert.NotNull(chatCompletionsOptions.ToolChoice); - this.AssertFunctions(config.FunctionsMetadata); + this.AssertTools(chatCompletionsOptions); } private KernelPlugin GetTestPlugin() @@ -222,25 +234,16 @@ private KernelPlugin GetTestPlugin() return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); } - private void AssertFunctions(IEnumerable? functions) + private void AssertTools(ChatCompletionsOptions chatCompletionsOptions) { - Assert.NotNull(functions); - - var function = Assert.Single(functions); - - Assert.NotNull(function); - - Assert.Equal("MyPlugin", function.PluginName); - Assert.Equal("MyFunction", function.Name); - Assert.Equal("Test Function", function.Description); + Assert.Single(chatCompletionsOptions.Tools); - Assert.NotNull(function.Parameters); - Assert.Equal(2, function.Parameters.Count); + var tool = chatCompletionsOptions.Tools[0] as ChatCompletionsFunctionToolDefinition; - Assert.Equal("parameter1", function.Parameters[0].Name); - Assert.Equal("parameter2", function.Parameters[1].Name); + Assert.NotNull(tool); - Assert.NotNull(function.ReturnParameter); - Assert.Equal("Function Result", function.ReturnParameter.Description); + 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()); } } diff --git a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs index e6df9363248b..363b28d2f2f9 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs @@ -30,9 +30,6 @@ public void ItShouldInitializeFunctionChoiceBehaviorsFromMarkdown() { // Arrange var kernel = new Kernel(); - kernel.Plugins.AddFromFunctions("p1", [KernelFunctionFactory.CreateFromMethod(() => { }, "f1")]); - kernel.Plugins.AddFromFunctions("p2", [KernelFunctionFactory.CreateFromMethod(() => { }, "f2")]); - kernel.Plugins.AddFromFunctions("p3", [KernelFunctionFactory.CreateFromMethod(() => { }, "f3")]); // Act var function = KernelFunctionMarkdown.CreateFromPromptMarkdown(Markdown, "TellMeAbout"); @@ -45,36 +42,34 @@ public void ItShouldInitializeFunctionChoiceBehaviorsFromMarkdown() // AutoFunctionCallChoice for service1 var service1ExecutionSettings = function.ExecutionSettings["service1"]; - Assert.NotNull(service1ExecutionSettings?.FunctionChoiceBehavior); + Assert.NotNull(service1ExecutionSettings); - var autoConfig = service1ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); - Assert.NotNull(autoConfig); - Assert.Equal(FunctionChoice.Auto, autoConfig.Choice); - Assert.NotNull(autoConfig.FunctionsMetadata); - Assert.Equal("p1", autoConfig.FunctionsMetadata.Single().PluginName); - Assert.Equal("f1", autoConfig.FunctionsMetadata.Single().Name); + var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; + Assert.NotNull(autoFunctionChoiceBehavior); + + Assert.NotNull(autoFunctionChoiceBehavior.Functions); + Assert.Single(autoFunctionChoiceBehavior.Functions); + Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.First()); // RequiredFunctionCallChoice for service2 var service2ExecutionSettings = function.ExecutionSettings["service2"]; - Assert.NotNull(service2ExecutionSettings?.FunctionChoiceBehavior); + Assert.NotNull(service2ExecutionSettings); - var requiredConfig = service2ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); - Assert.NotNull(requiredConfig); - Assert.Equal(FunctionChoice.Required, requiredConfig.Choice); - Assert.NotNull(requiredConfig.FunctionsMetadata); - Assert.Equal("p2", requiredConfig.FunctionsMetadata.Single().PluginName); - Assert.Equal("f2", requiredConfig.FunctionsMetadata.Single().Name); + var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; + Assert.NotNull(requiredFunctionChoiceBehavior); + Assert.NotNull(requiredFunctionChoiceBehavior.Functions); + Assert.Single(requiredFunctionChoiceBehavior.Functions); + Assert.Equal("p1-f1", requiredFunctionChoiceBehavior.Functions.First()); // NoneFunctionCallChoice for service3 var service3ExecutionSettings = function.ExecutionSettings["service3"]; - Assert.NotNull(service3ExecutionSettings?.FunctionChoiceBehavior); - - var noneConfig = service3ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); - Assert.NotNull(noneConfig); - Assert.Equal(FunctionChoice.None, noneConfig.Choice); - Assert.NotNull(noneConfig.FunctionsMetadata); - Assert.Equal("p3", noneConfig.FunctionsMetadata.Single().PluginName); - Assert.Equal("f3", noneConfig.FunctionsMetadata.Single().Name); + Assert.NotNull(service3ExecutionSettings); + + var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; + Assert.NotNull(noneFunctionChoiceBehavior); + Assert.NotNull(noneFunctionChoiceBehavior.Functions); + Assert.Single(noneFunctionChoiceBehavior.Functions); + Assert.Equal("p1-f1", noneFunctionChoiceBehavior.Functions.First()); } [Fact] @@ -117,7 +112,7 @@ These are more AI execution settings "temperature": 0.8, "function_choice_behavior": { "type": "required", - "functions": ["p2.f2"] + "functions": ["p1.f1"] } } } @@ -130,7 +125,7 @@ These are AI execution settings as well "temperature": 0.8, "function_choice_behavior": { "type": "none", - "functions": ["p3.f3"] + "functions": ["p1.f1"] } } } diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs new file mode 100644 index 000000000000..d84f56d03afa --- /dev/null +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using Microsoft.SemanticKernel; +using Xunit; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace SemanticKernel.Functions.UnitTests.Yaml; + +/// +/// Tests for . +/// +public sealed class FunctionChoiceBehaviorTypesConverterTests +{ + [Fact] + public void ItShouldDeserializeAutoFunctionChoiceBehavior() + { + // Arrange + var deserializer = new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) + .Build(); + + var yaml = """ + type: auto + functions: + - p1.f1 + """; + + // Act + var behavior = deserializer.Deserialize(yaml); + + // Assert + Assert.NotNull(behavior.Functions); + Assert.Single(behavior.Functions); + Assert.Equal("p1-f1", behavior.Functions.Single()); + } + + [Fact] + public void ItShouldDeserializeRequiredFunctionChoiceBehavior() + { + // Arrange + var deserializer = new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) + .Build(); + + var yaml = """ + type: required + functions: + - p2.f2 + """; + + // Act + var behavior = deserializer.Deserialize(yaml); + + // Assert + Assert.NotNull(behavior.Functions); + Assert.Single(behavior.Functions); + Assert.Equal("p2-f2", behavior.Functions.Single()); + } + + [Fact] + public void ItShouldDeserializeNoneFunctionChoiceBehavior() + { + // Arrange + var deserializer = new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) + .Build(); + + var yaml = """ + type: none, + functions: + - p1.f1 + """; + + // Act + var behavior = deserializer.Deserialize(yaml); + + // Assert + Assert.NotNull(behavior.Functions); + Assert.Single(behavior.Functions); + Assert.Equal("p1-f1", behavior.Functions.Single()); + } +} diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs index 11eac6a0e80c..4db0022e2e87 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs @@ -89,47 +89,31 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() // Act var promptTemplateConfig = KernelFunctionYaml.ToPromptTemplateConfig(this._yaml); - var kernel = new Kernel(); - kernel.Plugins.AddFromFunctions("p1", [KernelFunctionFactory.CreateFromMethod(() => { }, "f1")]); - kernel.Plugins.AddFromFunctions("p2", [KernelFunctionFactory.CreateFromMethod(() => { }, "f2")]); - kernel.Plugins.AddFromFunctions("p3", [KernelFunctionFactory.CreateFromMethod(() => { }, "f3")]); - // Assert Assert.NotNull(promptTemplateConfig?.ExecutionSettings); Assert.Equal(3, promptTemplateConfig.ExecutionSettings.Count); // Service with auto function choice behavior var service1ExecutionSettings = promptTemplateConfig.ExecutionSettings["service1"]; - Assert.NotNull(service1ExecutionSettings?.FunctionChoiceBehavior); - var autoConfig = service1ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); - Assert.NotNull(autoConfig); - Assert.Equal(FunctionChoice.Auto, autoConfig.Choice); - Assert.NotNull(autoConfig.FunctionsMetadata); - Assert.Equal("p1", autoConfig.FunctionsMetadata.Single().PluginName); - Assert.Equal("f1", autoConfig.FunctionsMetadata.Single().Name); + var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; + Assert.NotNull(autoFunctionChoiceBehavior?.Functions); + Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.Single()); // Service with required function choice behavior var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; - Assert.NotNull(service2ExecutionSettings?.FunctionChoiceBehavior); - var requiredConfig = service2ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); - Assert.NotNull(requiredConfig); - Assert.Equal(FunctionChoice.Required, requiredConfig.Choice); - Assert.NotNull(requiredConfig.FunctionsMetadata); - Assert.Equal("p2", requiredConfig.FunctionsMetadata.Single().PluginName); - Assert.Equal("f2", requiredConfig.FunctionsMetadata.Single().Name); + var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; + Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); + Assert.Equal("p2-f2", requiredFunctionChoiceBehavior.Functions.Single()); // Service with none function choice behavior var service3ExecutionSettings = promptTemplateConfig.ExecutionSettings["service3"]; - Assert.NotNull(service3ExecutionSettings?.FunctionChoiceBehavior); - var noneConfig = service3ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); - Assert.NotNull(noneConfig); - Assert.Equal(FunctionChoice.None, noneConfig.Choice); - Assert.NotNull(noneConfig.FunctionsMetadata); - Assert.Equal("p3", noneConfig.FunctionsMetadata.Single().PluginName); - Assert.Equal("f3", noneConfig.FunctionsMetadata.Single().Name); + var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; + Assert.NotNull(noneFunctionChoiceBehavior); + Assert.NotNull(noneFunctionChoiceBehavior?.Functions); + Assert.Equal("p3-f3", noneFunctionChoiceBehavior.Functions.Single()); } [Fact] diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs index 91a142d8c0dd..fe5717f3cd68 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs @@ -40,12 +40,6 @@ public void ItShouldCreatePromptFunctionFromYamlWithCustomModelSettings() [Fact] public void ItShouldDeserializeFunctionChoiceBehaviors() { - // Arrange - var kernel = new Kernel(); - kernel.Plugins.AddFromFunctions("p1", [KernelFunctionFactory.CreateFromMethod(() => { }, "f1")]); - kernel.Plugins.AddFromFunctions("p2", [KernelFunctionFactory.CreateFromMethod(() => { }, "f2")]); - kernel.Plugins.AddFromFunctions("p3", [KernelFunctionFactory.CreateFromMethod(() => { }, "f3")]); - // Act var promptTemplateConfig = KernelFunctionYaml.ToPromptTemplateConfig(this._yaml); @@ -55,36 +49,25 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() // Service with auto function choice behavior var service1ExecutionSettings = promptTemplateConfig.ExecutionSettings["service1"]; - Assert.NotNull(service1ExecutionSettings?.FunctionChoiceBehavior); - var autoConfig = service1ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); - Assert.NotNull(autoConfig); - Assert.Equal(FunctionChoice.Auto, autoConfig.Choice); - Assert.NotNull(autoConfig.FunctionsMetadata); - Assert.Equal("p1", autoConfig.FunctionsMetadata.Single().PluginName); - Assert.Equal("f1", autoConfig.FunctionsMetadata.Single().Name); + var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; + Assert.NotNull(autoFunctionChoiceBehavior?.Functions); + Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.Single()); // Service with required function choice behavior var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; - Assert.NotNull(service2ExecutionSettings?.FunctionChoiceBehavior); - var requiredConfig = service2ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); - Assert.NotNull(requiredConfig); - Assert.Equal(FunctionChoice.Required, requiredConfig.Choice); - Assert.NotNull(requiredConfig.FunctionsMetadata); - Assert.Equal("p2", requiredConfig.FunctionsMetadata.Single().PluginName); - Assert.Equal("f2", requiredConfig.FunctionsMetadata.Single().Name); + var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; + Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); + Assert.Equal("p2-f2", requiredFunctionChoiceBehavior.Functions.Single()); // Service with none function choice behavior var service3ExecutionSettings = promptTemplateConfig.ExecutionSettings["service3"]; - Assert.NotNull(service3ExecutionSettings?.FunctionChoiceBehavior); - var noneConfig = service3ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); - Assert.NotNull(noneConfig); - Assert.Equal(FunctionChoice.None, noneConfig.Choice); - Assert.NotNull(noneConfig.FunctionsMetadata); - Assert.Equal("p3", noneConfig.FunctionsMetadata.Single().PluginName); - Assert.Equal("f3", noneConfig.FunctionsMetadata.Single().Name); + var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; + Assert.NotNull(noneFunctionChoiceBehavior); + Assert.NotNull(noneFunctionChoiceBehavior?.Functions); + Assert.Equal("p3-f3", noneFunctionChoiceBehavior.Functions.Single()); } private readonly string _yaml = """ diff --git a/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs b/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs new file mode 100644 index 000000000000..8f5b0f42be6d --- /dev/null +++ b/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using YamlDotNet.Core; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Microsoft.SemanticKernel; + +/// +/// Allows custom deserialization for derivatives of . +/// +internal sealed class FunctionChoiceBehaviorTypesConverter : IYamlTypeConverter +{ + private const char PromptFunctionNameSeparator = '.'; + + private const char FunctionNameSeparator = '-'; + + private static IDeserializer? s_deserializer; + + /// + public bool Accepts(Type type) + { +#pragma warning disable SKEXP0001 + return + type == typeof(AutoFunctionChoiceBehavior) || + type == typeof(RequiredFunctionChoiceBehavior) || + type == typeof(NoneFunctionChoiceBehavior); +#pragma warning restore SKEXP0001 + } + + public object? ReadYaml(IParser parser, Type type) + { + s_deserializer ??= new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination. Otherwise, the "Property 'type' not found on type '{type.FullName}'" exception is thrown. + .Build(); + +#pragma warning disable SKEXP0001 + if (type == typeof(AutoFunctionChoiceBehavior)) + { + var behavior = s_deserializer.Deserialize(parser); + behavior.Functions = ConvertFunctionNames(behavior.Functions); + return behavior; + } + else if (type == typeof(RequiredFunctionChoiceBehavior)) + { + var behavior = s_deserializer.Deserialize(parser); + behavior.Functions = ConvertFunctionNames(behavior.Functions); + return behavior; + } + else if (type == typeof(NoneFunctionChoiceBehavior)) + { + var behavior = s_deserializer.Deserialize(parser); + behavior.Functions = ConvertFunctionNames(behavior.Functions); + return behavior; + } + + throw new YamlException($"Unexpected type '{type.FullName}' for function choice behavior."); +#pragma warning restore SKEXP0001 + } + + /// + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + throw new NotImplementedException(); + } + + private static IList? ConvertFunctionNames(IList? functions) + { + if (functions is null) + { + return functions; + } + + return functions.Select(fqn => + { + var functionName = fqn ?? throw new YamlException("Expected a non-null YAML string."); + return functionName.Replace(PromptFunctionNameSeparator, FunctionNameSeparator); + }).ToList(); + } +} diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs index 8edf6bf31e10..b4c5d8569f17 100644 --- a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs @@ -2,12 +2,9 @@ using System; using System.Collections.Generic; -using System.Text.Json.Serialization; -using System.Linq; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -using YamlDotNet.Serialization.BufferedDeserialization; using YamlDotNet.Serialization.NamingConventions; namespace Microsoft.SemanticKernel; @@ -30,8 +27,18 @@ public bool Accepts(Type type) { s_deserializer ??= new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) - .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination. Otherwise, the "Property 'type' not found on type '{type.FullName}'" exception is thrown. - .WithTypeDiscriminatingNodeDeserializer(ConfigureTypeDiscriminatingNodeDeserializer) + .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) + .WithTypeDiscriminatingNodeDeserializer((options) => + { +#pragma warning disable SKEXP0001 + options.AddKeyValueTypeDiscriminator("type", new Dictionary + { + { AutoFunctionChoiceBehavior.TypeDiscriminator, typeof(AutoFunctionChoiceBehavior) }, + { RequiredFunctionChoiceBehavior.TypeDiscriminator, typeof(RequiredFunctionChoiceBehavior) }, + { NoneFunctionChoiceBehavior.TypeDiscriminator, typeof(NoneFunctionChoiceBehavior) } + }); +#pragma warning restore SKEXP0010 + }) .Build(); parser.MoveNext(); // Move to the first property @@ -46,9 +53,7 @@ public bool Accepts(Type type) executionSettings.ModelId = s_deserializer.Deserialize(parser); break; case "function_choice_behavior": -#pragma warning disable SKEXP0001 executionSettings.FunctionChoiceBehavior = s_deserializer.Deserialize(parser); -#pragma warning restore SKEXP0010 break; default: (executionSettings.ExtensionData ??= new Dictionary()).Add(propertyName, s_deserializer.Deserialize(parser)); @@ -64,35 +69,4 @@ public void WriteYaml(IEmitter emitter, object? value, Type type) { throw new NotImplementedException(); } - - private static void ConfigureTypeDiscriminatingNodeDeserializer(ITypeDiscriminatingNodeDeserializerOptions options) - { -#pragma warning disable SKEXP0001 - var attributes = typeof(FunctionChoiceBehavior).GetCustomAttributes(false); - - // Getting the type discriminator property name - "type" from the JsonPolymorphicAttribute. - var discriminatorKey = attributes.OfType().Single().TypeDiscriminatorPropertyName; - if (string.IsNullOrEmpty(discriminatorKey)) - { - throw new InvalidOperationException("Type discriminator property name is not specified."); - } - - var discriminatorTypeMapping = new Dictionary(); - - // Getting FunctionChoiceBehavior subtypes and their type discriminators registered for polymorphic deserialization. - var derivedTypeAttributes = attributes.OfType(); - foreach (var derivedTypeAttribute in derivedTypeAttributes) - { - var discriminator = derivedTypeAttribute.TypeDiscriminator?.ToString(); - if (string.IsNullOrEmpty(discriminator)) - { - throw new InvalidOperationException($"Type discriminator is not specified for the {derivedTypeAttribute.DerivedType} type."); - } - - discriminatorTypeMapping.Add(discriminator!, derivedTypeAttribute.DerivedType); - } - - options.AddKeyValueTypeDiscriminator(discriminatorKey!, discriminatorTypeMapping); -#pragma warning restore SKEXP0010 - } } diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index eeeb7baf20db..1512530fe681 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -357,7 +357,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu } // Adding a simulated function call to the connector response message - var simulatedFunctionCall = new FunctionCallContent("weather_alert", id: "call_123"); + var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); messageContent.Items.Add(simulatedFunctionCall); // Adding a simulated function result to chat history diff --git a/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs b/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs index 02d7e67f56d9..76f54de92a56 100644 --- a/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs +++ b/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs @@ -28,8 +28,7 @@ internal sealed class FunctionName /// The plugin name. public FunctionName(string name, string? pluginName = null) { - Verify.ValidFunctionName(name); - if (pluginName is not null) { Verify.ValidPluginName(pluginName); } + Verify.NotNull(name); this.Name = name; this.PluginName = pluginName; @@ -44,9 +43,6 @@ public FunctionName(string name, string? pluginName = null) /// Fully-qualified name of the function. public static string ToFullyQualifiedName(string functionName, string? pluginName = null, string functionNameSeparator = "-") { - Verify.ValidFunctionName(functionName); - if (pluginName is not null) { Verify.ValidPluginName(pluginName); } - return string.IsNullOrEmpty(pluginName) ? functionName : $"{pluginName}{functionNameSeparator}{functionName}"; } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index 9bb0e9d2d54a..e9fe65991461 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -10,7 +11,8 @@ namespace Microsoft.SemanticKernel; /// Represents that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior allows the model to decide whether to call the functions and, if so, which ones to call. /// -internal sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior +[Experimental("SKEXP0001")] +public sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior { /// /// List of the functions that the model can choose from. @@ -22,6 +24,11 @@ internal sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior /// private readonly bool _autoInvoke = true; + /// + /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. + /// + public const string TypeDiscriminator = "auto"; + /// /// Initializes a new instance of the class. /// @@ -40,7 +47,7 @@ public AutoFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); + this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); } /// @@ -48,6 +55,7 @@ public AutoFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable's plugins' functions are provided to the model. /// [JsonPropertyName("functions")] + [JsonConverter(typeof(FunctionNameFormatJsonConverter))] public IList? Functions { get; set; } /// @@ -63,7 +71,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho throw new KernelException("Auto-invocation for Auto choice behavior is not supported when no kernel is provided."); } - List? availableFunctions = null; + List? availableFunctions = null; bool allowAnyRequestedKernelFunction = false; // Handle functions provided via the 'Functions' property as function fully qualified names. @@ -73,12 +81,12 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var functionFQN in functionFQNs) { - var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); + var nameParts = FunctionName.Parse(functionFQN); // Check if the function is available in the kernel. If it is, then connectors can find it for auto-invocation later. if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) { - availableFunctions.Add(function.Metadata); + availableFunctions.Add(function); continue; } @@ -92,7 +100,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); if (function is not null) { - availableFunctions.Add(function.Metadata); + availableFunctions.Add(function); continue; } @@ -107,14 +115,14 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var plugin in context.Kernel.Plugins) { availableFunctions ??= []; - availableFunctions.AddRange(plugin.Select(p => p.Metadata)); + availableFunctions.AddRange(plugin); } } return new FunctionChoiceBehaviorConfiguration() { Choice = FunctionChoice.Auto, - FunctionsMetadata = availableFunctions, + Functions = availableFunctions, AutoInvoke = this._autoInvoke, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index bf4c13aeef92..19e789beb61d 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -9,15 +9,19 @@ namespace Microsoft.SemanticKernel; /// /// Represents the base class for different function choice behaviors. /// -[JsonPolymorphic(TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)] -[JsonDerivedType(typeof(AutoFunctionChoiceBehavior), typeDiscriminator: "auto")] -[JsonDerivedType(typeof(RequiredFunctionChoiceBehavior), typeDiscriminator: "required")] -[JsonDerivedType(typeof(NoneFunctionChoiceBehavior), typeDiscriminator: "none")] +[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] +[JsonDerivedType(typeof(AutoFunctionChoiceBehavior), typeDiscriminator: AutoFunctionChoiceBehavior.TypeDiscriminator)] +[JsonDerivedType(typeof(RequiredFunctionChoiceBehavior), typeDiscriminator: RequiredFunctionChoiceBehavior.TypeDiscriminator)] +[JsonDerivedType(typeof(NoneFunctionChoiceBehavior), typeDiscriminator: NoneFunctionChoiceBehavior.TypeDiscriminator)] [Experimental("SKEXP0001")] public abstract class FunctionChoiceBehavior { - /// The separator used to separate plugin name and function name. - protected const string FunctionNameSeparator = "."; + /// + /// Creates a new instance of the class. + /// + internal FunctionChoiceBehavior() + { + } /// /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index ad5e1f3b9417..228281a3945a 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -11,23 +11,30 @@ namespace Microsoft.SemanticKernel; [Experimental("SKEXP0001")] public sealed class FunctionChoiceBehaviorConfiguration { + /// + /// Creates a new instance of the class. + /// + internal FunctionChoiceBehaviorConfiguration() + { + } + /// /// Represents an AI model's decision-making strategy for calling functions. /// - public FunctionChoice Choice { get; init; } + public FunctionChoice Choice { get; internal set; } /// /// The functions available for AI model. /// - public IEnumerable? FunctionsMetadata { get; init; } + public IEnumerable? Functions { get; internal set; } /// /// Indicates whether the functions should be automatically invoked by the AI service/connector. /// - public bool AutoInvoke { get; init; } = true; + public bool AutoInvoke { get; internal set; } = true; /// /// Specifies whether validation against a specified list of functions is required before allowing the model to request a function from the kernel. /// - public bool? AllowAnyRequestedKernelFunction { get; init; } + public bool? AllowAnyRequestedKernelFunction { get; internal set; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs new file mode 100644 index 000000000000..b828fa2f2c9a --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel; + +/// +/// A custom JSON converter for converting function names in a JSON array. +/// This converter replaces dots used as a function name separator in prompts with hyphens when reading and back when writing. +/// +[Experimental("SKEXP0001")] +public sealed class FunctionNameFormatJsonConverter : JsonConverter> +{ + private const char PromptFunctionNameSeparator = '.'; + + private const char FunctionNameSeparator = '-'; + + /// + public override IList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException("Expected a JSON array."); + } + + var functionNames = new List(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException("Expected a JSON string."); + } + + var functionName = reader.GetString() ?? throw new JsonException("Expected a non-null JSON string."); + + functionNames.Add(functionName.Replace(PromptFunctionNameSeparator, FunctionNameSeparator)); + } + + return functionNames; + } + + /// + public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + + foreach (string functionName in value) + { + writer.WriteStringValue(functionName.Replace(FunctionNameSeparator, PromptFunctionNameSeparator)); + } + + writer.WriteEndArray(); + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs index d0b99acab812..88e850aa99c9 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -14,13 +15,19 @@ namespace Microsoft.SemanticKernel; /// Although this behavior prevents the model from calling any functions, the model can use the provided function information /// to describe how it would complete the prompt if it had the ability to call the functions. /// -internal sealed class NoneFunctionChoiceBehavior : FunctionChoiceBehavior +[Experimental("SKEXP0001")] +public sealed class NoneFunctionChoiceBehavior : FunctionChoiceBehavior { /// /// List of the functions that the model can choose from. /// private readonly IEnumerable? _functions; + /// + /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. + /// + public const string TypeDiscriminator = "none"; + /// /// Initializes a new instance of the class. /// @@ -37,7 +44,7 @@ public NoneFunctionChoiceBehavior() public NoneFunctionChoiceBehavior(IEnumerable functions) { this._functions = functions; - this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); + this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); } /// @@ -45,12 +52,13 @@ public NoneFunctionChoiceBehavior(IEnumerable functions) /// If null or empty, all 's plugins' functions are provided to the model. /// [JsonPropertyName("functions")] + [JsonConverter(typeof(FunctionNameFormatJsonConverter))] public IList? Functions { get; set; } /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { - List? availableFunctions = null; + List? availableFunctions = null; // Handle functions provided via the 'Functions' property as function fully qualified names. if (this.Functions is { } functionFQNs && functionFQNs.Any()) @@ -59,12 +67,12 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var functionFQN in functionFQNs) { - var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); + var nameParts = FunctionName.Parse(functionFQN); // Check if the function is available in the kernel. if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) { - availableFunctions.Add(function.Metadata); + availableFunctions.Add(function); continue; } @@ -72,7 +80,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); if (function is not null) { - availableFunctions.Add(function.Metadata); + availableFunctions.Add(function); continue; } @@ -84,14 +92,15 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho { foreach (var plugin in context.Kernel.Plugins) { - (availableFunctions ??= []).AddRange(plugin.Select(p => p.Metadata)); + availableFunctions ??= []; + availableFunctions.AddRange(plugin); } } return new FunctionChoiceBehaviorConfiguration() { Choice = FunctionChoice.None, - FunctionsMetadata = availableFunctions, + Functions = availableFunctions, }; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index 55101fb838f2..c417a14393d5 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -10,7 +11,8 @@ namespace Microsoft.SemanticKernel; /// Represents that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. /// -internal sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior +[Experimental("SKEXP0001")] +public sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior { /// /// List of the functions that the model can choose from. @@ -22,6 +24,11 @@ internal sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior /// private readonly bool _autoInvoke = true; + /// + /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. + /// + public const string TypeDiscriminator = "required"; + /// /// Initializes a new instance of the class. /// @@ -40,7 +47,7 @@ public RequiredFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); + this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); } /// @@ -48,6 +55,7 @@ public RequiredFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable's plugins' functions are provided to the model. /// [JsonPropertyName("functions")] + [JsonConverter(typeof(FunctionNameFormatJsonConverter))] public IList? Functions { get; set; } /// @@ -63,7 +71,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho throw new KernelException("Auto-invocation for Required choice behavior is not supported when no kernel is provided."); } - List? availableFunctions = null; + List? availableFunctions = null; bool allowAnyRequestedKernelFunction = false; // Handle functions provided via the 'Functions' property as function fully qualified names. @@ -73,12 +81,12 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var functionFQN in functionFQNs) { - var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); + var nameParts = FunctionName.Parse(functionFQN); // Check if the function is available in the kernel. If it is, then connectors can find it for auto-invocation later. if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) { - availableFunctions.Add(function.Metadata); + availableFunctions.Add(function); continue; } @@ -92,7 +100,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); if (function is not null) { - availableFunctions.Add(function.Metadata); + availableFunctions.Add(function); continue; } @@ -106,16 +114,17 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var plugin in context.Kernel.Plugins) { - (availableFunctions ??= []).AddRange(plugin.Select(p => p.Metadata)); + availableFunctions ??= []; + availableFunctions.AddRange(plugin); } } return new FunctionChoiceBehaviorConfiguration() { Choice = FunctionChoice.Required, - FunctionsMetadata = availableFunctions, + Functions = availableFunctions, AutoInvoke = this._autoInvoke, - AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction, + AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; } } diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs index 6d3c2935ba0e..bbcb3fb808f5 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs @@ -34,11 +34,11 @@ public void ItShouldAdvertiseAllKernelFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(3, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] @@ -56,10 +56,10 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructor() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(2, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] @@ -72,7 +72,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() // Act var choiceBehavior = new AutoFunctionChoiceBehavior() { - Functions = ["MyPlugin.Function1", "MyPlugin.Function2"] + Functions = ["MyPlugin-Function1", "MyPlugin-Function2"] }; var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -80,10 +80,10 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(2, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] @@ -100,10 +100,10 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorForManualInvocat // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(2, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] @@ -121,11 +121,11 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(3, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] @@ -176,8 +176,8 @@ public void ItShouldInitializeFunctionPropertyByFunctionsPassedViaConstructor() Assert.NotNull(choiceBehavior.Functions); Assert.Equal(2, choiceBehavior.Functions.Count); - Assert.Equal("MyPlugin.Function1", choiceBehavior.Functions.ElementAt(0)); - Assert.Equal("MyPlugin.Function2", choiceBehavior.Functions.ElementAt(1)); + Assert.Equal("MyPlugin-Function1", choiceBehavior.Functions.ElementAt(0)); + Assert.Equal("MyPlugin-Function2", choiceBehavior.Functions.ElementAt(1)); } [Fact] @@ -212,7 +212,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegis choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("The specified function MyPlugin.Function1 is not available in the kernel.", exception.Message); + Assert.Equal("The specified function MyPlugin-Function1 is not available in the kernel.", exception.Message); } [Fact] @@ -224,7 +224,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste var choiceBehavior = new AutoFunctionChoiceBehavior(autoInvoke: false) { - Functions = ["MyPlugin.NonKernelFunction"] + Functions = ["MyPlugin-NonKernelFunction"] }; // Act @@ -233,7 +233,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("No instance of the specified function MyPlugin.NonKernelFunction is found.", exception.Message); + Assert.Equal("No instance of the specified function MyPlugin-NonKernelFunction is found.", exception.Message); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs index 6d7f0e1ec13c..959a76bbf328 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs @@ -25,7 +25,7 @@ public void ItShouldDeserializeAutoFunctionChoiceBehavior() // Assert Assert.NotNull(behavior?.Functions); Assert.Single(behavior.Functions); - Assert.Equal("p1.f1", behavior.Functions.Single()); + Assert.Equal("p1-f1", behavior.Functions.Single()); } [Fact] @@ -45,26 +45,6 @@ public void ItShouldDeserializeRequiredFunctionChoiceBehavior() // Assert Assert.NotNull(behavior?.Functions); Assert.Single(behavior.Functions); - Assert.Equal("p1.f1", behavior.Functions.Single()); - } - - [Fact] - public void ItShouldDeserializeNoneFunctionChoiceBehavior() - { - // Arrange - var json = """ - { - "type": "none", - "functions": ["p1.f1"] - } - """; - - // Act - var behavior = JsonSerializer.Deserialize(json); - - // Assert - Assert.NotNull(behavior?.Functions); - Assert.Single(behavior.Functions); - Assert.Equal("p1.f1", behavior.Functions.Single()); + Assert.Equal("p1-f1", behavior.Functions.Single()); } } diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs index 84f3ff5052ef..ef00c269e8d6 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs @@ -33,11 +33,11 @@ public void ItShouldAdvertiseKernelFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(3, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] @@ -54,10 +54,10 @@ public void ItShouldAdvertiseFunctionsIfSpecified() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(2, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } private static KernelPlugin GetTestPlugin() diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs index 512379090bee..ece75a9e8e10 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs @@ -34,11 +34,11 @@ public void ItShouldAdvertiseAllKernelFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(3, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] @@ -56,10 +56,10 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructor() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(2, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] @@ -72,7 +72,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() // Act var choiceBehavior = new RequiredFunctionChoiceBehavior() { - Functions = ["MyPlugin.Function1", "MyPlugin.Function2"] + Functions = ["MyPlugin-Function1", "MyPlugin-Function2"] }; var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -80,10 +80,10 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(2, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] @@ -100,10 +100,10 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorForManualInvocat // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(2, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] @@ -121,11 +121,11 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(3, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] @@ -176,8 +176,8 @@ public void ItShouldInitializeFunctionPropertyByFunctionsPassedViaConstructor() Assert.NotNull(choiceBehavior.Functions); Assert.Equal(2, choiceBehavior.Functions.Count); - Assert.Equal("MyPlugin.Function1", choiceBehavior.Functions.ElementAt(0)); - Assert.Equal("MyPlugin.Function2", choiceBehavior.Functions.ElementAt(1)); + Assert.Equal("MyPlugin-Function1", choiceBehavior.Functions.ElementAt(0)); + Assert.Equal("MyPlugin-Function2", choiceBehavior.Functions.ElementAt(1)); } [Fact] @@ -212,7 +212,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegis choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("The specified function MyPlugin.Function1 is not available in the kernel.", exception.Message); + Assert.Equal("The specified function MyPlugin-Function1 is not available in the kernel.", exception.Message); } [Fact] @@ -224,7 +224,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste var choiceBehavior = new RequiredFunctionChoiceBehavior(false) { - Functions = ["MyPlugin.NonKernelFunction"] + Functions = ["MyPlugin-NonKernelFunction"] }; // Act @@ -233,7 +233,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("No instance of the specified function MyPlugin.NonKernelFunction is found.", exception.Message); + Assert.Equal("No instance of the specified function MyPlugin-NonKernelFunction is found.", exception.Message); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs index 69382383843d..196ca39d6495 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs @@ -63,11 +63,11 @@ public void AutoFunctionChoiceShouldAdvertiseKernelFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(3, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] @@ -85,10 +85,10 @@ public void AutoFunctionChoiceShouldAdvertiseProvidedFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(2, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] @@ -140,11 +140,11 @@ public void RequiredFunctionChoiceShouldAdvertiseKernelFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(3, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] @@ -162,10 +162,10 @@ public void RequiredFunctionChoiceShouldAdvertiseProvidedFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(2, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); } [Fact] @@ -216,10 +216,10 @@ public void NoneFunctionChoiceShouldAdvertiseProvidedFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(2, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(2, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } [Fact] @@ -237,11 +237,11 @@ public void NoneFunctionChoiceShouldAdvertiseAllKernelFunctions() // Assert Assert.NotNull(config); - Assert.NotNull(config.FunctionsMetadata); - Assert.Equal(3, config.FunctionsMetadata.Count()); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function1"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function2"); - Assert.Contains(config.FunctionsMetadata, f => f.Name == "Function3"); + Assert.NotNull(config.Functions); + Assert.Equal(3, config.Functions.Count()); + Assert.Contains(config.Functions, f => f.Name == "Function1"); + Assert.Contains(config.Functions, f => f.Name == "Function2"); + Assert.Contains(config.Functions, f => f.Name == "Function3"); } private static KernelPlugin GetTestPlugin() diff --git a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs index 0133b515d8cc..ef230e17a9e8 100644 --- a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs @@ -174,7 +174,7 @@ public void DeserializingAutoFunctionCallingChoice() Assert.NotNull(autoFunctionCallChoice); Assert.NotNull(autoFunctionCallChoice.Functions); - Assert.Equal("p1.f1", autoFunctionCallChoice.Functions.Single()); + Assert.Equal("p1-f1", autoFunctionCallChoice.Functions.Single()); } [Fact] @@ -210,7 +210,7 @@ public void DeserializingRequiredFunctionCallingChoice() Assert.NotNull(requiredFunctionCallChoice); Assert.NotNull(requiredFunctionCallChoice.Functions); - Assert.Equal("p1.f1", requiredFunctionCallChoice.Functions.Single()); + Assert.Equal("p1-f1", requiredFunctionCallChoice.Functions.Single()); } [Fact] From 9e433dd31f52e5d57978954b6b30bf821334ef76 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 11 Jun 2024 11:32:56 +0100 Subject: [PATCH 80/90] fix: restrict constraining extensibility of choice behavior-related classes and change the way the function name separator is handled --- .../OpenAI_FunctionCalling.cs | 2 +- .../Functions/KernelFunctionMarkdownTests.cs | 49 ++++++----- ...nctionChoiceBehaviorTypesConverterTests.cs | 87 ------------------- .../Yaml/Functions/KernelFunctionYamlTests.cs | 36 +++++--- ...omptExecutionSettingsTypeConverterTests.cs | 37 +++++--- .../FunctionChoiceBehaviorTypesConverter.cs | 84 ------------------ .../PromptExecutionSettingsTypeConverter.cs | 50 ++++++++--- .../Connectors/OpenAI/OpenAIToolsTests.cs | 2 +- .../src/Functions/FunctionName.cs | 6 +- .../AutoFunctionChoiceBehavior.cs | 14 +-- .../FunctionChoiceBehavior.cs | 9 +- .../FunctionNameFormatJsonConverter.cs | 64 -------------- .../NoneFunctionChoiceBehavior.cs | 14 +-- .../RequiredFunctionChoiceBehavior.cs | 14 +-- .../AutoFunctionChoiceBehaviorTests.cs | 12 +-- .../FunctionNameFormatJsonConverterTests.cs | 24 ++++- .../RequiredFunctionChoiceBehaviorTests.cs | 12 +-- .../PromptTemplateConfigTests.cs | 4 +- 18 files changed, 176 insertions(+), 344 deletions(-) delete mode 100644 dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs delete mode 100644 dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs delete mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs diff --git a/dotnet/samples/Concepts/AutoFunctionCalling/OpenAI_FunctionCalling.cs b/dotnet/samples/Concepts/AutoFunctionCalling/OpenAI_FunctionCalling.cs index bc985e885916..d2d7f6d84a50 100644 --- a/dotnet/samples/Concepts/AutoFunctionCalling/OpenAI_FunctionCalling.cs +++ b/dotnet/samples/Concepts/AutoFunctionCalling/OpenAI_FunctionCalling.cs @@ -139,7 +139,7 @@ public async Task RunAsync() } // Adding a simulated function call to the connector response message - var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); + var simulatedFunctionCall = new FunctionCallContent("weather_alert", id: "call_123"); result.Items.Add(simulatedFunctionCall); // Adding a simulated function result to chat history diff --git a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs index 363b28d2f2f9..221752578bf6 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Markdown/Functions/KernelFunctionMarkdownTests.cs @@ -30,6 +30,9 @@ public void ItShouldInitializeFunctionChoiceBehaviorsFromMarkdown() { // Arrange var kernel = new Kernel(); + kernel.Plugins.AddFromFunctions("p1", [KernelFunctionFactory.CreateFromMethod(() => { }, "f1")]); + kernel.Plugins.AddFromFunctions("p2", [KernelFunctionFactory.CreateFromMethod(() => { }, "f2")]); + kernel.Plugins.AddFromFunctions("p3", [KernelFunctionFactory.CreateFromMethod(() => { }, "f3")]); // Act var function = KernelFunctionMarkdown.CreateFromPromptMarkdown(Markdown, "TellMeAbout"); @@ -42,34 +45,36 @@ public void ItShouldInitializeFunctionChoiceBehaviorsFromMarkdown() // AutoFunctionCallChoice for service1 var service1ExecutionSettings = function.ExecutionSettings["service1"]; - Assert.NotNull(service1ExecutionSettings); + Assert.NotNull(service1ExecutionSettings?.FunctionChoiceBehavior); - var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; - Assert.NotNull(autoFunctionChoiceBehavior); - - Assert.NotNull(autoFunctionChoiceBehavior.Functions); - Assert.Single(autoFunctionChoiceBehavior.Functions); - Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.First()); + var autoConfig = service1ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(autoConfig); + Assert.Equal(FunctionChoice.Auto, autoConfig.Choice); + Assert.NotNull(autoConfig.Functions); + Assert.Equal("p1", autoConfig.Functions.Single().PluginName); + Assert.Equal("f1", autoConfig.Functions.Single().Name); // RequiredFunctionCallChoice for service2 var service2ExecutionSettings = function.ExecutionSettings["service2"]; - Assert.NotNull(service2ExecutionSettings); + Assert.NotNull(service2ExecutionSettings?.FunctionChoiceBehavior); - var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; - Assert.NotNull(requiredFunctionChoiceBehavior); - Assert.NotNull(requiredFunctionChoiceBehavior.Functions); - Assert.Single(requiredFunctionChoiceBehavior.Functions); - Assert.Equal("p1-f1", requiredFunctionChoiceBehavior.Functions.First()); + var requiredConfig = service2ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(requiredConfig); + Assert.Equal(FunctionChoice.Required, requiredConfig.Choice); + Assert.NotNull(requiredConfig.Functions); + Assert.Equal("p2", requiredConfig.Functions.Single().PluginName); + Assert.Equal("f2", requiredConfig.Functions.Single().Name); // NoneFunctionCallChoice for service3 var service3ExecutionSettings = function.ExecutionSettings["service3"]; - Assert.NotNull(service3ExecutionSettings); - - var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; - Assert.NotNull(noneFunctionChoiceBehavior); - Assert.NotNull(noneFunctionChoiceBehavior.Functions); - Assert.Single(noneFunctionChoiceBehavior.Functions); - Assert.Equal("p1-f1", noneFunctionChoiceBehavior.Functions.First()); + Assert.NotNull(service3ExecutionSettings?.FunctionChoiceBehavior); + + var noneConfig = service3ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(noneConfig); + Assert.Equal(FunctionChoice.None, noneConfig.Choice); + Assert.NotNull(noneConfig.Functions); + Assert.Equal("p3", noneConfig.Functions.Single().PluginName); + Assert.Equal("f3", noneConfig.Functions.Single().Name); } [Fact] @@ -112,7 +117,7 @@ These are more AI execution settings "temperature": 0.8, "function_choice_behavior": { "type": "required", - "functions": ["p1.f1"] + "functions": ["p2.f2"] } } } @@ -125,7 +130,7 @@ These are AI execution settings as well "temperature": 0.8, "function_choice_behavior": { "type": "none", - "functions": ["p1.f1"] + "functions": ["p3.f3"] } } } diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs deleted file mode 100644 index d84f56d03afa..000000000000 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/FunctionChoiceBehaviorTypesConverterTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Linq; -using Microsoft.SemanticKernel; -using Xunit; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace SemanticKernel.Functions.UnitTests.Yaml; - -/// -/// Tests for . -/// -public sealed class FunctionChoiceBehaviorTypesConverterTests -{ - [Fact] - public void ItShouldDeserializeAutoFunctionChoiceBehavior() - { - // Arrange - var deserializer = new DeserializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) - .Build(); - - var yaml = """ - type: auto - functions: - - p1.f1 - """; - - // Act - var behavior = deserializer.Deserialize(yaml); - - // Assert - Assert.NotNull(behavior.Functions); - Assert.Single(behavior.Functions); - Assert.Equal("p1-f1", behavior.Functions.Single()); - } - - [Fact] - public void ItShouldDeserializeRequiredFunctionChoiceBehavior() - { - // Arrange - var deserializer = new DeserializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) - .Build(); - - var yaml = """ - type: required - functions: - - p2.f2 - """; - - // Act - var behavior = deserializer.Deserialize(yaml); - - // Assert - Assert.NotNull(behavior.Functions); - Assert.Single(behavior.Functions); - Assert.Equal("p2-f2", behavior.Functions.Single()); - } - - [Fact] - public void ItShouldDeserializeNoneFunctionChoiceBehavior() - { - // Arrange - var deserializer = new DeserializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) - .Build(); - - var yaml = """ - type: none, - functions: - - p1.f1 - """; - - // Act - var behavior = deserializer.Deserialize(yaml); - - // Assert - Assert.NotNull(behavior.Functions); - Assert.Single(behavior.Functions); - Assert.Equal("p1-f1", behavior.Functions.Single()); - } -} diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs index 4db0022e2e87..d898822893ca 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/Functions/KernelFunctionYamlTests.cs @@ -89,31 +89,47 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() // Act var promptTemplateConfig = KernelFunctionYaml.ToPromptTemplateConfig(this._yaml); + var kernel = new Kernel(); + kernel.Plugins.AddFromFunctions("p1", [KernelFunctionFactory.CreateFromMethod(() => { }, "f1")]); + kernel.Plugins.AddFromFunctions("p2", [KernelFunctionFactory.CreateFromMethod(() => { }, "f2")]); + kernel.Plugins.AddFromFunctions("p3", [KernelFunctionFactory.CreateFromMethod(() => { }, "f3")]); + // Assert Assert.NotNull(promptTemplateConfig?.ExecutionSettings); Assert.Equal(3, promptTemplateConfig.ExecutionSettings.Count); // Service with auto function choice behavior var service1ExecutionSettings = promptTemplateConfig.ExecutionSettings["service1"]; + Assert.NotNull(service1ExecutionSettings?.FunctionChoiceBehavior); - var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; - Assert.NotNull(autoFunctionChoiceBehavior?.Functions); - Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.Single()); + var autoConfig = service1ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(autoConfig); + Assert.Equal(FunctionChoice.Auto, autoConfig.Choice); + Assert.NotNull(autoConfig.Functions); + Assert.Equal("p1", autoConfig.Functions.Single().PluginName); + Assert.Equal("f1", autoConfig.Functions.Single().Name); // Service with required function choice behavior var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; + Assert.NotNull(service2ExecutionSettings?.FunctionChoiceBehavior); - var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; - Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); - Assert.Equal("p2-f2", requiredFunctionChoiceBehavior.Functions.Single()); + var requiredConfig = service2ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(requiredConfig); + Assert.Equal(FunctionChoice.Required, requiredConfig.Choice); + Assert.NotNull(requiredConfig.Functions); + Assert.Equal("p2", requiredConfig.Functions.Single().PluginName); + Assert.Equal("f2", requiredConfig.Functions.Single().Name); // Service with none function choice behavior var service3ExecutionSettings = promptTemplateConfig.ExecutionSettings["service3"]; + Assert.NotNull(service3ExecutionSettings?.FunctionChoiceBehavior); - var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; - Assert.NotNull(noneFunctionChoiceBehavior); - Assert.NotNull(noneFunctionChoiceBehavior?.Functions); - Assert.Equal("p3-f3", noneFunctionChoiceBehavior.Functions.Single()); + var noneConfig = service3ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(noneConfig); + Assert.Equal(FunctionChoice.None, noneConfig.Choice); + Assert.NotNull(noneConfig.Functions); + Assert.Equal("p3", noneConfig.Functions.Single().PluginName); + Assert.Equal("f3", noneConfig.Functions.Single().Name); } [Fact] diff --git a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs index fe5717f3cd68..2ef79ef0e850 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/Yaml/PromptExecutionSettingsTypeConverterTests.cs @@ -40,6 +40,12 @@ public void ItShouldCreatePromptFunctionFromYamlWithCustomModelSettings() [Fact] public void ItShouldDeserializeFunctionChoiceBehaviors() { + // Arrange + var kernel = new Kernel(); + kernel.Plugins.AddFromFunctions("p1", [KernelFunctionFactory.CreateFromMethod(() => { }, "f1")]); + kernel.Plugins.AddFromFunctions("p2", [KernelFunctionFactory.CreateFromMethod(() => { }, "f2")]); + kernel.Plugins.AddFromFunctions("p3", [KernelFunctionFactory.CreateFromMethod(() => { }, "f3")]); + // Act var promptTemplateConfig = KernelFunctionYaml.ToPromptTemplateConfig(this._yaml); @@ -49,25 +55,36 @@ public void ItShouldDeserializeFunctionChoiceBehaviors() // Service with auto function choice behavior var service1ExecutionSettings = promptTemplateConfig.ExecutionSettings["service1"]; + Assert.NotNull(service1ExecutionSettings?.FunctionChoiceBehavior); - var autoFunctionChoiceBehavior = service1ExecutionSettings.FunctionChoiceBehavior as AutoFunctionChoiceBehavior; - Assert.NotNull(autoFunctionChoiceBehavior?.Functions); - Assert.Equal("p1-f1", autoFunctionChoiceBehavior.Functions.Single()); + var autoConfig = service1ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(autoConfig); + Assert.Equal(FunctionChoice.Auto, autoConfig.Choice); + Assert.NotNull(autoConfig.Functions); + Assert.Equal("p1", autoConfig.Functions.Single().PluginName); + Assert.Equal("f1", autoConfig.Functions.Single().Name); // Service with required function choice behavior var service2ExecutionSettings = promptTemplateConfig.ExecutionSettings["service2"]; + Assert.NotNull(service2ExecutionSettings?.FunctionChoiceBehavior); - var requiredFunctionChoiceBehavior = service2ExecutionSettings.FunctionChoiceBehavior as RequiredFunctionChoiceBehavior; - Assert.NotNull(requiredFunctionChoiceBehavior?.Functions); - Assert.Equal("p2-f2", requiredFunctionChoiceBehavior.Functions.Single()); + var requiredConfig = service2ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(requiredConfig); + Assert.Equal(FunctionChoice.Required, requiredConfig.Choice); + Assert.NotNull(requiredConfig.Functions); + Assert.Equal("p2", requiredConfig.Functions.Single().PluginName); + Assert.Equal("f2", requiredConfig.Functions.Single().Name); // Service with none function choice behavior var service3ExecutionSettings = promptTemplateConfig.ExecutionSettings["service3"]; + Assert.NotNull(service3ExecutionSettings?.FunctionChoiceBehavior); - var noneFunctionChoiceBehavior = service3ExecutionSettings.FunctionChoiceBehavior as NoneFunctionChoiceBehavior; - Assert.NotNull(noneFunctionChoiceBehavior); - Assert.NotNull(noneFunctionChoiceBehavior?.Functions); - Assert.Equal("p3-f3", noneFunctionChoiceBehavior.Functions.Single()); + var noneConfig = service3ExecutionSettings.FunctionChoiceBehavior.GetConfiguration(new FunctionChoiceBehaviorContext() { Kernel = kernel }); + Assert.NotNull(noneConfig); + Assert.Equal(FunctionChoice.None, noneConfig.Choice); + Assert.NotNull(noneConfig.Functions); + Assert.Equal("p3", noneConfig.Functions.Single().PluginName); + Assert.Equal("f3", noneConfig.Functions.Single().Name); } private readonly string _yaml = """ diff --git a/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs b/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs deleted file mode 100644 index 8f5b0f42be6d..000000000000 --- a/dotnet/src/Functions/Functions.Yaml/FunctionChoiceBehaviorTypesConverter.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using YamlDotNet.Core; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace Microsoft.SemanticKernel; - -/// -/// Allows custom deserialization for derivatives of . -/// -internal sealed class FunctionChoiceBehaviorTypesConverter : IYamlTypeConverter -{ - private const char PromptFunctionNameSeparator = '.'; - - private const char FunctionNameSeparator = '-'; - - private static IDeserializer? s_deserializer; - - /// - public bool Accepts(Type type) - { -#pragma warning disable SKEXP0001 - return - type == typeof(AutoFunctionChoiceBehavior) || - type == typeof(RequiredFunctionChoiceBehavior) || - type == typeof(NoneFunctionChoiceBehavior); -#pragma warning restore SKEXP0001 - } - - public object? ReadYaml(IParser parser, Type type) - { - s_deserializer ??= new DeserializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination. Otherwise, the "Property 'type' not found on type '{type.FullName}'" exception is thrown. - .Build(); - -#pragma warning disable SKEXP0001 - if (type == typeof(AutoFunctionChoiceBehavior)) - { - var behavior = s_deserializer.Deserialize(parser); - behavior.Functions = ConvertFunctionNames(behavior.Functions); - return behavior; - } - else if (type == typeof(RequiredFunctionChoiceBehavior)) - { - var behavior = s_deserializer.Deserialize(parser); - behavior.Functions = ConvertFunctionNames(behavior.Functions); - return behavior; - } - else if (type == typeof(NoneFunctionChoiceBehavior)) - { - var behavior = s_deserializer.Deserialize(parser); - behavior.Functions = ConvertFunctionNames(behavior.Functions); - return behavior; - } - - throw new YamlException($"Unexpected type '{type.FullName}' for function choice behavior."); -#pragma warning restore SKEXP0001 - } - - /// - public void WriteYaml(IEmitter emitter, object? value, Type type) - { - throw new NotImplementedException(); - } - - private static IList? ConvertFunctionNames(IList? functions) - { - if (functions is null) - { - return functions; - } - - return functions.Select(fqn => - { - var functionName = fqn ?? throw new YamlException("Expected a non-null YAML string."); - return functionName.Replace(PromptFunctionNameSeparator, FunctionNameSeparator); - }).ToList(); - } -} diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs index b4c5d8569f17..8edf6bf31e10 100644 --- a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs @@ -2,9 +2,12 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Linq; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; +using YamlDotNet.Serialization.BufferedDeserialization; using YamlDotNet.Serialization.NamingConventions; namespace Microsoft.SemanticKernel; @@ -27,18 +30,8 @@ public bool Accepts(Type type) { s_deserializer ??= new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithTypeConverter(new FunctionChoiceBehaviorTypesConverter()) - .WithTypeDiscriminatingNodeDeserializer((options) => - { -#pragma warning disable SKEXP0001 - options.AddKeyValueTypeDiscriminator("type", new Dictionary - { - { AutoFunctionChoiceBehavior.TypeDiscriminator, typeof(AutoFunctionChoiceBehavior) }, - { RequiredFunctionChoiceBehavior.TypeDiscriminator, typeof(RequiredFunctionChoiceBehavior) }, - { NoneFunctionChoiceBehavior.TypeDiscriminator, typeof(NoneFunctionChoiceBehavior) } - }); -#pragma warning restore SKEXP0010 - }) + .IgnoreUnmatchedProperties() // Required to ignore the 'type' property used as type discrimination. Otherwise, the "Property 'type' not found on type '{type.FullName}'" exception is thrown. + .WithTypeDiscriminatingNodeDeserializer(ConfigureTypeDiscriminatingNodeDeserializer) .Build(); parser.MoveNext(); // Move to the first property @@ -53,7 +46,9 @@ public bool Accepts(Type type) executionSettings.ModelId = s_deserializer.Deserialize(parser); break; case "function_choice_behavior": +#pragma warning disable SKEXP0001 executionSettings.FunctionChoiceBehavior = s_deserializer.Deserialize(parser); +#pragma warning restore SKEXP0010 break; default: (executionSettings.ExtensionData ??= new Dictionary()).Add(propertyName, s_deserializer.Deserialize(parser)); @@ -69,4 +64,35 @@ public void WriteYaml(IEmitter emitter, object? value, Type type) { throw new NotImplementedException(); } + + private static void ConfigureTypeDiscriminatingNodeDeserializer(ITypeDiscriminatingNodeDeserializerOptions options) + { +#pragma warning disable SKEXP0001 + var attributes = typeof(FunctionChoiceBehavior).GetCustomAttributes(false); + + // Getting the type discriminator property name - "type" from the JsonPolymorphicAttribute. + var discriminatorKey = attributes.OfType().Single().TypeDiscriminatorPropertyName; + if (string.IsNullOrEmpty(discriminatorKey)) + { + throw new InvalidOperationException("Type discriminator property name is not specified."); + } + + var discriminatorTypeMapping = new Dictionary(); + + // Getting FunctionChoiceBehavior subtypes and their type discriminators registered for polymorphic deserialization. + var derivedTypeAttributes = attributes.OfType(); + foreach (var derivedTypeAttribute in derivedTypeAttributes) + { + var discriminator = derivedTypeAttribute.TypeDiscriminator?.ToString(); + if (string.IsNullOrEmpty(discriminator)) + { + throw new InvalidOperationException($"Type discriminator is not specified for the {derivedTypeAttribute.DerivedType} type."); + } + + discriminatorTypeMapping.Add(discriminator!, derivedTypeAttribute.DerivedType); + } + + options.AddKeyValueTypeDiscriminator(discriminatorKey!, discriminatorTypeMapping); +#pragma warning restore SKEXP0010 + } } diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index 1512530fe681..eeeb7baf20db 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -357,7 +357,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu } // Adding a simulated function call to the connector response message - var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); + var simulatedFunctionCall = new FunctionCallContent("weather_alert", id: "call_123"); messageContent.Items.Add(simulatedFunctionCall); // Adding a simulated function result to chat history diff --git a/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs b/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs index 76f54de92a56..02d7e67f56d9 100644 --- a/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs +++ b/dotnet/src/InternalUtilities/src/Functions/FunctionName.cs @@ -28,7 +28,8 @@ internal sealed class FunctionName /// The plugin name. public FunctionName(string name, string? pluginName = null) { - Verify.NotNull(name); + Verify.ValidFunctionName(name); + if (pluginName is not null) { Verify.ValidPluginName(pluginName); } this.Name = name; this.PluginName = pluginName; @@ -43,6 +44,9 @@ public FunctionName(string name, string? pluginName = null) /// Fully-qualified name of the function. public static string ToFullyQualifiedName(string functionName, string? pluginName = null, string functionNameSeparator = "-") { + Verify.ValidFunctionName(functionName); + if (pluginName is not null) { Verify.ValidPluginName(pluginName); } + return string.IsNullOrEmpty(pluginName) ? functionName : $"{pluginName}{functionNameSeparator}{functionName}"; } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index e9fe65991461..9f0a31c7a90e 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -11,8 +10,7 @@ namespace Microsoft.SemanticKernel; /// Represents that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior allows the model to decide whether to call the functions and, if so, which ones to call. /// -[Experimental("SKEXP0001")] -public sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior +internal sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior { /// /// List of the functions that the model can choose from. @@ -24,11 +22,6 @@ public sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior /// private readonly bool _autoInvoke = true; - /// - /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. - /// - public const string TypeDiscriminator = "auto"; - /// /// Initializes a new instance of the class. /// @@ -47,7 +40,7 @@ public AutoFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); + this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); } /// @@ -55,7 +48,6 @@ public AutoFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable's plugins' functions are provided to the model. /// [JsonPropertyName("functions")] - [JsonConverter(typeof(FunctionNameFormatJsonConverter))] public IList? Functions { get; set; } /// @@ -81,7 +73,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var functionFQN in functionFQNs) { - var nameParts = FunctionName.Parse(functionFQN); + var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); // Check if the function is available in the kernel. If it is, then connectors can find it for auto-invocation later. if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index 19e789beb61d..6cba385bfbeb 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -10,12 +10,15 @@ namespace Microsoft.SemanticKernel; /// Represents the base class for different function choice behaviors. /// [JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] -[JsonDerivedType(typeof(AutoFunctionChoiceBehavior), typeDiscriminator: AutoFunctionChoiceBehavior.TypeDiscriminator)] -[JsonDerivedType(typeof(RequiredFunctionChoiceBehavior), typeDiscriminator: RequiredFunctionChoiceBehavior.TypeDiscriminator)] -[JsonDerivedType(typeof(NoneFunctionChoiceBehavior), typeDiscriminator: NoneFunctionChoiceBehavior.TypeDiscriminator)] +[JsonDerivedType(typeof(AutoFunctionChoiceBehavior), typeDiscriminator: "auto")] +[JsonDerivedType(typeof(RequiredFunctionChoiceBehavior), typeDiscriminator: "required")] +[JsonDerivedType(typeof(NoneFunctionChoiceBehavior), typeDiscriminator: "none")] [Experimental("SKEXP0001")] public abstract class FunctionChoiceBehavior { + /// The separator used to separate plugin name and function name. + protected const string FunctionNameSeparator = "."; + /// /// Creates a new instance of the class. /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs deleted file mode 100644 index b828fa2f2c9a..000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverter.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel; - -/// -/// A custom JSON converter for converting function names in a JSON array. -/// This converter replaces dots used as a function name separator in prompts with hyphens when reading and back when writing. -/// -[Experimental("SKEXP0001")] -public sealed class FunctionNameFormatJsonConverter : JsonConverter> -{ - private const char PromptFunctionNameSeparator = '.'; - - private const char FunctionNameSeparator = '-'; - - /// - public override IList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartArray) - { - throw new JsonException("Expected a JSON array."); - } - - var functionNames = new List(); - - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndArray) - { - break; - } - - if (reader.TokenType != JsonTokenType.String) - { - throw new JsonException("Expected a JSON string."); - } - - var functionName = reader.GetString() ?? throw new JsonException("Expected a non-null JSON string."); - - functionNames.Add(functionName.Replace(PromptFunctionNameSeparator, FunctionNameSeparator)); - } - - return functionNames; - } - - /// - public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) - { - writer.WriteStartArray(); - - foreach (string functionName in value) - { - writer.WriteStringValue(functionName.Replace(FunctionNameSeparator, PromptFunctionNameSeparator)); - } - - writer.WriteEndArray(); - } -} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs index 88e850aa99c9..5f88cff67752 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -15,19 +14,13 @@ namespace Microsoft.SemanticKernel; /// Although this behavior prevents the model from calling any functions, the model can use the provided function information /// to describe how it would complete the prompt if it had the ability to call the functions. /// -[Experimental("SKEXP0001")] -public sealed class NoneFunctionChoiceBehavior : FunctionChoiceBehavior +internal sealed class NoneFunctionChoiceBehavior : FunctionChoiceBehavior { /// /// List of the functions that the model can choose from. /// private readonly IEnumerable? _functions; - /// - /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. - /// - public const string TypeDiscriminator = "none"; - /// /// Initializes a new instance of the class. /// @@ -44,7 +37,7 @@ public NoneFunctionChoiceBehavior() public NoneFunctionChoiceBehavior(IEnumerable functions) { this._functions = functions; - this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); + this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); } /// @@ -52,7 +45,6 @@ public NoneFunctionChoiceBehavior(IEnumerable functions) /// If null or empty, all 's plugins' functions are provided to the model. /// [JsonPropertyName("functions")] - [JsonConverter(typeof(FunctionNameFormatJsonConverter))] public IList? Functions { get; set; } /// @@ -67,7 +59,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var functionFQN in functionFQNs) { - var nameParts = FunctionName.Parse(functionFQN); + var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); // Check if the function is available in the kernel. if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index c417a14393d5..612039ba91f7 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -11,8 +10,7 @@ namespace Microsoft.SemanticKernel; /// Represents that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. /// -[Experimental("SKEXP0001")] -public sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior +internal sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior { /// /// List of the functions that the model can choose from. @@ -24,11 +22,6 @@ public sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior /// private readonly bool _autoInvoke = true; - /// - /// This class type discriminator used for polymorphic deserialization of the type specified in JSON and YAML prompts. - /// - public const string TypeDiscriminator = "required"; - /// /// Initializes a new instance of the class. /// @@ -47,7 +40,7 @@ public RequiredFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable FunctionName.ToFullyQualifiedName(f.Name, f.PluginName)).ToList(); + this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); } /// @@ -55,7 +48,6 @@ public RequiredFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable's plugins' functions are provided to the model. /// [JsonPropertyName("functions")] - [JsonConverter(typeof(FunctionNameFormatJsonConverter))] public IList? Functions { get; set; } /// @@ -81,7 +73,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho foreach (var functionFQN in functionFQNs) { - var nameParts = FunctionName.Parse(functionFQN); + var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); // Check if the function is available in the kernel. If it is, then connectors can find it for auto-invocation later. if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs index bbcb3fb808f5..2a9221b77cef 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs @@ -72,7 +72,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() // Act var choiceBehavior = new AutoFunctionChoiceBehavior() { - Functions = ["MyPlugin-Function1", "MyPlugin-Function2"] + Functions = ["MyPlugin.Function1", "MyPlugin.Function2"] }; var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -176,8 +176,8 @@ public void ItShouldInitializeFunctionPropertyByFunctionsPassedViaConstructor() Assert.NotNull(choiceBehavior.Functions); Assert.Equal(2, choiceBehavior.Functions.Count); - Assert.Equal("MyPlugin-Function1", choiceBehavior.Functions.ElementAt(0)); - Assert.Equal("MyPlugin-Function2", choiceBehavior.Functions.ElementAt(1)); + Assert.Equal("MyPlugin.Function1", choiceBehavior.Functions.ElementAt(0)); + Assert.Equal("MyPlugin.Function2", choiceBehavior.Functions.ElementAt(1)); } [Fact] @@ -212,7 +212,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegis choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("The specified function MyPlugin-Function1 is not available in the kernel.", exception.Message); + Assert.Equal("The specified function MyPlugin.Function1 is not available in the kernel.", exception.Message); } [Fact] @@ -224,7 +224,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste var choiceBehavior = new AutoFunctionChoiceBehavior(autoInvoke: false) { - Functions = ["MyPlugin-NonKernelFunction"] + Functions = ["MyPlugin.NonKernelFunction"] }; // Act @@ -233,7 +233,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("No instance of the specified function MyPlugin-NonKernelFunction is found.", exception.Message); + Assert.Equal("No instance of the specified function MyPlugin.NonKernelFunction is found.", exception.Message); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs index 959a76bbf328..6d7f0e1ec13c 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs @@ -25,7 +25,7 @@ public void ItShouldDeserializeAutoFunctionChoiceBehavior() // Assert Assert.NotNull(behavior?.Functions); Assert.Single(behavior.Functions); - Assert.Equal("p1-f1", behavior.Functions.Single()); + Assert.Equal("p1.f1", behavior.Functions.Single()); } [Fact] @@ -45,6 +45,26 @@ public void ItShouldDeserializeRequiredFunctionChoiceBehavior() // Assert Assert.NotNull(behavior?.Functions); Assert.Single(behavior.Functions); - Assert.Equal("p1-f1", behavior.Functions.Single()); + Assert.Equal("p1.f1", behavior.Functions.Single()); + } + + [Fact] + public void ItShouldDeserializeNoneFunctionChoiceBehavior() + { + // Arrange + var json = """ + { + "type": "none", + "functions": ["p1.f1"] + } + """; + + // Act + var behavior = JsonSerializer.Deserialize(json); + + // Assert + Assert.NotNull(behavior?.Functions); + Assert.Single(behavior.Functions); + Assert.Equal("p1.f1", behavior.Functions.Single()); } } diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs index ece75a9e8e10..04c1ecaab7e2 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs @@ -72,7 +72,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() // Act var choiceBehavior = new RequiredFunctionChoiceBehavior() { - Functions = ["MyPlugin-Function1", "MyPlugin-Function2"] + Functions = ["MyPlugin.Function1", "MyPlugin.Function2"] }; var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -176,8 +176,8 @@ public void ItShouldInitializeFunctionPropertyByFunctionsPassedViaConstructor() Assert.NotNull(choiceBehavior.Functions); Assert.Equal(2, choiceBehavior.Functions.Count); - Assert.Equal("MyPlugin-Function1", choiceBehavior.Functions.ElementAt(0)); - Assert.Equal("MyPlugin-Function2", choiceBehavior.Functions.ElementAt(1)); + Assert.Equal("MyPlugin.Function1", choiceBehavior.Functions.ElementAt(0)); + Assert.Equal("MyPlugin.Function2", choiceBehavior.Functions.ElementAt(1)); } [Fact] @@ -212,7 +212,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegis choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("The specified function MyPlugin-Function1 is not available in the kernel.", exception.Message); + Assert.Equal("The specified function MyPlugin.Function1 is not available in the kernel.", exception.Message); } [Fact] @@ -224,7 +224,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste var choiceBehavior = new RequiredFunctionChoiceBehavior(false) { - Functions = ["MyPlugin-NonKernelFunction"] + Functions = ["MyPlugin.NonKernelFunction"] }; // Act @@ -233,7 +233,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("No instance of the specified function MyPlugin-NonKernelFunction is found.", exception.Message); + Assert.Equal("No instance of the specified function MyPlugin.NonKernelFunction is found.", exception.Message); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs index ef230e17a9e8..0133b515d8cc 100644 --- a/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/PromptTemplate/PromptTemplateConfigTests.cs @@ -174,7 +174,7 @@ public void DeserializingAutoFunctionCallingChoice() Assert.NotNull(autoFunctionCallChoice); Assert.NotNull(autoFunctionCallChoice.Functions); - Assert.Equal("p1-f1", autoFunctionCallChoice.Functions.Single()); + Assert.Equal("p1.f1", autoFunctionCallChoice.Functions.Single()); } [Fact] @@ -210,7 +210,7 @@ public void DeserializingRequiredFunctionCallingChoice() Assert.NotNull(requiredFunctionCallChoice); Assert.NotNull(requiredFunctionCallChoice.Functions); - Assert.Equal("p1-f1", requiredFunctionCallChoice.Functions.Single()); + Assert.Equal("p1.f1", requiredFunctionCallChoice.Functions.Single()); } [Fact] From 058895a8813fc5ccfa523ade9e5eb845d50f26fc Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 11 Jun 2024 14:30:27 +0100 Subject: [PATCH 81/90] fix: fix compilation warnings --- .../Functions.Yaml/PromptExecutionSettingsTypeConverter.cs | 2 +- .../CrossLanguage/PromptWithComplexObjectsTest.cs | 2 +- .../CrossLanguage/PromptWithHelperFunctionsTest.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs index 8edf6bf31e10..3f128806c145 100644 --- a/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs +++ b/dotnet/src/Functions/Functions.Yaml/PromptExecutionSettingsTypeConverter.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; -using System.Text.Json.Serialization; using System.Linq; +using System.Text.Json.Serialization; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; diff --git a/dotnet/src/IntegrationTests/CrossLanguage/PromptWithComplexObjectsTest.cs b/dotnet/src/IntegrationTests/CrossLanguage/PromptWithComplexObjectsTest.cs index cae56a022f7b..87fb3e1c888d 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/PromptWithComplexObjectsTest.cs +++ b/dotnet/src/IntegrationTests/CrossLanguage/PromptWithComplexObjectsTest.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel; using System.IO; using System.Text.Json.Nodes; using System.Threading.Tasks; +using Microsoft.SemanticKernel; using Xunit; namespace SemanticKernel.IntegrationTests.CrossLanguage; diff --git a/dotnet/src/IntegrationTests/CrossLanguage/PromptWithHelperFunctionsTest.cs b/dotnet/src/IntegrationTests/CrossLanguage/PromptWithHelperFunctionsTest.cs index 9fad909d790a..12d7166e0bb5 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/PromptWithHelperFunctionsTest.cs +++ b/dotnet/src/IntegrationTests/CrossLanguage/PromptWithHelperFunctionsTest.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel; using System; using System.IO; using System.Text.Json.Nodes; using System.Threading.Tasks; +using Microsoft.SemanticKernel; using Xunit; namespace SemanticKernel.IntegrationTests.CrossLanguage; From a0de34514b145d6f12f3bf7c85861c16a91ce88a Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 11 Jun 2024 17:31:29 +0100 Subject: [PATCH 82/90] fix: address PR comments --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 26 +++++++++---------- .../AutoFunctionChoiceBehavior.cs | 2 +- .../FunctionChoiceBehaviors/FunctionChoice.cs | 4 +-- .../FunctionChoiceBehaviorConfiguration.cs | 2 +- .../FunctionChoiceBehaviorContext.cs | 2 +- .../NoneFunctionChoiceBehavior.cs | 2 +- .../RequiredFunctionChoiceBehavior.cs | 2 +- .../AI/PromptExecutionSettings.cs | 4 +-- .../AutoFunctionChoiceBehaviorTests.cs | 12 ++++----- ...tionChoiceBehaviorDeserializationTests.cs} | 2 +- .../NoneFunctionChoiceBehaviorTests.cs | 4 +-- .../RequiredFunctionChoiceBehaviorTests.cs | 12 ++++----- .../Functions/FunctionChoiceBehaviorTests.cs | 12 ++++----- 13 files changed, 43 insertions(+), 43 deletions(-) rename dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/{FunctionNameFormatJsonConverterTests.cs => FunctionChoiceBehaviorDeserializationTests.cs} (96%) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 477fcfc00437..7640d9d3e2ae 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -409,7 +409,7 @@ internal async Task> GetChatMessageContentsAsy // Create the Azure SDK ChatCompletionOptions instance from all available information. var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); - var functionCallConfiguration = this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, 0); + var functionCallConfiguration = this.ConfigureFunctionCalling(requestIndex: 0, kernel, chatExecutionSettings, chatOptions); bool autoInvoke = kernel is not null && functionCallConfiguration?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); @@ -614,7 +614,7 @@ static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory c } // Update tool use information for the next go-around based on having completed another iteration. - functionCallConfiguration = this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, requestIndex); + functionCallConfiguration = this.ConfigureFunctionCalling(requestIndex, kernel, chatExecutionSettings, chatOptions); // Disable auto invocation if we've exceeded the allowed limit. if (requestIndex >= functionCallConfiguration?.MaximumAutoInvokeAttempts) @@ -641,7 +641,7 @@ internal async IAsyncEnumerable GetStreamingC var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); - var functionCallConfiguration = this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, 0); + var functionCallConfiguration = this.ConfigureFunctionCalling(requestIndex: 0, kernel, chatExecutionSettings, chatOptions); bool autoInvoke = kernel is not null && functionCallConfiguration?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); @@ -893,7 +893,7 @@ static void AddResponseMessage( } // Update tool use information for the next go-around based on having completed another iteration. - functionCallConfiguration = this.ConfigureFunctionCalling(kernel, chatExecutionSettings, chatOptions, requestIndex); + functionCallConfiguration = this.ConfigureFunctionCalling(requestIndex, kernel, chatExecutionSettings, chatOptions); // Disable auto invocation if we've exceeded the allowed limit. if (requestIndex >= functionCallConfiguration?.MaximumAutoInvokeAttempts) @@ -1538,11 +1538,11 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context /// /// Configures the function calling functionality based on the provided parameters. /// + /// Request sequence index of automatic function invocation process. /// The to be used for function calling. /// Execution settings for the completion API. /// The chat completion options from the Azure.AI.OpenAI package. - /// Request sequence index of automatic function invocation process. - private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(Kernel? kernel, OpenAIPromptExecutionSettings executionSettings, ChatCompletionsOptions chatOptions, int requestIndex) + private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(int requestIndex, Kernel? kernel, OpenAIPromptExecutionSettings executionSettings, ChatCompletionsOptions chatOptions) { (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? result = null; @@ -1565,12 +1565,12 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context // Handling new tool behavior represented by `PromptExecutionSettings.FunctionChoiceBehavior` property. if (executionSettings.FunctionChoiceBehavior is { } functionChoiceBehavior) { - result = this.ConfigureFunctionCalling(kernel, chatOptions, requestIndex, functionChoiceBehavior); + result = this.ConfigureFunctionCalling(requestIndex, kernel, chatOptions, functionChoiceBehavior); } // Handling old-style tool call behavior represented by `OpenAIPromptExecutionSettings.ToolCallBehavior` property. else if (executionSettings.ToolCallBehavior is { } toolCallBehavior) { - result = this.ConfigureFunctionCalling(kernel, chatOptions, requestIndex, toolCallBehavior); + result = this.ConfigureFunctionCalling(requestIndex, kernel, chatOptions, toolCallBehavior); } // Having already sent tools and with tool call information in history, the service can become unhappy "Invalid 'tools': empty array. Expected an array with minimum length 1, but got an empty array instead." @@ -1585,7 +1585,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context return result; } - private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(Kernel? kernel, ChatCompletionsOptions chatOptions, int requestIndex, FunctionChoiceBehavior functionChoiceBehavior) + private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(int requestIndex, Kernel? kernel, ChatCompletionsOptions chatOptions, FunctionChoiceBehavior functionChoiceBehavior) { // Regenerate the tool list as necessary and getting other call behavior properties. The invocation of the function(s) could have augmented // what functions are available in the kernel. @@ -1626,14 +1626,14 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context if (config.Choice == FunctionChoice.Required) { - if (config.Functions is { } functions && functions.Any()) + if (config.Functions is { Count: > 0 } functions) { - if (functions.Count() > 1) + if (functions.Count > 1) { throw new KernelException("Only one required function is allowed."); } - var functionDefinition = functions.First().Metadata.ToOpenAIFunction().ToFunctionDefinition(); + var functionDefinition = functions[0].Metadata.ToOpenAIFunction().ToFunctionDefinition(); chatOptions.ToolChoice = new ChatCompletionsToolChoice(functionDefinition); chatOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition(functionDefinition)); @@ -1661,7 +1661,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context throw new NotSupportedException($"Unsupported function choice '{config.Choice}'."); } - private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(Kernel? kernel, ChatCompletionsOptions chatOptions, int requestIndex, ToolCallBehavior toolCallBehavior) + private (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts)? ConfigureFunctionCalling(int requestIndex, Kernel? kernel, ChatCompletionsOptions chatOptions, ToolCallBehavior toolCallBehavior) { if (requestIndex >= toolCallBehavior.MaximumUseAttempts) { diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index 9f0a31c7a90e..71f40a26a2d3 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -96,7 +96,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho continue; } - throw new KernelException($"No instance of the specified function {functionFQN} is found."); + throw new KernelException($"The specified function {functionFQN} was not found."); } } // Provide all functions from the kernel. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs index 51bc301f23ad..14daa4b303c5 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs @@ -72,8 +72,8 @@ public bool Equals(FunctionChoice other) /// public override int GetHashCode() - => StringComparer.OrdinalIgnoreCase.GetHashCode(this.Label ?? string.Empty); + => StringComparer.OrdinalIgnoreCase.GetHashCode(this.Label); /// - public override string ToString() => this.Label ?? string.Empty; + public override string ToString() => this.Label; } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index 228281a3945a..84571e7fd72c 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -26,7 +26,7 @@ internal FunctionChoiceBehaviorConfiguration() /// /// The functions available for AI model. /// - public IEnumerable? Functions { get; internal set; } + public IReadOnlyList? Functions { get; internal set; } /// /// Indicates whether the functions should be automatically invoked by the AI service/connector. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs index db6bcb10b944..9efc41975aa1 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel; /// The context to be provided by the choice behavior consumer in order to obtain the choice behavior configuration. /// [Experimental("SKEXP0001")] -public class FunctionChoiceBehaviorContext +public sealed class FunctionChoiceBehaviorContext { /// /// The to be used for function calling. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs index 5f88cff67752..074e35b2d3de 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -76,7 +76,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho continue; } - throw new KernelException($"The specified function {functionFQN} is not available."); + throw new KernelException($"The specified function {functionFQN} was not found."); } } // Provide all functions from the kernel. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index 612039ba91f7..8100a231e414 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -96,7 +96,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho continue; } - throw new KernelException($"No instance of the specified function {functionFQN} is found."); + throw new KernelException($"The specified function {functionFQN} was not found."); } } // Provide all functions from the kernel. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs index 2e5bd8c69a98..4bc9bef203a1 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs @@ -135,8 +135,8 @@ public virtual PromptExecutionSettings Clone() return new() { ModelId = this.ModelId, - ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, - FunctionChoiceBehavior = this.FunctionChoiceBehavior + FunctionChoiceBehavior = this.FunctionChoiceBehavior, + ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null }; } diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs index 2a9221b77cef..84e0727e22c2 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs @@ -35,7 +35,7 @@ public void ItShouldAdvertiseAllKernelFunctions() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); + Assert.Equal(3, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); Assert.Contains(config.Functions, f => f.Name == "Function3"); @@ -57,7 +57,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructor() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); + Assert.Equal(2, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); } @@ -81,7 +81,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); + Assert.Equal(2, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); } @@ -101,7 +101,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorForManualInvocat Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); + Assert.Equal(2, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); } @@ -122,7 +122,7 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); + Assert.Equal(3, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); Assert.Contains(config.Functions, f => f.Name == "Function3"); @@ -233,7 +233,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("No instance of the specified function MyPlugin.NonKernelFunction is found.", exception.Message); + Assert.Equal("The specified function MyPlugin.NonKernelFunction was not found.", exception.Message); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorDeserializationTests.cs similarity index 96% rename from dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs rename to dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorDeserializationTests.cs index 6d7f0e1ec13c..ce3ba01e15c0 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionNameFormatJsonConverterTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorDeserializationTests.cs @@ -6,7 +6,7 @@ using Xunit; namespace SemanticKernel.UnitTests.AI.FunctionChoiceBehaviors; -public class FunctionNameFormatJsonConverterTests +public class FunctionChoiceBehaviorDeserializationTests { [Fact] public void ItShouldDeserializeAutoFunctionChoiceBehavior() diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs index ef00c269e8d6..cc30e869b8b0 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehaviorTests.cs @@ -34,7 +34,7 @@ public void ItShouldAdvertiseKernelFunctions() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); + Assert.Equal(3, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); Assert.Contains(config.Functions, f => f.Name == "Function3"); @@ -55,7 +55,7 @@ public void ItShouldAdvertiseFunctionsIfSpecified() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); + Assert.Equal(2, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function3"); } diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs index 04c1ecaab7e2..1241b3d02b21 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs @@ -35,7 +35,7 @@ public void ItShouldAdvertiseAllKernelFunctions() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); + Assert.Equal(3, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); Assert.Contains(config.Functions, f => f.Name == "Function3"); @@ -57,7 +57,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructor() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); + Assert.Equal(2, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); } @@ -81,7 +81,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedInFunctionsProperty() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); + Assert.Equal(2, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); } @@ -101,7 +101,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorForManualInvocat Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); + Assert.Equal(2, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); } @@ -122,7 +122,7 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); + Assert.Equal(3, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); Assert.Contains(config.Functions, f => f.Name == "Function3"); @@ -233,7 +233,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); }); - Assert.Equal("No instance of the specified function MyPlugin.NonKernelFunction is found.", exception.Message); + Assert.Equal("The specified function MyPlugin.NonKernelFunction was not found.", exception.Message); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs index 196ca39d6495..87e0cdc6b135 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs @@ -64,7 +64,7 @@ public void AutoFunctionChoiceShouldAdvertiseKernelFunctions() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); + Assert.Equal(3, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); Assert.Contains(config.Functions, f => f.Name == "Function3"); @@ -86,7 +86,7 @@ public void AutoFunctionChoiceShouldAdvertiseProvidedFunctions() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); + Assert.Equal(2, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); } @@ -141,7 +141,7 @@ public void RequiredFunctionChoiceShouldAdvertiseKernelFunctions() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); + Assert.Equal(3, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); Assert.Contains(config.Functions, f => f.Name == "Function3"); @@ -163,7 +163,7 @@ public void RequiredFunctionChoiceShouldAdvertiseProvidedFunctions() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); + Assert.Equal(2, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); } @@ -217,7 +217,7 @@ public void NoneFunctionChoiceShouldAdvertiseProvidedFunctions() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(2, config.Functions.Count()); + Assert.Equal(2, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function3"); } @@ -238,7 +238,7 @@ public void NoneFunctionChoiceShouldAdvertiseAllKernelFunctions() Assert.NotNull(config); Assert.NotNull(config.Functions); - Assert.Equal(3, config.Functions.Count()); + Assert.Equal(3, config.Functions.Count); Assert.Contains(config.Functions, f => f.Name == "Function1"); Assert.Contains(config.Functions, f => f.Name == "Function2"); Assert.Contains(config.Functions, f => f.Name == "Function3"); From abed585deae251b3d636e894b5d148f4af9e4588 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 11 Jun 2024 18:11:57 +0100 Subject: [PATCH 83/90] fix: IDE0005 formatting issue --- .../RequiredFunctionChoiceBehaviorTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs index 1241b3d02b21..f3abe24dc32f 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Linq; using Microsoft.SemanticKernel; using Xunit; From 955777b396c27d8649ccfc2315c41d1f913a2e4a Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Tue, 11 Jun 2024 18:40:10 +0100 Subject: [PATCH 84/90] Update dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- .../AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index 6cba385bfbeb..d64570d1ad47 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -69,7 +69,7 @@ public static FunctionChoiceBehavior None(IEnumerable? functions } /// Returns the configuration specified by the . - /// The caller context. + /// The function choice caller context. /// The configuration. public abstract FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context); } From 12df1e0049bf1142fb890d43f326f8458a33cd31 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 11 Jun 2024 19:00:09 +0100 Subject: [PATCH 85/90] fix: fix compilation warning --- .../FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs index 84e0727e22c2..6af32f148b23 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Linq; using Microsoft.SemanticKernel; using Xunit; From 4d748b9c258ae3189da7a8327324b97f97779a94 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 12 Jun 2024 11:24:59 +0100 Subject: [PATCH 86/90] fix: restoring lost logic in OpenAI connector to disable function calling if no functions provided by funciton choice behavior. --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 7640d9d3e2ae..5ddaf2488845 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1610,10 +1610,10 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context if (config.Choice == FunctionChoice.Auto) { - chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; - - if (config.Functions is { } functions) + if (config.Functions is { Count: > 0 } functions) { + chatOptions.ToolChoice = ChatCompletionsToolChoice.Auto; + foreach (var function in functions) { var functionDefinition = function.Metadata.ToOpenAIFunction().ToFunctionDefinition(); @@ -1644,10 +1644,10 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context if (config.Choice == FunctionChoice.None) { - chatOptions.ToolChoice = ChatCompletionsToolChoice.None; - - if (config.Functions is { } functions) + if (config.Functions is { Count: > 0 } functions) { + chatOptions.ToolChoice = ChatCompletionsToolChoice.None; + foreach (var function in functions) { var functionDefinition = function.Metadata.ToOpenAIFunction().ToFunctionDefinition(); From 7530ceff0a6952ea74603ff0373247f7a4b058f0 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 14 Jun 2024 16:13:57 +0100 Subject: [PATCH 87/90] fix: fix unit test and samples --- .../samples/Concepts/FunctionCalling/OpenAI_FunctionCalling.cs | 2 +- .../src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/Concepts/FunctionCalling/OpenAI_FunctionCalling.cs b/dotnet/samples/Concepts/FunctionCalling/OpenAI_FunctionCalling.cs index 1b817fbc60fe..3acf2bc61b95 100644 --- a/dotnet/samples/Concepts/FunctionCalling/OpenAI_FunctionCalling.cs +++ b/dotnet/samples/Concepts/FunctionCalling/OpenAI_FunctionCalling.cs @@ -233,7 +233,7 @@ public async Task RunNonStreamingPromptWithSimulatedFunctionAsync() } // Adding a simulated function call to the connector response message - FunctionCallContent simulatedFunctionCall = new("weather-alert", id: "call_123"); + FunctionCallContent simulatedFunctionCall = new("weather_alert", id: "call_123"); result.Items.Add(simulatedFunctionCall); // Adding a simulated function result to chat history diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index 6fdda501ce77..c75c198b6001 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -737,7 +737,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu } // Adding a simulated function call to the connector response message - var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); + var simulatedFunctionCall = new FunctionCallContent("weather_alert", id: "call_123"); fcContent.Items.Add(simulatedFunctionCall); // Adding a simulated function result to chat history From 1801926add7b9a09a1bf77d25aed93aec16a4578 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 17 Jun 2024 11:40:04 +0100 Subject: [PATCH 88/90] feat: add options to funciton choice behavior classes to supply different parameters LLM may have. --- .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 2 +- .../OpenAIAutoFunctionChoiceBehaviorTests.cs | 12 ++-- ...enAIRequiredFunctionChoiceBehaviorTests.cs | 12 ++-- .../AutoFunctionChoiceBehavior.cs | 18 +++--- .../FunctionChoiceBehavior.cs | 56 +++++++++++++++++-- .../FunctionChoiceBehaviorConfiguration.cs | 8 ++- .../FunctionChoiceBehaviorOptions.cs | 17 ++++++ .../NoneFunctionChoiceBehavior.cs | 14 ++++- .../RequiredFunctionChoiceBehavior.cs | 18 +++--- .../AI/PromptExecutionSettings.cs | 6 +- .../AutoFunctionChoiceBehaviorTests.cs | 12 ++-- .../RequiredFunctionChoiceBehaviorTests.cs | 14 ++--- .../Functions/FunctionChoiceBehaviorTests.cs | 16 +++--- 13 files changed, 138 insertions(+), 67 deletions(-) create mode 100644 dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorOptions.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs index 57a621c3bb42..18594daa58db 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs @@ -1600,7 +1600,7 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context (bool? AllowAnyRequestedKernelFunction, int? MaximumAutoInvokeAttempts) result = new() { AllowAnyRequestedKernelFunction = config.AllowAnyRequestedKernelFunction, - MaximumAutoInvokeAttempts = config.AutoInvoke ? MaximumAutoInvokeAttempts : 0, + MaximumAutoInvokeAttempts = config.Options.AutoInvoke ? MaximumAutoInvokeAttempts : 0, }; if (config.Choice == FunctionChoice.Required && requestIndex >= MaximumUseAttempts) diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs index e2955da2bf64..cdc84f54ed9e 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAutoFunctionChoiceBehaviorTests.cs @@ -44,7 +44,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomat }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: true) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { AutoInvoke = true }) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -106,7 +106,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { AutoInvoke = false }) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -141,7 +141,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomat await next(context); }); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: true) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { AutoInvoke = true }) }; string result = ""; @@ -215,7 +215,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall var functionsForManualInvocation = new List(); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { AutoInvoke = false }) }; // Act await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) @@ -248,7 +248,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManu }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(false, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto([plugin.ElementAt(1)], autoInvoke: false) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -285,7 +285,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManu var functionsForManualInvocation = new List(); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(false, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto([plugin.ElementAt(1)], autoInvoke: false) }; // Act await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs index ebaae89f3b04..efc8e4fa68fd 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIRequiredFunctionChoiceBehaviorTests.cs @@ -45,7 +45,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomat }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Required(true, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Required([plugin.ElementAt(1)], autoInvoke: true) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -110,7 +110,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Required(false, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Required([plugin.ElementAt(1)], autoInvoke: false) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -146,7 +146,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionAutomat await next(context); }); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Required(true, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Required([plugin.ElementAt(1)], autoInvoke: true) }; string result = ""; @@ -223,7 +223,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeKernelFunctionManuall var functionsForManualInvocation = new List(); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Required(false, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Required([plugin.ElementAt(1)], autoInvoke: false) }; // Act await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) @@ -256,7 +256,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManu }); // Act - var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Required(false, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Required([plugin.ElementAt(1)], autoInvoke: false) }; var result = await this._kernel.InvokePromptAsync("How many days until Christmas?", new(settings)); @@ -293,7 +293,7 @@ public async Task SpecifiedInCodeInstructsConnectorToInvokeNonKernelFunctionManu var functionsForManualInvocation = new List(); - var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Required(false, [plugin.ElementAt(1)]) }; + var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Required([plugin.ElementAt(1)], autoInvoke: false) }; // Act await foreach (var content in this._kernel.InvokePromptStreamingAsync("How many days until Christmas?", new(settings))) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index 71f40a26a2d3..266afc6dc451 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -18,9 +18,9 @@ internal sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior private readonly IEnumerable? _functions; /// - /// Indicates whether the functions should be automatically invoked by the AI service/connector. + /// The behavior options. /// - private readonly bool _autoInvoke = true; + private readonly FunctionChoiceBehaviorOptions _options; /// /// Initializes a new instance of the class. @@ -28,19 +28,20 @@ internal sealed class AutoFunctionChoiceBehavior : FunctionChoiceBehavior [JsonConstructor] public AutoFunctionChoiceBehavior() { + this._options = new FunctionChoiceBehaviorOptions(); } /// /// Initializes a new instance of the class. /// - /// Indicates whether the functions should be automatically invoked by the AI service/connector. /// The subset of the 's plugins' functions to provide to the model. /// If null or empty, all 's plugins' functions are provided to the model. - public AutoFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable? functions = null) + /// The behavior options. + public AutoFunctionChoiceBehavior(IEnumerable? functions = null, FunctionChoiceBehaviorOptions? options = null) { - this._autoInvoke = autoInvoke; this._functions = functions; this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); + this._options = options ?? new FunctionChoiceBehaviorOptions(); } /// @@ -58,7 +59,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho // and then fail to do so, so we fail before we get to that point. This is an error // on the consumers behalf: if they specify auto-invocation with any functions, they must // specify the kernel and the kernel must contain those functions. - if (this._autoInvoke && context.Kernel is null) + if (this._options.AutoInvoke && context.Kernel is null) { throw new KernelException("Auto-invocation for Auto choice behavior is not supported when no kernel is provided."); } @@ -83,7 +84,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho } // If auto-invocation is requested and no function is found in the kernel, fail early. - if (this._autoInvoke) + if (this._options.AutoInvoke) { throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); } @@ -111,11 +112,10 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho } } - return new FunctionChoiceBehaviorConfiguration() + return new FunctionChoiceBehaviorConfiguration(this._options) { Choice = FunctionChoice.Auto, Functions = availableFunctions, - AutoInvoke = this._autoInvoke, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index d64570d1ad47..d783d4aa9bcb 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -30,26 +30,69 @@ internal FunctionChoiceBehavior() /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior allows the model to decide whether to call the functions and, if so, which ones to call. /// + /// The subset of the 's plugins' functions to provide to the model. + /// If null or empty, all 's plugins' functions are provided to the model. /// Indicates whether the functions should be automatically invoked by the AI service/connector. + /// An instance of one of the . + public static FunctionChoiceBehavior Auto(IEnumerable? functions, bool autoInvoke) + { + return new AutoFunctionChoiceBehavior(functions, new FunctionChoiceBehaviorOptions() { AutoInvoke = autoInvoke }); + } + + /// + /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. + /// This behavior allows the model to decide whether to call the functions and, if so, which ones to call. + /// /// The subset of the 's plugins' functions to provide to the model. /// If null or empty, all 's plugins' functions are provided to the model. + /// The options for the behavior. /// An instance of one of the . - public static FunctionChoiceBehavior Auto(bool autoInvoke = true, IEnumerable? functions = null) + public static FunctionChoiceBehavior Auto(IEnumerable? functions = null, FunctionChoiceBehaviorOptions? options = null) { - return new AutoFunctionChoiceBehavior(autoInvoke, functions); + return new AutoFunctionChoiceBehavior(functions, options); } /// /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. /// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. /// + /// The subset of the 's plugins' functions to provide to the model. + /// If null or empty, all 's plugins' functions are provided to the model. /// Indicates whether the functions should be automatically invoked by the AI service/connector. + /// An instance of one of the . + public static FunctionChoiceBehavior Required(IEnumerable? functions, bool autoInvoke) + { + return new RequiredFunctionChoiceBehavior(functions, new FunctionChoiceBehaviorOptions { AutoInvoke = autoInvoke }); + } + + /// + /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. + /// This behavior forces the model to always call one or more functions. The model will then select which function(s) to call. + /// /// The subset of the 's plugins' functions to provide to the model. /// If null or empty, all 's plugins' functions are provided to the model. + /// The options for the behavior. /// An instance of one of the . - public static FunctionChoiceBehavior Required(bool autoInvoke = true, IEnumerable? functions = null) + public static FunctionChoiceBehavior Required(IEnumerable? functions = null, FunctionChoiceBehaviorOptions? options = null) + { + return new RequiredFunctionChoiceBehavior(functions, options); + } + + /// + /// Gets an instance of the that provides either all of the 's plugins' function information to the model or a specified subset. + /// This behavior forces the model to not call any functions and only generate a user-facing message. + /// + /// The subset of the 's plugins' functions to provide to the model. + /// If null or empty, all 's plugins' functions are provided to the model. + /// Indicates whether the functions should be automatically invoked by the AI service/connector. + /// An instance of one of the . + /// + /// Although this behavior prevents the model from calling any functions, the model can use the provided function information + /// to describe how it would complete the prompt if it had the ability to call the functions. + /// + public static FunctionChoiceBehavior None(IEnumerable? functions, bool autoInvoke) { - return new RequiredFunctionChoiceBehavior(autoInvoke, functions); + return new NoneFunctionChoiceBehavior(functions, new FunctionChoiceBehaviorOptions { AutoInvoke = autoInvoke }); } /// @@ -58,14 +101,15 @@ public static FunctionChoiceBehavior Required(bool autoInvoke = true, IEnumerabl /// /// The subset of the 's plugins' functions to provide to the model. /// If null or empty, all 's plugins' functions are provided to the model. + /// The options for the behavior. /// An instance of one of the . /// /// Although this behavior prevents the model from calling any functions, the model can use the provided function information /// to describe how it would complete the prompt if it had the ability to call the functions. /// - public static FunctionChoiceBehavior None(IEnumerable? functions = null) + public static FunctionChoiceBehavior None(IEnumerable? functions = null, FunctionChoiceBehaviorOptions? options = null) { - return new NoneFunctionChoiceBehavior(functions ?? []); + return new NoneFunctionChoiceBehavior(functions, options); } /// Returns the configuration specified by the . diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index 84571e7fd72c..95ce11c32787 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -13,9 +13,11 @@ public sealed class FunctionChoiceBehaviorConfiguration { /// /// Creates a new instance of the class. + /// The options for the behavior." /// - internal FunctionChoiceBehaviorConfiguration() + internal FunctionChoiceBehaviorConfiguration(FunctionChoiceBehaviorOptions options) { + this.Options = options; } /// @@ -29,9 +31,9 @@ internal FunctionChoiceBehaviorConfiguration() public IReadOnlyList? Functions { get; internal set; } /// - /// Indicates whether the functions should be automatically invoked by the AI service/connector. + /// The behavior options. /// - public bool AutoInvoke { get; internal set; } = true; + public FunctionChoiceBehaviorOptions Options { get; } /// /// Specifies whether validation against a specified list of functions is required before allowing the model to request a function from the kernel. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorOptions.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorOptions.cs new file mode 100644 index 000000000000..cab047d375f5 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorOptions.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.SemanticKernel; + +/// +/// Represents the options for a function choice behavior. +/// +[Experimental("SKEXP0001")] +public sealed class FunctionChoiceBehaviorOptions +{ + /// + /// Indicates whether the functions should be automatically invoked by the AI service/connector. + /// + public bool AutoInvoke { get; set; } = true; +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs index 074e35b2d3de..1f1936bf5bcf 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -21,23 +21,31 @@ internal sealed class NoneFunctionChoiceBehavior : FunctionChoiceBehavior /// private readonly IEnumerable? _functions; + /// + /// The behavior options. + /// + private readonly FunctionChoiceBehaviorOptions _options; + /// /// Initializes a new instance of the class. /// [JsonConstructor] public NoneFunctionChoiceBehavior() { + this._options = new FunctionChoiceBehaviorOptions(); } /// /// Initializes a new instance of the class. /// /// The subset of the 's plugins' functions to provide to the model. + /// The behavior options. /// If null or empty, all 's plugins' functions are provided to the model. - public NoneFunctionChoiceBehavior(IEnumerable functions) + public NoneFunctionChoiceBehavior(IEnumerable? functions, FunctionChoiceBehaviorOptions? options = null) { this._functions = functions; - this.Functions = functions.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); + this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); + this._options = options ?? new FunctionChoiceBehaviorOptions(); } /// @@ -89,7 +97,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho } } - return new FunctionChoiceBehaviorConfiguration() + return new FunctionChoiceBehaviorConfiguration(this._options) { Choice = FunctionChoice.None, Functions = availableFunctions, diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index 8100a231e414..a09736a9715e 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -18,9 +18,9 @@ internal sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior private readonly IEnumerable? _functions; /// - /// Indicates whether the functions should be automatically invoked by the AI service/connector. + /// The behavior options. /// - private readonly bool _autoInvoke = true; + private readonly FunctionChoiceBehaviorOptions _options; /// /// Initializes a new instance of the class. @@ -28,19 +28,20 @@ internal sealed class RequiredFunctionChoiceBehavior : FunctionChoiceBehavior [JsonConstructor] public RequiredFunctionChoiceBehavior() { + this._options = new FunctionChoiceBehaviorOptions(); } /// /// Initializes a new instance of the class. /// - /// Indicates whether the functions should be automatically invoked by the AI service/connector. /// The subset of the 's plugins' functions to provide to the model. + /// The behavior options."" /// If null or empty, all 's plugins' functions are provided to the model. - public RequiredFunctionChoiceBehavior(bool autoInvoke = true, IEnumerable? functions = null) + public RequiredFunctionChoiceBehavior(IEnumerable? functions = null, FunctionChoiceBehaviorOptions? options = null) { - this._autoInvoke = autoInvoke; this._functions = functions; this.Functions = functions?.Select(f => FunctionName.ToFullyQualifiedName(f.Name, f.PluginName, FunctionNameSeparator)).ToList(); + this._options = options ?? new FunctionChoiceBehaviorOptions(); } /// @@ -58,7 +59,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho // and then fail to do so, so we fail before we get to that point. This is an error // on the consumers behalf: if they specify auto-invocation with any functions, they must // specify the kernel and the kernel must contain those functions. - if (this._autoInvoke && context.Kernel is null) + if (this._options.AutoInvoke && context.Kernel is null) { throw new KernelException("Auto-invocation for Required choice behavior is not supported when no kernel is provided."); } @@ -83,7 +84,7 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho } // If auto-invocation is requested and no function is found in the kernel, fail early. - if (this._autoInvoke) + if (this._options.AutoInvoke) { throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); } @@ -111,11 +112,10 @@ public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionCho } } - return new FunctionChoiceBehaviorConfiguration() + return new FunctionChoiceBehaviorConfiguration(this._options) { Choice = FunctionChoice.Required, Functions = availableFunctions, - AutoInvoke = this._autoInvoke, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs index 4bc9bef203a1..a981e0ee7a19 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/PromptExecutionSettings.cs @@ -52,17 +52,17 @@ public string? ModelId /// To disable function calling, and have the model only generate a user-facing message, set the property to null (the default). /// /// To allow the model to decide whether to call the functions and, if so, which ones to call, set the property to an instance returned - /// from method. By default, all functions in the will be available. + /// from method. By default, all functions in the will be available. /// To limit the functions available, pass a list of the functions when calling the method. /// /// /// To force the model to always call one or more functions, set the property to an instance returned - /// from method. By default, all functions in the will be available. + /// from method. By default, all functions in the will be available. /// To limit the functions available, pass a list of the functions when calling the method. /// /// /// To force the model to not call any functions and only generate a user-facing message, set the property to an instance returned - /// from property. By default, all functions in the will be available. + /// from property. By default, all functions in the will be available. /// To limit the functions available, pass a list of the functions when calling the method. /// /// diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs index 6af32f148b23..3de1e71b5262 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs @@ -92,7 +92,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorForManualInvocat var plugin = GetTestPlugin(); // Act - var choiceBehavior = new AutoFunctionChoiceBehavior(functions: [plugin.ElementAt(0), plugin.ElementAt(1)], autoInvoke: false); + var choiceBehavior = new AutoFunctionChoiceBehavior([plugin.ElementAt(0), plugin.ElementAt(1)], new FunctionChoiceBehaviorOptions() { AutoInvoke = false }); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -113,7 +113,7 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = new AutoFunctionChoiceBehavior(autoInvoke: false); + var choiceBehavior = new AutoFunctionChoiceBehavior(options: new() { AutoInvoke = false }); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -141,7 +141,7 @@ public void ItShouldAllowAutoInvocationByDefault() // Assert Assert.NotNull(config); - Assert.True(config.AutoInvoke); + Assert.True(config.Options.AutoInvoke); } [Fact] @@ -152,13 +152,13 @@ public void ItShouldAllowManualInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = new AutoFunctionChoiceBehavior(autoInvoke: false); + var choiceBehavior = new AutoFunctionChoiceBehavior(options: new() { AutoInvoke = false }); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); // Assert Assert.NotNull(config); - Assert.False(config.AutoInvoke); + Assert.False(config.Options.AutoInvoke); } [Fact] @@ -221,7 +221,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste var plugin = GetTestPlugin(); this._kernel.Plugins.Add(plugin); - var choiceBehavior = new AutoFunctionChoiceBehavior(autoInvoke: false) + var choiceBehavior = new AutoFunctionChoiceBehavior(options: new() { AutoInvoke = false }) { Functions = ["MyPlugin.NonKernelFunction"] }; diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs index f3abe24dc32f..63802453f3f6 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs @@ -92,7 +92,7 @@ public void ItShouldAdvertiseOnlyFunctionsSuppliedViaConstructorForManualInvocat var plugin = GetTestPlugin(); // Act - var choiceBehavior = new RequiredFunctionChoiceBehavior(false, [plugin.ElementAt(0), plugin.ElementAt(1)]); + var choiceBehavior = new RequiredFunctionChoiceBehavior([plugin.ElementAt(0), plugin.ElementAt(1)], new() { AutoInvoke = false }); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -113,7 +113,7 @@ public void ItShouldAdvertiseAllKernelFunctionsForManualInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = new RequiredFunctionChoiceBehavior(false); + var choiceBehavior = new RequiredFunctionChoiceBehavior(options: new() { AutoInvoke = false }); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); @@ -141,7 +141,7 @@ public void ItShouldAllowAutoInvocationByDefault() // Assert Assert.NotNull(config); - Assert.True(config.AutoInvoke); + Assert.True(config.Options.AutoInvoke); } [Fact] @@ -152,13 +152,13 @@ public void ItShouldAllowManualInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = new RequiredFunctionChoiceBehavior(false); + var choiceBehavior = new RequiredFunctionChoiceBehavior(options: new() { AutoInvoke = false }); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); // Assert Assert.NotNull(config); - Assert.False(config.AutoInvoke); + Assert.False(config.Options.AutoInvoke); } [Fact] @@ -203,7 +203,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedAndFunctionIsNotRegis // Arrange var plugin = GetTestPlugin(); - var choiceBehavior = new RequiredFunctionChoiceBehavior(true, [plugin.ElementAt(0)]); + var choiceBehavior = new RequiredFunctionChoiceBehavior([plugin.ElementAt(0)], options: new() { AutoInvoke = true }); // Act var exception = Assert.Throws(() => @@ -221,7 +221,7 @@ public void ItShouldThrowExceptionIfNoFunctionFoundAndManualInvocationIsRequeste var plugin = GetTestPlugin(); this._kernel.Plugins.Add(plugin); - var choiceBehavior = new RequiredFunctionChoiceBehavior(false) + var choiceBehavior = new RequiredFunctionChoiceBehavior(options: new() { AutoInvoke = false }) { Functions = ["MyPlugin.NonKernelFunction"] }; diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs index 87e0cdc6b135..97326a23d44f 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/FunctionChoiceBehaviorTests.cs @@ -99,13 +99,13 @@ public void AutoFunctionChoiceShouldAllowAutoInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: true); + var choiceBehavior = FunctionChoiceBehavior.Auto(options: new() { AutoInvoke = true }); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); // Assert Assert.NotNull(config); - Assert.True(config.AutoInvoke); + Assert.True(config.Options.AutoInvoke); } [Fact] @@ -116,13 +116,13 @@ public void AutoFunctionChoiceShouldAllowManualInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false); + var choiceBehavior = FunctionChoiceBehavior.Auto(options: new() { AutoInvoke = false }); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); // Assert Assert.NotNull(config); - Assert.False(config.AutoInvoke); + Assert.False(config.Options.AutoInvoke); } [Fact] @@ -176,13 +176,13 @@ public void RequiredFunctionChoiceShouldAllowAutoInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.Required(autoInvoke: true); + var choiceBehavior = FunctionChoiceBehavior.Required(options: new() { AutoInvoke = true }); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); // Assert Assert.NotNull(config); - Assert.True(config.AutoInvoke); + Assert.True(config.Options.AutoInvoke); } [Fact] @@ -193,13 +193,13 @@ public void RequiredFunctionChoiceShouldAllowManualInvocation() this._kernel.Plugins.Add(plugin); // Act - var choiceBehavior = FunctionChoiceBehavior.Required(autoInvoke: false); + var choiceBehavior = FunctionChoiceBehavior.Required(options: new() { AutoInvoke = false }); var config = choiceBehavior.GetConfiguration(new() { Kernel = this._kernel }); // Assert Assert.NotNull(config); - Assert.False(config.AutoInvoke); + Assert.False(config.Options.AutoInvoke); } [Fact] From e89284843cfcc785aa611788902c2482867ee6f8 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 17 Jun 2024 13:51:25 +0100 Subject: [PATCH 89/90] fix: remove duplication of function resolution functionality. --- .../AutoFunctionChoiceBehavior.cs | 60 +--------------- .../FunctionChoiceBehavior.cs | 70 +++++++++++++++++++ .../NoneFunctionChoiceBehavior.cs | 41 +---------- .../RequiredFunctionChoiceBehavior.cs | 60 +--------------- .../AutoFunctionChoiceBehaviorTests.cs | 2 +- .../RequiredFunctionChoiceBehaviorTests.cs | 2 +- 6 files changed, 78 insertions(+), 157 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs index 266afc6dc451..2eca7d9dc715 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehavior.cs @@ -54,68 +54,12 @@ public AutoFunctionChoiceBehavior(IEnumerable? functions = null, /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { - // If auto-invocation is specified, we need a kernel to be able to invoke the functions. - // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions - // and then fail to do so, so we fail before we get to that point. This is an error - // on the consumers behalf: if they specify auto-invocation with any functions, they must - // specify the kernel and the kernel must contain those functions. - if (this._options.AutoInvoke && context.Kernel is null) - { - throw new KernelException("Auto-invocation for Auto choice behavior is not supported when no kernel is provided."); - } - - List? availableFunctions = null; - bool allowAnyRequestedKernelFunction = false; - - // Handle functions provided via the 'Functions' property as function fully qualified names. - if (this.Functions is { } functionFQNs && functionFQNs.Any()) - { - availableFunctions = []; - - foreach (var functionFQN in functionFQNs) - { - var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); - - // Check if the function is available in the kernel. If it is, then connectors can find it for auto-invocation later. - if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) - { - availableFunctions.Add(function); - continue; - } - - // If auto-invocation is requested and no function is found in the kernel, fail early. - if (this._options.AutoInvoke) - { - throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); - } - - // Check if the function instance was provided via the constructor for manual-invocation. - function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); - if (function is not null) - { - availableFunctions.Add(function); - continue; - } - - throw new KernelException($"The specified function {functionFQN} was not found."); - } - } - // Provide all functions from the kernel. - else if (context.Kernel is not null) - { - allowAnyRequestedKernelFunction = true; - - foreach (var plugin in context.Kernel.Plugins) - { - availableFunctions ??= []; - availableFunctions.AddRange(plugin); - } - } + (IReadOnlyList? functions, bool allowAnyRequestedKernelFunction) = base.GetFunctions(this.Functions, this._functions, context.Kernel, this._options.AutoInvoke); return new FunctionChoiceBehaviorConfiguration(this._options) { Choice = FunctionChoice.Auto, - Functions = availableFunctions, + Functions = functions, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index d783d4aa9bcb..7f768ab2eaf1 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Text.Json.Serialization; namespace Microsoft.SemanticKernel; @@ -116,4 +117,73 @@ public static FunctionChoiceBehavior None(IEnumerable? functions /// The function choice caller context. /// The configuration. public abstract FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context); + + /// + /// Returns the functions that the model can choose from. + /// + /// Functions provided as fully qualified names. + /// Functions provided as instances of . + /// /// The to be used for function calling. + /// Indicates whether the functions should be automatically invoked by the AI service/connector. + /// The functions that the model can choose from and a flag indicating whether any requested kernel function is allowed to be called. + private protected (IReadOnlyList? Functions, bool AllowAnyRequestedKernelFunction) GetFunctions(IList? functionFQNs, IEnumerable? functions, Kernel? kernel, bool autoInvoke) + { + // If auto-invocation is specified, we need a kernel to be able to invoke the functions. + // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions + // and then fail to do so, so we fail before we get to that point. This is an error + // on the consumers behalf: if they specify auto-invocation with any functions, they must + // specify the kernel and the kernel must contain those functions. + if (autoInvoke && kernel is null) + { + throw new KernelException("Auto-invocation is not supported when no kernel is provided."); + } + + List? availableFunctions = null; + bool allowAnyRequestedKernelFunction = false; + + if (functionFQNs is { Count: > 0 }) + { + availableFunctions = new List(functionFQNs.Count); + + foreach (var functionFQN in functionFQNs) + { + var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); + + // Look up the function in the kernel. + if (kernel is not null && kernel.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) + { + availableFunctions.Add(function); + continue; + } + + // If auto-invocation is requested and no function is found in the kernel, fail early. + if (autoInvoke) + { + throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); + } + + // Look up the function in the list of functions provided as instances of KernelFunction. + function = functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); + if (function is not null) + { + availableFunctions.Add(function); + continue; + } + + throw new KernelException($"The specified function {functionFQN} was not found."); + } + } + // Provide all functions from the kernel. + else if (kernel is not null) + { + allowAnyRequestedKernelFunction = true; + + foreach (var plugin in kernel.Plugins) + { + (availableFunctions ??= new List(kernel.Plugins.Count)).AddRange(plugin); + } + } + + return new(availableFunctions, allowAnyRequestedKernelFunction); + } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs index 1f1936bf5bcf..1def71f30b2a 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/NoneFunctionChoiceBehavior.cs @@ -58,49 +58,12 @@ public NoneFunctionChoiceBehavior(IEnumerable? functions, Functi /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { - List? availableFunctions = null; - - // Handle functions provided via the 'Functions' property as function fully qualified names. - if (this.Functions is { } functionFQNs && functionFQNs.Any()) - { - availableFunctions = []; - - foreach (var functionFQN in functionFQNs) - { - var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); - - // Check if the function is available in the kernel. - if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) - { - availableFunctions.Add(function); - continue; - } - - // Check if a function instance that was not imported into the kernel was provided through the constructor. - function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); - if (function is not null) - { - availableFunctions.Add(function); - continue; - } - - throw new KernelException($"The specified function {functionFQN} was not found."); - } - } - // Provide all functions from the kernel. - else if (context.Kernel is not null) - { - foreach (var plugin in context.Kernel.Plugins) - { - availableFunctions ??= []; - availableFunctions.AddRange(plugin); - } - } + (IReadOnlyList? functions, _) = base.GetFunctions(this.Functions, this._functions, context.Kernel, autoInvoke: false); return new FunctionChoiceBehaviorConfiguration(this._options) { Choice = FunctionChoice.None, - Functions = availableFunctions, + Functions = functions, }; } } diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs index a09736a9715e..ecb767eccbeb 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs @@ -54,68 +54,12 @@ public RequiredFunctionChoiceBehavior(IEnumerable? functions = n /// public override FunctionChoiceBehaviorConfiguration GetConfiguration(FunctionChoiceBehaviorContext context) { - // If auto-invocation is specified, we need a kernel to be able to invoke the functions. - // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions - // and then fail to do so, so we fail before we get to that point. This is an error - // on the consumers behalf: if they specify auto-invocation with any functions, they must - // specify the kernel and the kernel must contain those functions. - if (this._options.AutoInvoke && context.Kernel is null) - { - throw new KernelException("Auto-invocation for Required choice behavior is not supported when no kernel is provided."); - } - - List? availableFunctions = null; - bool allowAnyRequestedKernelFunction = false; - - // Handle functions provided via the 'Functions' property as function fully qualified names. - if (this.Functions is { } functionFQNs && functionFQNs.Any()) - { - availableFunctions = []; - - foreach (var functionFQN in functionFQNs) - { - var nameParts = FunctionName.Parse(functionFQN, FunctionNameSeparator); - - // Check if the function is available in the kernel. If it is, then connectors can find it for auto-invocation later. - if (context.Kernel!.Plugins.TryGetFunction(nameParts.PluginName, nameParts.Name, out var function)) - { - availableFunctions.Add(function); - continue; - } - - // If auto-invocation is requested and no function is found in the kernel, fail early. - if (this._options.AutoInvoke) - { - throw new KernelException($"The specified function {functionFQN} is not available in the kernel."); - } - - // Check if the function instance was provided via the constructor for manual-invocation. - function = this._functions?.FirstOrDefault(f => f.Name == nameParts.Name && f.PluginName == nameParts.PluginName); - if (function is not null) - { - availableFunctions.Add(function); - continue; - } - - throw new KernelException($"The specified function {functionFQN} was not found."); - } - } - // Provide all functions from the kernel. - else if (context.Kernel is not null) - { - allowAnyRequestedKernelFunction = true; - - foreach (var plugin in context.Kernel.Plugins) - { - availableFunctions ??= []; - availableFunctions.AddRange(plugin); - } - } + (IReadOnlyList? functions, bool allowAnyRequestedKernelFunction) = base.GetFunctions(this.Functions, this._functions, context.Kernel, this._options.AutoInvoke); return new FunctionChoiceBehaviorConfiguration(this._options) { Choice = FunctionChoice.Required, - Functions = availableFunctions, + Functions = functions, AllowAnyRequestedKernelFunction = allowAnyRequestedKernelFunction }; } diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs index 3de1e71b5262..c43810b3c406 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs @@ -194,7 +194,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedButNoKernelIsProvided choiceBehavior.GetConfiguration(new() { Kernel = null }); }); - Assert.Equal("Auto-invocation for Auto choice behavior is not supported when no kernel is provided.", exception.Message); + Assert.Equal("Auto-invocation is not supported when no kernel is provided.", exception.Message); } [Fact] diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs index 63802453f3f6..cc74809398b2 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs @@ -194,7 +194,7 @@ public void ItShouldThrowExceptionIfAutoInvocationRequestedButNoKernelIsProvided choiceBehavior.GetConfiguration(new() { Kernel = null }); }); - Assert.Equal("Auto-invocation for Required choice behavior is not supported when no kernel is provided.", exception.Message); + Assert.Equal("Auto-invocation is not supported when no kernel is provided.", exception.Message); } [Fact] From a72ee47c9df3bad23b01222928e7c4c7cdb96bd2 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 24 Jun 2024 10:30:58 +0100 Subject: [PATCH 90/90] fix: remove the [Experimental("SKEXP0001")] tag from all ne funciton choice behavior related functionality that is going to replace existing tool calling functionality --- .../AI/FunctionChoiceBehaviors/FunctionChoice.cs | 1 - .../AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs | 2 -- .../FunctionChoiceBehaviorConfiguration.cs | 2 -- .../FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs | 3 --- .../FunctionChoiceBehaviors/FunctionChoiceBehaviorOptions.cs | 3 --- 5 files changed, 11 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs index 14daa4b303c5..59eb0e1e5eba 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoice.cs @@ -9,7 +9,6 @@ namespace Microsoft.SemanticKernel; /// Represents an AI model's decision-making strategy for calling functions, offering predefined choices: Auto, Required, and None. /// Auto allows the model to decide if and which functions to call, Required enforces calling one or more functions, and None prevents any function calls, generating only a user-facing message. /// -[Experimental("SKEXP0001")] public readonly struct FunctionChoice : IEquatable { /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs index 7f768ab2eaf1..419c02bfbca5 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -14,7 +13,6 @@ namespace Microsoft.SemanticKernel; [JsonDerivedType(typeof(AutoFunctionChoiceBehavior), typeDiscriminator: "auto")] [JsonDerivedType(typeof(RequiredFunctionChoiceBehavior), typeDiscriminator: "required")] [JsonDerivedType(typeof(NoneFunctionChoiceBehavior), typeDiscriminator: "none")] -[Experimental("SKEXP0001")] public abstract class FunctionChoiceBehavior { /// The separator used to separate plugin name and function name. diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs index 95ce11c32787..cdf8fbb4a0d6 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorConfiguration.cs @@ -1,14 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace Microsoft.SemanticKernel; /// /// Represents function choice behavior configuration produced by a . /// -[Experimental("SKEXP0001")] public sealed class FunctionChoiceBehaviorConfiguration { /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs index 9efc41975aa1..49c2ce1eb6c9 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorContext.cs @@ -1,13 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics.CodeAnalysis; - namespace Microsoft.SemanticKernel; /// /// The context to be provided by the choice behavior consumer in order to obtain the choice behavior configuration. /// -[Experimental("SKEXP0001")] public sealed class FunctionChoiceBehaviorContext { /// diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorOptions.cs b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorOptions.cs index cab047d375f5..d46096b6e963 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorOptions.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehaviorOptions.cs @@ -1,13 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics.CodeAnalysis; - namespace Microsoft.SemanticKernel; /// /// Represents the options for a function choice behavior. /// -[Experimental("SKEXP0001")] public sealed class FunctionChoiceBehaviorOptions { ///