diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 274d92c6e7..92f7afe87a 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -189,6 +189,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Agents", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Agents.UnitTests", "src\Experimental\Agents.UnitTests\Experimental.Agents.UnitTests.csproj", "{4AD80279-9AC1-476F-8103-E6CD5E4FD525}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Orchestration.Flow", "src\Experimental\Orchestration.Flow\Experimental.Orchestration.Flow.csproj", "{B0CE8C69-EC56-4825-94AB-01CA7E8BA55B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Orchestration.Flow.IntegrationTests", "src\Experimental\Orchestration.Flow.IntegrationTests\Experimental.Orchestration.Flow.IntegrationTests.csproj", "{3A4B8F90-3E74-43E0-800C-84F8AA9B5BF3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Orchestration.Flow.UnitTests", "src\Experimental\Orchestration.Flow.UnitTests\Experimental.Orchestration.Flow.UnitTests.csproj", "{731CC542-8BE9-42D4-967D-99206EC2B310}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -439,6 +445,24 @@ Global {4AD80279-9AC1-476F-8103-E6CD5E4FD525}.Publish|Any CPU.Build.0 = Debug|Any CPU {4AD80279-9AC1-476F-8103-E6CD5E4FD525}.Release|Any CPU.ActiveCfg = Release|Any CPU {4AD80279-9AC1-476F-8103-E6CD5E4FD525}.Release|Any CPU.Build.0 = Release|Any CPU + {B0CE8C69-EC56-4825-94AB-01CA7E8BA55B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0CE8C69-EC56-4825-94AB-01CA7E8BA55B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0CE8C69-EC56-4825-94AB-01CA7E8BA55B}.Publish|Any CPU.ActiveCfg = Publish|Any CPU + {B0CE8C69-EC56-4825-94AB-01CA7E8BA55B}.Publish|Any CPU.Build.0 = Publish|Any CPU + {B0CE8C69-EC56-4825-94AB-01CA7E8BA55B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0CE8C69-EC56-4825-94AB-01CA7E8BA55B}.Release|Any CPU.Build.0 = Release|Any CPU + {3A4B8F90-3E74-43E0-800C-84F8AA9B5BF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A4B8F90-3E74-43E0-800C-84F8AA9B5BF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A4B8F90-3E74-43E0-800C-84F8AA9B5BF3}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {3A4B8F90-3E74-43E0-800C-84F8AA9B5BF3}.Publish|Any CPU.Build.0 = Debug|Any CPU + {3A4B8F90-3E74-43E0-800C-84F8AA9B5BF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A4B8F90-3E74-43E0-800C-84F8AA9B5BF3}.Release|Any CPU.Build.0 = Release|Any CPU + {731CC542-8BE9-42D4-967D-99206EC2B310}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {731CC542-8BE9-42D4-967D-99206EC2B310}.Debug|Any CPU.Build.0 = Debug|Any CPU + {731CC542-8BE9-42D4-967D-99206EC2B310}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {731CC542-8BE9-42D4-967D-99206EC2B310}.Publish|Any CPU.Build.0 = Debug|Any CPU + {731CC542-8BE9-42D4-967D-99206EC2B310}.Release|Any CPU.ActiveCfg = Release|Any CPU + {731CC542-8BE9-42D4-967D-99206EC2B310}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -504,6 +528,9 @@ Global {24503383-A8C4-4255-9998-28D70FE8E99A} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} {5438D1E3-E03D-444B-BBBA-478F93161AA8} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658} {4AD80279-9AC1-476F-8103-E6CD5E4FD525} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658} + {B0CE8C69-EC56-4825-94AB-01CA7E8BA55B} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658} + {3A4B8F90-3E74-43E0-800C-84F8AA9B5BF3} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658} + {731CC542-8BE9-42D4-967D-99206EC2B310} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/samples/KernelSyntaxExamples/Example63_FlowOrchestrator.cs b/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs similarity index 70% rename from dotnet/samples/KernelSyntaxExamples/Example63_FlowOrchestrator.cs rename to dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs index 8b9543c8ef..958685d6d7 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example63_FlowOrchestrator.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs @@ -10,12 +10,11 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI; -using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Experimental.Orchestration; +using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticKernel.Plugins.Core; -using Microsoft.SemanticKernel.Plugins.Memory; using Microsoft.SemanticKernel.Plugins.Web; using Microsoft.SemanticKernel.Plugins.Web.Bing; using NCalcPlugins; @@ -24,13 +23,13 @@ using NCalcPlugins; * This example shows how to use FlowOrchestrator to execute a given flow with interaction with client. */ // ReSharper disable once InconsistentNaming -public static class Example63_FlowOrchestrator +public static class Example74_FlowOrchestrator { private static readonly Flow s_flow = FlowSerializer.DeserializeFromYaml(@" name: FlowOrchestrator_Example_Flow goal: answer question and send email steps: - - goal: What is the tallest mountain on Earth? How tall is it divided by 2? + - goal: What is the tallest mountain in Asia? How tall is it divided by 2? plugins: - WebSearchEnginePlugin - LanguageCalculatorPlugin @@ -93,7 +92,7 @@ provides: sw.Start(); Console.WriteLine("Flow: " + s_flow.Name); Console.WriteLine("Please type the question you'd like to ask"); - ContextVariables? result; + FunctionResult? result = null; string? goal = null; do { @@ -108,21 +107,36 @@ provides: if (string.IsNullOrEmpty(goal)) { + goal = input; s_flow.Steps.First().Goal = input; } - result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, input); - Console.WriteLine("Assistant: " + result.ToString()); + try + { + result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, input); + } + catch (KernelException ex) + { + Console.WriteLine("Error: " + ex.Message); + Console.WriteLine("Please try again."); + continue; + } + + var responses = result.GetValue>()!; + foreach (var response in responses) + { + Console.WriteLine("Assistant: " + response); + } if (result.IsComplete(s_flow)) { - Console.WriteLine("\tEmail Address: " + result["email_addresses"]); - Console.WriteLine("\tEmail Payload: " + result["email"]); + Console.WriteLine("\tEmail Address: " + result.Metadata!["email_addresses"]); + Console.WriteLine("\tEmail Payload: " + result.Metadata!["email"]); Console.WriteLine("Flow completed, exiting"); break; } - } while (!string.IsNullOrEmpty(result.ToString()) && result.ToString() != "[]"); + } while (result == null || result.GetValue>()?.Count > 0); Console.WriteLine("Time Taken: " + sw.Elapsed); Console.WriteLine("*****************************************************"); @@ -159,8 +173,8 @@ provides: var result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, question).ConfigureAwait(false); Console.WriteLine("Question: " + question); - Console.WriteLine("Answer: " + result["answer"]); - Console.WriteLine("Assistant: " + result.ToString()); + Console.WriteLine("Answer: " + result.Metadata!["answer"]); + Console.WriteLine("Assistant: " + result.GetValue>()!.Single()); string[] userInputs = new[] { @@ -175,7 +189,11 @@ provides: { Console.WriteLine($"User: {t}"); result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, t).ConfigureAwait(false); - Console.WriteLine("Assistant: " + result.ToString()); + var responses = result.GetValue>()!; + foreach (var response in responses) + { + Console.WriteLine("Assistant: " + response); + } if (result.IsComplete(s_flow)) { @@ -183,8 +201,8 @@ provides: } } - Console.WriteLine("\tEmail Address: " + result["email_addresses"]); - Console.WriteLine("\tEmail Payload: " + result["email"]); + Console.WriteLine("\tEmail Address: " + result.Metadata!["email_addresses"]); + Console.WriteLine("\tEmail Payload: " + result.Metadata!["email"]); Console.WriteLine("Time Taken: " + sw.Elapsed); Console.WriteLine("*****************************************************"); @@ -200,16 +218,15 @@ provides: return config; } - private static KernelBuilder GetKernelBuilder(ILoggerFactory loggerFactory) + private static IKernelBuilder GetKernelBuilder(ILoggerFactory loggerFactory) { var builder = Kernel.CreateBuilder(); return builder - .WithAzureOpenAIChatCompletion( + .AddAzureOpenAIChatCompletion( TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey) - .WithLoggerFactory(loggerFactory); + TestConfiguration.AzureOpenAI.ApiKey); } public sealed class ChatPlugin @@ -223,10 +240,12 @@ provides: The email should conform the regex: {EmailRegex} If I cannot answer, say that I don't know. -Do not expose the regex unless asked. + +# IMPORTANT +Do not expose the regex in your response. "; - private readonly IChatCompletion _chat; + private readonly IChatCompletionService _chat; private int MaxTokens { get; set; } = 256; @@ -234,7 +253,7 @@ Do not expose the regex unless asked. public ChatPlugin(Kernel kernel) { - this._chat = kernel.GetService(); + this._chat = kernel.GetRequiredService(); this._chatRequestSettings = new OpenAIPromptExecutionSettings { MaxTokens = this.MaxTokens, @@ -247,29 +266,28 @@ Do not expose the regex unless asked. [Description("Useful to assist in configuration of email address, must be called after email provided")] public async Task CollectEmailAsync( [Description("The email address provided by the user, pass no matter what the value is")] - string email_address, - ContextVariables variables) + string email_addresses, + KernelArguments arguments) { - var chat = this._chat.CreateNewChat(SystemPrompt); + var chat = new ChatHistory(SystemPrompt); chat.AddUserMessage(Goal); - ChatHistory? chatHistory = variables.GetChatHistory(); - if (chatHistory?.Any() ?? false) + ChatHistory? chatHistory = arguments.GetChatHistory(); + if (chatHistory?.Count > 0) { chat.AddRange(chatHistory); } - if (!string.IsNullOrEmpty(email) && IsValidEmail(email)) + if (!string.IsNullOrEmpty(email_addresses) && IsValidEmail(email_addresses)) { - variables["email_addresses"] = email; - - return "Thanks for providing the info, the following email would be used in subsequent steps: " + email; + return "Thanks for providing the info, the following email would be used in subsequent steps: " + email_addresses; } - variables["email_addresses"] = string.Empty; - variables.PromptInput(); + arguments["email_addresses"] = string.Empty; + arguments.PromptInput(); - return await this._chat.GenerateMessageAsync(chat, this._chatRequestSettings).ConfigureAwait(false); + var response = await this._chat.GetChatMessageContentAsync(chat).ConfigureAwait(false); + return response.Content ?? string.Empty; } private static bool IsValidEmail(string email) @@ -282,24 +300,26 @@ Do not expose the regex unless asked. public sealed class EmailPluginV2 { + private readonly JsonSerializerOptions _serializerOptions = new() { WriteIndented = true }; + [KernelFunction] [Description("Send email")] public string SendEmail( [Description("target email addresses")] - string email_addresses, + string emailAddresses, [Description("answer, which is going to be the email content")] string answer, - ContextVariables variables) + KernelArguments arguments) { var contract = new Email() { - Address = email_addresses, + Address = emailAddresses, Content = answer, }; // for demo purpose only - string emailPayload = JsonSerializer.Serialize(contract, new JsonSerializerOptions() { WriteIndented = true }); - variables["email"] = emailPayload; + string emailPayload = JsonSerializer.Serialize(contract, this._serializerOptions); + arguments["email"] = emailPayload; return "Here's the API contract I will post to mail server: " + emailPayload; } @@ -312,28 +332,28 @@ Do not expose the regex unless asked. } } } + //***************************************************** //Executing RunExampleAsync //Flow: FlowOrchestrator_Example_Flow -//Question: What is the tallest mountain on Earth? How tall is it divided by 2? -//Answer: The tallest mountain on Earth is Mount Everest, which is 29,031.69 feet (8,848.86 meters) above sea level. Half of its height is 14,515.845 feet (4,424.43 meters). -//Assistant: ["Please provide a valid email address."] +//Question: What is the tallest mountain in Asia? How tall is it divided by 2? +//Answer: The tallest mountain in Asia is Mount Everest and its height divided by 2 is 14516. +//Assistant: Please provide a valid email address. //User: my email is bad*email&address -//Assistant: ["I\u0027m sorry, but \u0022bad*email\u0026address\u0022 is not a valid email address. A valid email address should have the format \u0022example@example.com\u0022."] +//Assistant: I'm sorry, but "bad*email&address" does not conform to the standard email format. Please provide a valid email address. //User: my email is sample@xyz.com -//Assistant: ["Do you want to send it to another email address?"] +//Assistant: Did the user indicate whether they want to repeat the previous step? //User: yes -//Assistant: ["Please provide a valid email address."] +//Assistant: Please enter a valid email address. //User: I also want to notify foo@bar.com -//Assistant: ["Do you want to send it to another email address?"] +//Assistant: Did the user indicate whether they want to repeat the previous step? //User: no I don't need notify any more address -//Assistant: [] // Email Address: ["sample@xyz.com","foo@bar.com"] // Email Payload: { // "Address": "[\u0022sample@xyz.com\u0022,\u0022foo@bar.com\u0022]", -// "Content": "The tallest mountain on Earth is Mount Everest, which is 29,031.69 feet (8,848.86 meters) above sea level. Half of its height is 14,515.845 feet (4,424.43 meters)." +// "Content": "The tallest mountain in Asia is Mount Everest and its height divided by 2 is 14516." //} -//Time Taken: 00:00:24.2450785 +//Time Taken: 00:00:21.9681103 //***************************************************** //***************************************************** @@ -341,19 +361,18 @@ Do not expose the regex unless asked. //Flow: FlowOrchestrator_Example_Flow //Please type the question you'd like to ask //User: -//What is the length of the longest river in ireland? -//Assistant: ["Please provide a valid email address."] +//What is the tallest mountain in Asia? How tall is it divided by 2? +//Assistant: Please enter a valid email address. //User: -//foo@bar.com -//Assistant: ["Do you want to send it to another email address?"] +//foo@hotmail.com +//Assistant: Do you want to send it to another email address? //User: -//no -//Assistant: [] -// Email Address: ["foo@bar.com"] +//no I don't +// Email Address: ["foo@hotmail.com"] // Email Payload: { -// "Address": "[\u0022foo@bar.com\u0022]", -// "Content": "The longest river in Ireland is the River Shannon with a length of 360 km (223 miles)." +// "Address": "[\u0022foo@hotmail.com\u0022]", +// "Content": "The tallest mountain in Asia is Mount Everest and its height divided by 2 is 14515.845." //} //Flow completed, exiting -//Time Taken: 00:00:44.0215449 +//Time Taken: 00:01:47.0752303 //***************************************************** diff --git a/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj b/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj index d43c9f8589..a8f404c93b 100644 --- a/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj +++ b/dotnet/samples/KernelSyntaxExamples/KernelSyntaxExamples.csproj @@ -16,7 +16,6 @@ - @@ -57,6 +56,7 @@ + diff --git a/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/CollectEmailPlugin.cs b/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/CollectEmailPlugin.cs index db8b2c07bb..9fee46ea2b 100644 --- a/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/CollectEmailPlugin.cs +++ b/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/CollectEmailPlugin.cs @@ -2,13 +2,12 @@ using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI; -using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Experimental.Orchestration; namespace SemanticKernel.Experimental.Orchestration.Flow.IntegrationTests; @@ -26,7 +25,7 @@ If I cannot answer, say that I don't know. Do not expose the regex unless asked. "; - private readonly IChatCompletion _chat; + private readonly IChatCompletionService _chat; private int MaxTokens { get; set; } = 256; @@ -34,7 +33,7 @@ Do not expose the regex unless asked. public CollectEmailPlugin(Kernel kernel) { - this._chat = kernel.GetService(); + this._chat = kernel.GetRequiredService(); this._chatRequestSettings = new OpenAIPromptExecutionSettings { MaxTokens = this.MaxTokens, @@ -47,29 +46,33 @@ Do not expose the regex unless asked. [Description("Useful to assist in configuration of email address, must be called after email provided")] public async Task CollectEmailAsync( [Description("The email address provided by the user, pass no matter what the value is")] + // ReSharper disable once InconsistentNaming +#pragma warning disable CA1707 // Identifiers should not contain underscores string email_address, - ContextVariables variables) +#pragma warning restore CA1707 // Identifiers should not contain underscores + KernelArguments arguments) { - var chat = this._chat.CreateNewChat(SystemPrompt); + var chat = new ChatHistory(SystemPrompt); chat.AddUserMessage(Goal); - ChatHistory? chatHistory = variables.GetChatHistory(); - if (chatHistory?.Any() ?? false) + ChatHistory? chatHistory = arguments.GetChatHistory(); + if (chatHistory?.Count > 0) { chat.AddRange(chatHistory); } if (!string.IsNullOrEmpty(email_address) && IsValidEmail(email_address)) { - variables["email_address"] = email_address; - return "Thanks for providing the info, the following email would be used in subsequent steps: " + email_address; } // invalid email, prompt user to provide a valid email - variables["email_address"] = string.Empty; - variables.PromptInput(); - return await this._chat.GenerateMessageAsync(chat, this._chatRequestSettings).ConfigureAwait(false); + arguments["email_address"] = string.Empty; + arguments.PromptInput(); + + var response = await this._chat.GetChatMessageContentAsync(chat).ConfigureAwait(false); + + return response.Content ?? string.Empty; } private static bool IsValidEmail(string email) diff --git a/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/Experimental.Orchestration.Flow.IntegrationTests.csproj b/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/Experimental.Orchestration.Flow.IntegrationTests.csproj index f0a70b4577..d28f7036a6 100644 --- a/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/Experimental.Orchestration.Flow.IntegrationTests.csproj +++ b/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/Experimental.Orchestration.Flow.IntegrationTests.csproj @@ -6,7 +6,7 @@ LatestMajor true false - CA2007,VSTHRD111,SKEXP0102 + CA2007,VSTHRD111,SKEXP0102,SKEXP0052,SKEXP0054 b7762d10-e29b-4bb1-8b74-b6d69a667dd4 @@ -32,6 +32,7 @@ + diff --git a/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/FlowOrchestratorTests.cs b/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/FlowOrchestratorTests.cs index 7427d4eddc..f4be196ac8 100644 --- a/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/FlowOrchestratorTests.cs +++ b/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/FlowOrchestratorTests.cs @@ -2,11 +2,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Plugins.Memory; +using Microsoft.SemanticKernel.Experimental.Orchestration; +using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticKernel.Plugins.Web; using Microsoft.SemanticKernel.Plugins.Web.Bing; using SemanticKernel.Experimental.Orchestration.Flow.IntegrationTests.TestSettings; @@ -56,7 +58,7 @@ public sealed class FlowOrchestratorTests : IDisposable Microsoft.SemanticKernel.Experimental.Orchestration.Flow flow = FlowSerializer.DeserializeFromYaml(@" goal: answer question and sent email steps: - - goal: What is the tallest mountain on Earth? How tall is it divided by 2? + - goal: What is the tallest mountain in Asia? How tall is it divided by 2? plugins: - WebSearchEnginePlugin provides: @@ -83,28 +85,29 @@ steps: config: new FlowOrchestratorConfig() { MaxStepIterations = 20 }); // Act - var result = await flowOrchestrator.ExecuteFlowAsync(flow, sessionId, "What is the tallest mountain on Earth? How tall is it divided by 2?"); + var result = await flowOrchestrator.ExecuteFlowAsync(flow, sessionId, "What is the tallest mountain in Asia? How tall is it divided by 2?"); // Assert // Loose assertion -- make sure that the plan was executed and pause when it needs interact with user to get more input - Assert.Contains("email", result.ToString(), StringComparison.InvariantCultureIgnoreCase); + var response = result.GetValue>()!.First(); + Assert.Contains("email", response, StringComparison.InvariantCultureIgnoreCase); // Act result = await flowOrchestrator.ExecuteFlowAsync(flow, sessionId, $"my email is {dummyAddress}"); // Assert - var emailPayload = result["email"]; + var emailPayload = result.Metadata!["email"] as string; Assert.Contains(dummyAddress, emailPayload, StringComparison.InvariantCultureIgnoreCase); Assert.Contains("Everest", emailPayload, StringComparison.InvariantCultureIgnoreCase); } - private KernelBuilder InitializeKernelBuilder() + private IKernelBuilder InitializeKernelBuilder() { AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); Assert.NotNull(azureOpenAIConfiguration); return Kernel.CreateBuilder() - .WithAzureOpenAIChatCompletion( + .AddAzureOpenAIChatCompletion( deploymentName: azureOpenAIConfiguration.ChatDeploymentName!, endpoint: azureOpenAIConfiguration.Endpoint, apiKey: azureOpenAIConfiguration.ApiKey); diff --git a/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/SendEmailPlugin.cs b/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/SendEmailPlugin.cs index 0cf7449e59..a1ecd274ff 100644 --- a/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/SendEmailPlugin.cs +++ b/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/SendEmailPlugin.cs @@ -13,9 +13,12 @@ public sealed class SendEmailPlugin [KernelFunction] [Description("Send email")] public string SendEmail( + // ReSharper disable once InconsistentNaming +#pragma warning disable CA1707 // Identifiers should not contain underscores string email_address, +#pragma warning restore CA1707 // Identifiers should not contain underscores string answer, - ContextVariables variables) + KernelArguments variables) { var contract = new Email() { diff --git a/dotnet/src/Experimental/Orchestration.Flow.UnitTests/ChatHistorySerializerTest.cs b/dotnet/src/Experimental/Orchestration.Flow.UnitTests/ChatHistorySerializerTest.cs index f44910a1e0..308824090b 100644 --- a/dotnet/src/Experimental/Orchestration.Flow.UnitTests/ChatHistorySerializerTest.cs +++ b/dotnet/src/Experimental/Orchestration.Flow.UnitTests/ChatHistorySerializerTest.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Experimental.Orchestration.Execution; using Xunit; diff --git a/dotnet/src/Experimental/Orchestration.Flow/Abstractions/IFlowExecutor.cs b/dotnet/src/Experimental/Orchestration.Flow/Abstractions/IFlowExecutor.cs index 82da65f8e3..48f6f8eabd 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/Abstractions/IFlowExecutor.cs +++ b/dotnet/src/Experimental/Orchestration.Flow/Abstractions/IFlowExecutor.cs @@ -15,7 +15,7 @@ public interface IFlowExecutor /// Flow /// Session id, which is used to track the execution status. /// The input from client to continue the execution. - /// The request context variables + /// The request kernel arguments /// The execution context - Task ExecuteAsync(Flow flow, string sessionId, string input, ContextVariables contextVariables); + Task ExecuteFlowAsync(Flow flow, string sessionId, string input, KernelArguments kernelArguments); } diff --git a/dotnet/src/Experimental/Orchestration.Flow/Abstractions/IFlowStatusProvider.cs b/dotnet/src/Experimental/Orchestration.Flow/Abstractions/IFlowStatusProvider.cs index 1546b1fd38..8491a406ab 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/Abstractions/IFlowStatusProvider.cs +++ b/dotnet/src/Experimental/Orchestration.Flow/Abstractions/IFlowStatusProvider.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Experimental.Orchestration.Execution; namespace Microsoft.SemanticKernel.Experimental.Orchestration.Abstractions; diff --git a/dotnet/src/Experimental/Orchestration.Flow/Execution/ChatHistorySerializer.cs b/dotnet/src/Experimental/Orchestration.Flow/Execution/ChatHistorySerializer.cs index 350913de40..c22eae855e 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/Execution/ChatHistorySerializer.cs +++ b/dotnet/src/Experimental/Orchestration.Flow/Execution/ChatHistorySerializer.cs @@ -3,7 +3,7 @@ using System; using System.Linq; using System.Text.Json; -using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Experimental.Orchestration.Execution; diff --git a/dotnet/src/Experimental/Orchestration.Flow/Execution/FlowExecutor.cs b/dotnet/src/Experimental/Orchestration.Flow/Execution/FlowExecutor.cs index 314749b00e..f719c75434 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/Execution/FlowExecutor.cs +++ b/dotnet/src/Experimental/Orchestration.Flow/Execution/FlowExecutor.cs @@ -7,8 +7,10 @@ using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Experimental.Orchestration.Abstractions; +using Microsoft.SemanticKernel.Experimental.Orchestration.Extensions; namespace Microsoft.SemanticKernel.Experimental.Orchestration.Execution; @@ -94,6 +96,16 @@ internal class FlowExecutor : IFlowExecutor /// private readonly KernelFunction _checkStartStepFunction; + /// + /// ExecuteFlow function + /// + private readonly KernelFunction _executeFlowFunction; + + /// + /// ExecuteStep function + /// + private readonly KernelFunction _executeStepFunction; + internal FlowExecutor(IKernelBuilder kernelBuilder, IFlowStatusProvider statusProvider, Dictionary globalPluginCollection, FlowOrchestratorConfig? config = null) { this._kernelBuilder = kernelBuilder; @@ -105,34 +117,35 @@ internal class FlowExecutor : IFlowExecutor this._flowStatusProvider = statusProvider; this._globalPluginCollection = globalPluginCollection; - var checkRepeatStepPrompt = EmbeddedResource.Read("Plugins.CheckRepeatStep.skprompt.txt")!; var checkRepeatStepConfig = PromptTemplateConfig.FromJson(EmbeddedResource.Read("Plugins.CheckRepeatStep.config.json")!); - this._checkRepeatStepFunction = CreateSemanticFunction(this._systemKernel, "CheckRepeatStep", checkRepeatStepPrompt, checkRepeatStepConfig); + checkRepeatStepConfig.Template = EmbeddedResource.Read("Plugins.CheckRepeatStep.skprompt.txt")!; + this._checkRepeatStepFunction = KernelFunctionFactory.CreateFromPrompt(checkRepeatStepConfig); - var checkStartStepPrompt = EmbeddedResource.Read("Plugins.CheckStartStep.skprompt.txt")!; var checkStartStepConfig = PromptTemplateConfig.FromJson(EmbeddedResource.Read("Plugins.CheckStartStep.config.json")!); - this._checkStartStepFunction = CreateSemanticFunction(this._systemKernel, "CheckStartStep", checkStartStepPrompt, checkStartStepConfig); - - this._systemKernel.Plugins.Add(new KernelPlugin(RestrictedPluginName, new[] - { - this._checkRepeatStepFunction, - this._checkStartStepFunction, - })); + checkStartStepConfig.Template = EmbeddedResource.Read("Plugins.CheckStartStep.skprompt.txt")!; + this._checkStartStepFunction = KernelFunctionFactory.CreateFromPrompt(checkStartStepConfig); this._config.ExcludedPlugins.Add(RestrictedPluginName); this._reActEngine = new ReActEngine(this._systemKernel, this._logger, this._config); + + this._executeFlowFunction = KernelFunctionFactory.CreateFromMethod(this.ExecuteFlowAsync, "ExecuteFlow", "Execute a flow"); + this._executeStepFunction = KernelFunctionFactory.CreateFromMethod(this.ExecuteStepAsync, "ExecuteStep", "Execute a flow step"); } - public async Task ExecuteAsync(Flow flow, string sessionId, string input, ContextVariables contextVariables) + public async Task ExecuteFlowAsync(Flow flow, string sessionId, string input, KernelArguments kernelArguments) { Verify.NotNull(flow, nameof(flow)); - this._logger?.LogInformation("Executing flow {FlowName} with sessionId={SessionId}.", flow.Name, sessionId); + if (this._logger.IsEnabled(LogLevel.Information)) + { + this._logger.LogInformation("Executing flow {FlowName} with sessionId={SessionId}.", flow.Name, sessionId); + } + var sortedSteps = flow.SortSteps(); - var rootContext = contextVariables.Clone(); + var rootContext = new KernelArguments(kernelArguments); - // populate persisted state variables + // populate persisted state arguments ExecutionState executionState = await this._flowStatusProvider.GetExecutionStateAsync(sessionId).ConfigureAwait(false); List outputs = new(); @@ -143,26 +156,26 @@ internal class FlowExecutor : IFlowExecutor foreach (var kv in executionState.Variables) { - rootContext.Set(kv.Key, kv.Value); + rootContext[kv.Key] = kv.Value; } this.ValidateStep(step, rootContext); // init step execution state string stepKey = $"{stepIndex}_{step.Goal}"; - if (!executionState.StepStates.ContainsKey(stepKey)) + if (!executionState.StepStates.TryGetValue(stepKey, out ExecutionState.StepExecutionState? stepState)) { - executionState.StepStates.Add(stepKey, new ExecutionState.StepExecutionState()); + stepState = new ExecutionState.StepExecutionState(); + executionState.StepStates.Add(stepKey, stepState); } - ExecutionState.StepExecutionState stepState = executionState.StepStates[stepKey]; var stepId = $"{stepKey}_{stepState.ExecutionCount}"; var continueLoop = false; var completed = step.Provides.All(_ => executionState.Variables.ContainsKey(_)); if (!completed) { - // On the first iteration of an Optional or ZeroOrMore step, we need to check whether the user wants to start the stepstep + // On the first iteration of an Optional or ZeroOrMore step, we need to check whether the user wants to start the step if (step.CompletionType is CompletionType.Optional or CompletionType.ZeroOrMore && stepState.Status == ExecutionState.Status.NotStarted) { RepeatOrStartStepResult? startStep = await this.CheckStartStepAsync(rootContext, step, sessionId, stepId, input).ConfigureAwait(false); @@ -183,7 +196,11 @@ internal class FlowExecutor : IFlowExecutor { stepState.Status = ExecutionState.Status.InProgress; await this._flowStatusProvider.SaveExecutionStateAsync(sessionId, executionState).ConfigureAwait(false); - this._logger?.LogInformation("Need to start step {StepIndex} for iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + + if (this._logger?.IsEnabled(LogLevel.Information) ?? false) + { + this._logger.LogInformation("Need to start step {StepIndex} for iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + } } else { @@ -194,35 +211,43 @@ internal class FlowExecutor : IFlowExecutor } await this.CompleteStepAsync(rootContext, sessionId, executionState, step, stepState).ConfigureAwait(false); - this._logger?.LogInformation("Completed step {StepIndex} with iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + + if (this._logger?.IsEnabled(LogLevel.Information) ?? false) + { + this._logger.LogInformation("Completed step {StepIndex} with iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + } + continue; } } // execute step - this._logger?.LogInformation( - "Executing step {StepIndex} for iteration={Iteration}, goal={StepGoal}, input={Input}.", stepIndex, - stepState.ExecutionCount, step.Goal, input); + if (this._logger?.IsEnabled(LogLevel.Information) ?? false) + { + this._logger.LogInformation( + "Executing step {StepIndex} for iteration={Iteration}, goal={StepGoal}, input={Input}.", stepIndex, + stepState.ExecutionCount, step.Goal, input); + } Kernel stepKernel = this._kernelBuilder.Build(); - var stepVariables = new ContextVariables(); + var stepArguments = new KernelArguments(); foreach (var key in step.Requires) { - stepVariables.Set(key, rootContext[key]); + stepArguments[key] = rootContext[key]; } foreach (var key in step.Passthrough) { if (rootContext.TryGetValue(key, out var val)) { - stepVariables.Set(key, val); + stepArguments[key] = val; } } - ContextVariables? stepResult; + FunctionResult? stepResult; if (step is Flow flowStep) { - stepResult = await this.ExecuteAsync(flowStep, $"{sessionId}_{stepId}", input, stepVariables).ConfigureAwait(false); + stepResult = await this.ExecuteFlowAsync(flowStep, $"{sessionId}_{stepId}", input, stepArguments).ConfigureAwait(false); } else { @@ -232,7 +257,7 @@ internal class FlowExecutor : IFlowExecutor stepKernel.ImportPluginFromObject(plugin, plugin.GetType().Name); } - stepResult = await this.ExecuteStepAsync(step, sessionId, stepId, input, stepKernel, stepVariables).ConfigureAwait(false); + stepResult = await this.ExecuteStepAsync(step, sessionId, stepId, input, stepKernel, stepArguments).ConfigureAwait(false); } if (!string.IsNullOrEmpty(stepResult.ToString()) && (stepResult.IsPromptInput() || stepResult.IsTerminateFlow())) @@ -247,60 +272,63 @@ internal class FlowExecutor : IFlowExecutor outputs.Add(stepResult.ToString()); } } - else if (stepResult.TryGetValue(Constants.ChatPluginVariables.ExitLoopName, out var exitResponse)) + else if (stepResult.TryGetExitLoopResponse(out string? exitResponse)) { stepState.Status = ExecutionState.Status.Completed; - foreach (var variable in step.Provides) - { - if (!stepResult.ContainsKey(variable)) - { - stepResult[variable] = string.Empty; - } - } + + var metadata = stepResult.Metadata! + .Where(kvp => step.Provides.Contains(kvp.Key)) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + stepResult = new FunctionResult(stepResult.Function, stepResult.GetValue(), metadata: metadata); if (!string.IsNullOrWhiteSpace(exitResponse)) { - outputs.Add(exitResponse); + outputs.Add(exitResponse!); } - this._logger?.LogInformation("Exiting loop for step {StepIndex} with iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + if (this._logger?.IsEnabled(LogLevel.Information) ?? false) + { + this._logger.LogInformation("Exiting loop for step {StepIndex} with iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + } } else if (stepResult.IsContinueLoop()) { continueLoop = true; - this._logger?.LogInformation("Continuing to the next loop iteration for step {StepIndex} with iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + + if (this._logger?.IsEnabled(LogLevel.Information) ?? false) + { + this._logger.LogInformation("Continuing to the next loop iteration for step {StepIndex} with iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + } } - // check if current execution is complete by checking whether all variables are already provided + // check if current execution is complete by checking whether all arguments are already provided completed = true; foreach (var variable in step.Provides) { - if (!stepResult.ContainsKey(variable)) + if (!stepResult.Metadata!.ContainsKey(variable)) { completed = false; } else { - executionState.Variables[variable] = stepResult[variable]; - stepState.AddOrUpdateVariable(stepState.ExecutionCount, variable, - stepResult[variable]); + executionState.Variables[variable] = (string)stepResult.Metadata[variable]!; + stepState.AddOrUpdateVariable(stepState.ExecutionCount, variable, (string)stepResult.Metadata[variable]!); } } foreach (var variable in step.Passthrough) { - if (stepResult.ContainsKey(variable)) + if (stepResult.Metadata!.TryGetValue(variable, out object? variableValue)) { - executionState.Variables[variable] = stepResult[variable]; - stepState.AddOrUpdateVariable(stepState.ExecutionCount, variable, - stepResult[variable]); + executionState.Variables[variable] = (string)variableValue!; + stepState.AddOrUpdateVariable(stepState.ExecutionCount, variable, (string)variableValue!); - // propagate variables to root context, needed if Flow itself is a step + // propagate arguments to root context, needed if Flow itself is a step this.PropagateVariable(rootContext, stepResult, variable); } } - // propagate variables to root context, needed if Flow itself is a step + // propagate arguments to root context, needed if Flow itself is a step foreach (var variable in Constants.ChatPluginVariables.ControlVariables) { this.PropagateVariable(rootContext, stepResult, variable); @@ -309,7 +337,10 @@ internal class FlowExecutor : IFlowExecutor if (completed) { - this._logger?.LogInformation("Completed step {StepIndex} for iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + if (this._logger?.IsEnabled(LogLevel.Information) ?? false) + { + this._logger.LogInformation("Completed step {StepIndex} for iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + } if (step.CompletionType is CompletionType.AtLeastOnce or CompletionType.ZeroOrMore && stepState.Status != ExecutionState.Status.Completed) { @@ -327,7 +358,12 @@ internal class FlowExecutor : IFlowExecutor { // unconfirmed, prompt user outputs.Add(repeatStep.Prompt!); - this._logger?.LogInformation("Unclear intention, need follow up to check whether to repeat the step"); + + if (this._logger?.IsEnabled(LogLevel.Information) ?? false) + { + this._logger.LogInformation("Unclear intention, need follow up to check whether to repeat the step"); + } + await this._flowStatusProvider.SaveExecutionStateAsync(sessionId, executionState).ConfigureAwait(false); break; } @@ -341,13 +377,21 @@ internal class FlowExecutor : IFlowExecutor stepState.ExecutionCount++; await this._flowStatusProvider.SaveExecutionStateAsync(sessionId, executionState).ConfigureAwait(false); - this._logger?.LogInformation("Need repeat step {StepIndex} for iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + + if (this._logger?.IsEnabled(LogLevel.Information) ?? false) + { + this._logger.LogInformation("Need repeat step {StepIndex} for iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + } } else { // completed await this.CompleteStepAsync(rootContext, sessionId, executionState, step, stepState).ConfigureAwait(false); - this._logger?.LogInformation("Completed step {StepIndex} with iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + + if (this._logger?.IsEnabled(LogLevel.Information) ?? false) + { + this._logger.LogInformation("Completed step {StepIndex} with iteration={Iteration}, goal={StepGoal}.", stepIndex, stepState.ExecutionCount, step.Goal); + } } } else @@ -362,25 +406,26 @@ internal class FlowExecutor : IFlowExecutor } } - foreach (var output in outputs) + if (this._logger?.IsEnabled(LogLevel.Information) ?? false) { - this._logger?.LogInformation("[Output] {Output}", output); + foreach (var output in outputs) + { + this._logger?.LogInformation("[Output] {Output}", output); + } } - rootContext.Update(JsonSerializer.Serialize(outputs)); - - return rootContext; + return new FunctionResult(this._executeFlowFunction, outputs, metadata: rootContext); } - private void PropagateVariable(ContextVariables rootContext, ContextVariables stepResult, string variableName) + private void PropagateVariable(KernelArguments rootContext, FunctionResult stepResult, string variableName) { - if (stepResult.ContainsKey(variableName)) + if (stepResult.Metadata!.ContainsKey(variableName)) { - rootContext[variableName] = stepResult[variableName]; + rootContext[variableName] = stepResult.Metadata[variableName]; } } - private async Task CompleteStepAsync(ContextVariables context, string sessionId, ExecutionState state, FlowStep step, ExecutionState.StepExecutionState stepState) + private async Task CompleteStepAsync(KernelArguments context, string sessionId, ExecutionState state, FlowStep step, ExecutionState.StepExecutionState stepState) { stepState.Status = ExecutionState.Status.Completed; state.CurrentStepIndex++; @@ -393,7 +438,7 @@ internal class FlowExecutor : IFlowExecutor } else { - // kvp.Value may contain empty strings when the loop was exited and the variables the step provides weren't set + // kvp.Value may contain empty strings when the loop was exited and the arguments the step provides weren't set state.Variables[kvp.Key] = JsonSerializer.Serialize(kvp.Value.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()); } } @@ -406,31 +451,35 @@ internal class FlowExecutor : IFlowExecutor await this._flowStatusProvider.SaveExecutionStateAsync(sessionId, state).ConfigureAwait(false); } - private void ValidateStep(FlowStep step, ContextVariables context) + private void ValidateStep(FlowStep step, KernelArguments context) { - if (step.Requires.Any(p => !context.ContainsKey(p))) + if (step.Requires.Any(p => !context.ContainsName(p))) { - throw new KernelException($"Step {step.Goal} requires variables {string.Join(",", step.Requires.Where(p => !context.ContainsKey(p)))} that are not provided. "); + throw new KernelException($"Step {step.Goal} requires arguments {string.Join(",", step.Requires.Where(p => !context.ContainsName(p)))} that are not provided. "); } } - private async Task CheckStartStepAsync(ContextVariables context, FlowStep step, string sessionId, string stepId, string input) + private async Task CheckStartStepAsync(KernelArguments context, FlowStep step, string sessionId, string stepId, string input) { - context = context.Clone(); - context.Set("goal", step.Goal); - context.Set("message", step.StartingMessage); + context = new KernelArguments(context) + { + ["goal"] = step.Goal, + ["message"] = step.StartingMessage + }; return await this.CheckRepeatOrStartStepAsync(context, this._checkStartStepFunction, sessionId, $"{stepId}_CheckStartStep", input).ConfigureAwait(false); } - private async Task CheckRepeatStepAsync(ContextVariables context, FlowStep step, string sessionId, string nextStepId, string input) + private async Task CheckRepeatStepAsync(KernelArguments context, FlowStep step, string sessionId, string nextStepId, string input) { - context = context.Clone(); - context.Set("goal", step.Goal); - context.Set("transitionMessage", step.TransitionMessage); + context = new KernelArguments(context) + { + ["goal"] = step.Goal, + ["transitionMessage"] = step.TransitionMessage + }; return await this.CheckRepeatOrStartStepAsync(context, this._checkRepeatStepFunction, sessionId, $"{nextStepId}_CheckRepeatStep", input).ConfigureAwait(false); } - private async Task CheckRepeatOrStartStepAsync(ContextVariables context, KernelFunction function, string sessionId, string checkRepeatOrStartStepId, string input) + private async Task CheckRepeatOrStartStepAsync(KernelArguments context, KernelFunction function, string sessionId, string checkRepeatOrStartStepId, string input) { var chatHistory = await this._flowStatusProvider.GetChatHistoryAsync(sessionId, checkRepeatOrStartStepId).ConfigureAwait(false); if (chatHistory != null) @@ -443,13 +492,21 @@ internal class FlowExecutor : IFlowExecutor } var scratchPad = this.CreateRepeatOrStartStepScratchPad(chatHistory); - context.Set("agentScratchPad", scratchPad); - this._logger?.LogInformation("Scratchpad: {ScratchPad}", scratchPad); + context["agentScratchPad"] = scratchPad; + + if (this._logger.IsEnabled(LogLevel.Information)) + { + this._logger.LogInformation("Scratchpad: {ScratchPad}", scratchPad); + } var llmResponse = await this._systemKernel.InvokeAsync(function, context).ConfigureAwait(false); string llmResponseText = llmResponse.GetValue()?.Trim() ?? string.Empty; - this._logger?.LogInformation("Response from {Function} : {ActionText}", "CheckRepeatOrStartStep", llmResponseText); + + if (this._logger.IsEnabled(LogLevel.Information)) + { + this._logger.LogInformation("Response from {Function} : {ActionText}", "CheckRepeatOrStartStep", llmResponseText); + } Match finalAnswerMatch = s_finalAnswerRegex.Match(llmResponseText); if (finalAnswerMatch.Success) @@ -480,7 +537,7 @@ internal class FlowExecutor : IFlowExecutor return new RepeatOrStartStepResult(null, prompt); } - this._logger?.LogWarning("Missing result tag from {Function} : {ActionText}", "CheckRepeatOrStartStep", llmResponseText); + this._logger.LogWarning("Missing result tag from {Function} : {ActionText}", "CheckRepeatOrStartStep", llmResponseText); chatHistory.AddSystemMessage(llmResponseText + "\nI should provide either [QUESTION] or [FINAL_ANSWER]"); await this._flowStatusProvider.SaveChatHistoryAsync(sessionId, checkRepeatOrStartStepId, chatHistory).ConfigureAwait(false); return null; @@ -504,13 +561,13 @@ internal class FlowExecutor : IFlowExecutor scratchPadLines.Add("[THOUGHT]"); } - scratchPadLines.Add(message.Content); + scratchPadLines.Add(message.Content!); } return string.Join("\n", scratchPadLines).Trim(); } - private async Task ExecuteStepAsync(FlowStep step, string sessionId, string stepId, string input, Kernel kernel, ContextVariables variables) + private async Task ExecuteStepAsync(FlowStep step, string sessionId, string stepId, string input, Kernel kernel, KernelArguments arguments) { var stepsTaken = await this._flowStatusProvider.GetReActStepsAsync(sessionId, stepId).ConfigureAwait(false); var lastStep = stepsTaken.LastOrDefault(); @@ -523,15 +580,15 @@ internal class FlowExecutor : IFlowExecutor var question = step.Goal; foreach (var variable in step.Requires) { - if (!variable.StartsWith("_", StringComparison.InvariantCulture) && variables[variable].Length <= this._config.MaxVariableLength) + if (!variable.StartsWith("_", StringComparison.InvariantCulture) && ((string)arguments[variable]!).Length <= this._config.MaxVariableLength) { - question += $"\n - {variable}: {JsonSerializer.Serialize(variables[variable])}"; + question += $"\n - {variable}: {JsonSerializer.Serialize(arguments[variable])}"; } } for (int i = stepsTaken.Count; i < this._config.MaxStepIterations; i++) { - var actionStep = await this._reActEngine.GetNextStepAsync(kernel, variables, question, stepsTaken).ConfigureAwait(false); + var actionStep = await this._reActEngine.GetNextStepAsync(kernel, arguments, question, stepsTaken).ConfigureAwait(false); if (actionStep is null) { @@ -541,20 +598,23 @@ internal class FlowExecutor : IFlowExecutor stepsTaken.Add(actionStep); - this._logger?.LogInformation("Thought: {Thought}", actionStep.Thought); + if (this._logger?.IsEnabled(LogLevel.Information) ?? false) + { + this._logger.LogInformation("Thought: {Thought}", actionStep.Thought); + } + if (!string.IsNullOrEmpty(actionStep.Action!)) { if (actionStep.Action!.Contains(Constants.StopAndPromptFunctionName)) { string prompt = actionStep.ActionVariables![Constants.StopAndPromptParameterName]; - variables.Update(prompt); - variables.TerminateFlow(); + arguments.TerminateFlow(); - return variables; + return new FunctionResult(this._executeStepFunction, prompt, metadata: arguments); } - var actionContextVariables = new ContextVariables(); - foreach (var kvp in variables) + var actionContextVariables = new KernelArguments(); + foreach (var kvp in arguments) { if (step.Requires.Contains(kvp.Key) || step.Passthrough.Contains(kvp.Key)) { @@ -573,43 +633,43 @@ internal class FlowExecutor : IFlowExecutor chatHistory.AddUserMessage(input); } + string? actionResult; try { await Task.Delay(this._config.MinIterationTimeMs).ConfigureAwait(false); - var result = await this._reActEngine.InvokeActionAsync(actionStep, input, chatHistory, kernel, actionContextVariables).ConfigureAwait(false); + actionResult = await this._reActEngine.InvokeActionAsync(actionStep, input, chatHistory, kernel, actionContextVariables).ConfigureAwait(false); - if (string.IsNullOrEmpty(result)) + if (string.IsNullOrEmpty(actionResult)) { actionStep.Observation = "Got no result from action"; } else { - actionStep.Observation = $"{AuthorRole.Assistant.Label}: {result}\n"; - variables.Update(result); - chatHistory.AddAssistantMessage(result); + actionStep.Observation = $"{AuthorRole.Assistant.Label}: {actionResult}\n"; + chatHistory.AddAssistantMessage(actionResult); await this._flowStatusProvider.SaveChatHistoryAsync(sessionId, stepId, chatHistory).ConfigureAwait(false); foreach (var passthroughParam in step.Passthrough) { - if (actionContextVariables.TryGetValue(passthroughParam, out string? paramValue) && !string.IsNullOrEmpty(paramValue)) + if (actionContextVariables.TryGetValue(passthroughParam, out object? paramValue) && paramValue is string paramStringValue && !string.IsNullOrEmpty(paramStringValue)) { - variables.Set(passthroughParam, actionContextVariables[passthroughParam]); + arguments[passthroughParam] = actionContextVariables[passthroughParam]; } } foreach (var providedParam in step.Provides) { - if (actionContextVariables.TryGetValue(providedParam, out string? paramValue) && !string.IsNullOrEmpty(paramValue)) + if (actionContextVariables.TryGetValue(providedParam, out object? paramValue) && paramValue is string paramStringValue && !string.IsNullOrEmpty(paramStringValue)) { - variables.Set(providedParam, actionContextVariables[providedParam]); + arguments[providedParam] = actionContextVariables[providedParam]; } } foreach (var variable in Constants.ChatPluginVariables.ControlVariables) { - if (actionContextVariables.TryGetValue(variable, out string? variableValue)) + if (actionContextVariables.TryGetValue(variable, out object? variableValue)) { - variables.Set(variable, variableValue); + arguments[variable] = variableValue; } } } @@ -622,7 +682,7 @@ internal class FlowExecutor : IFlowExecutor continue; } - catch (Exception ex) when (!ex.IsCriticalException()) + catch (Exception ex) when (!ex.IsNonRetryable()) { actionStep.Observation = $"Error invoking action {actionStep.Action} : {ex.Message}"; this._logger?.LogWarning(ex, "Error invoking action {Action}", actionStep.Action); @@ -630,51 +690,55 @@ internal class FlowExecutor : IFlowExecutor continue; } - this._logger?.LogInformation("Observation: {Observation}", actionStep.Observation); + if (this._logger?.IsEnabled(LogLevel.Information) ?? false) + { + this._logger.LogInformation("Observation: {Observation}", actionStep.Observation); + } + await this._flowStatusProvider.SaveReActStepsAsync(sessionId, stepId, stepsTaken).ConfigureAwait(false); - if (!string.IsNullOrEmpty(variables.Input)) + if (!string.IsNullOrEmpty(actionResult)) { - if (variables.IsTerminateFlow()) + if (arguments.IsTerminateFlow()) { // Terminate the flow without another round of reasoning, to save the LLM reasoning calls. // This is not suggested unless plugin has performance requirement and has explicitly set the control variable. - return variables; + return new FunctionResult(this._executeStepFunction, actionResult, metadata: arguments); } foreach (var variable in Constants.ChatPluginVariables.ControlVariables) { - if (variables.ContainsKey(variable)) + if (arguments.ContainsName(variable)) { // redirect control to client - return variables; + return new FunctionResult(this._executeStepFunction, actionResult, metadata: arguments); } } - if (!step.Provides.Except(variables.Where(v => !string.IsNullOrEmpty(v.Value)).Select(_ => _.Key)).Any()) + if (!step.Provides.Except(arguments.Where(v => !string.IsNullOrEmpty((string)v.Value!)).Select(_ => _.Key)).Any()) { // step is complete - return variables; + return new FunctionResult(this._executeStepFunction, actionResult, metadata: arguments); } // continue to next iteration continue; } - this._logger?.LogInformation("Action: No result from action"); + this._logger?.LogWarning("Action: No result from action"); } else if (!string.IsNullOrEmpty(actionStep.FinalAnswer)) { if (step.Provides.Count() == 1) { - variables.Set(step.Provides.Single(), actionStep.FinalAnswer); - return variables; + arguments[step.Provides.Single()] = actionStep.FinalAnswer; + return new FunctionResult(this._executeStepFunction, actionStep.FinalAnswer, metadata: arguments); } } else { actionStep.Observation = "ACTION $JSON_BLOB must be provided as part of thought process."; - this._logger?.LogInformation("Action: No action to take"); + this._logger?.LogWarning("Action: No action to take"); } // continue to next iteration @@ -684,13 +748,6 @@ internal class FlowExecutor : IFlowExecutor throw new KernelException($"Failed to complete step {stepId} for session {sessionId}."); } - private static KernelFunction CreateSemanticFunction(Kernel kernel, string functionName, string promptTemplate, PromptTemplateConfig config) - { - var factory = new KernelPromptTemplateFactory(kernel.LoggerFactory); - var template = factory.Create(promptTemplate, config); - return kernel.CreateFunctionFromPrompt(template, config, functionName); - } - private class RepeatOrStartStepResult { public RepeatOrStartStepResult(bool? execute, string? prompt = null) diff --git a/dotnet/src/Experimental/Orchestration.Flow/Execution/FlowStatusProvider.cs b/dotnet/src/Experimental/Orchestration.Flow/Execution/FlowStatusProvider.cs index 64d3818053..74e0b2527c 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/Execution/FlowStatusProvider.cs +++ b/dotnet/src/Experimental/Orchestration.Flow/Execution/FlowStatusProvider.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Experimental.Orchestration.Abstractions; using Microsoft.SemanticKernel.Experimental.Orchestration.Execution; using Microsoft.SemanticKernel.Memory; diff --git a/dotnet/src/Experimental/Orchestration.Flow/Execution/ReActEngine.cs b/dotnet/src/Experimental/Orchestration.Flow/Execution/ReActEngine.cs index da7757d449..cab59dfbe4 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/Execution/ReActEngine.cs +++ b/dotnet/src/Experimental/Orchestration.Flow/Execution/ReActEngine.cs @@ -7,7 +7,8 @@ using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Experimental.Orchestration.Extensions; namespace Microsoft.SemanticKernel.Experimental.Orchestration.Execution; @@ -119,18 +120,18 @@ internal sealed class ReActEngine if (!string.IsNullOrEmpty(promptTemplate)) { var modelPromptTemplate = EmbeddedResource.Read($"Plugins.ReActEngine.{modelId}.skprompt.txt", false); - promptTemplate = string.IsNullOrEmpty(modelPromptTemplate) ? promptTemplate : modelPromptTemplate!; + promptConfig.Template = string.IsNullOrEmpty(modelPromptTemplate) ? promptTemplate : modelPromptTemplate!; } } - this._reActFunction = this.ImportSemanticFunction(systemKernel, "ReActFunction", promptTemplate!, promptConfig); + this._reActFunction = systemKernel.CreateFunctionFromPrompt(promptConfig); } - internal async Task GetNextStepAsync(Kernel kernel, ContextVariables variables, string question, List previousSteps) + internal async Task GetNextStepAsync(Kernel kernel, KernelArguments arguments, string question, List previousSteps) { - variables.Set("question", question); + arguments["question"] = question; var scratchPad = this.CreateScratchPad(previousSteps); - variables.Set("agentScratchPad", scratchPad); + arguments["agentScratchPad"] = scratchPad; var availableFunctions = this.GetAvailableFunctions(kernel).ToArray(); if (availableFunctions.Length == 1) @@ -139,7 +140,12 @@ internal sealed class ReActEngine if (firstActionFunction.Parameters.Count == 0) { var action = $"{firstActionFunction.PluginName}.{firstActionFunction.Name}"; - this._logger?.LogDebug($"Auto selecting {Action} as it is the only function available and it has no parameters.", action); + + if (this._logger.IsEnabled(LogLevel.Debug)) + { + this._logger?.LogDebug("Auto selecting {Action} as it is the only function available and it has no parameters.", action); + } + return new ReActStep { Action = action @@ -148,16 +154,23 @@ internal sealed class ReActEngine } var functionDesc = this.GetFunctionDescriptions(availableFunctions); - variables.Set("functionDescriptions", functionDesc); + arguments["functionDescriptions"] = functionDesc; - this._logger?.LogInformation("question: {Question}", question); - this._logger?.LogInformation("functionDescriptions: {FunctionDescriptions}", functionDesc); - this._logger?.LogInformation("Scratchpad: {ScratchPad}", scratchPad); + if (this._logger.IsEnabled(LogLevel.Information)) + { + this._logger?.LogInformation("question: {Question}", question); + this._logger?.LogInformation("functionDescriptions: {FunctionDescriptions}", functionDesc); + this._logger?.LogInformation("Scratchpad: {ScratchPad}", scratchPad); + } - var llmResponse = await this._reActFunction.InvokeAsync(kernel, variables).ConfigureAwait(false); + var llmResponse = await this._reActFunction.InvokeAsync(kernel, arguments).ConfigureAwait(false); string llmResponseText = llmResponse.GetValue()!.Trim(); - this._logger?.LogDebug("Response : {ActionText}", llmResponseText); + + if (this._logger?.IsEnabled(LogLevel.Debug) ?? false) + { + this._logger?.LogDebug("Response : {ActionText}", llmResponseText); + } var actionStep = this.ParseResult(llmResponseText); @@ -172,31 +185,30 @@ internal sealed class ReActEngine return actionStep; } - internal async Task InvokeActionAsync(ReActStep actionStep, string chatInput, ChatHistory chatHistory, Kernel kernel, ContextVariables contextVariables) + internal async Task InvokeActionAsync(ReActStep actionStep, string chatInput, ChatHistory chatHistory, Kernel kernel, KernelArguments contextVariables) { var variables = actionStep.ActionVariables ?? new Dictionary(); variables[Constants.ActionVariableNames.ChatInput] = chatInput; variables[Constants.ActionVariableNames.ChatHistory] = ChatHistorySerializer.Serialize(chatHistory); - this._logger?.LogInformation("Action: {Action}({ActionVariables})", actionStep.Action, JsonSerializer.Serialize(variables)); - var availableFunctions = this.GetAvailableFunctions(kernel); - var targetFunction = availableFunctions.FirstOrDefault(f => ToFullyQualifiedName(f) == actionStep.Action); - if (targetFunction is null) + if (this._logger.IsEnabled(LogLevel.Information)) { - throw new MissingMethodException($"The function '{actionStep.Action}' was not found."); + this._logger?.LogInformation("Action: {Action}({ActionVariables})", actionStep.Action, JsonSerializer.Serialize(variables)); } + var availableFunctions = this.GetAvailableFunctions(kernel); + var targetFunction = availableFunctions.FirstOrDefault(f => ToFullyQualifiedName(f) == actionStep.Action) ?? throw new MissingMethodException($"The function '{actionStep.Action}' was not found."); var function = kernel.Plugins.GetFunction(targetFunction.PluginName, targetFunction.Name); var functionView = function.Metadata; - var actionContextVariables = this.CreateActionContextVariables(variables, kernel, contextVariables); + var actionContextVariables = this.CreateActionKernelArguments(variables, contextVariables); foreach (var parameter in functionView.Parameters) { - if (!actionContextVariables.ContainsKey(parameter.Name)) + if (!actionContextVariables.ContainsName(parameter.Name)) { - actionContextVariables.Set(parameter.Name, parameter.DefaultValue ?? string.Empty); + actionContextVariables[parameter.Name] = parameter.DefaultValue ?? string.Empty; } } @@ -206,44 +218,35 @@ internal sealed class ReActEngine foreach (var variable in actionContextVariables) { - contextVariables.Set(variable.Key, variable.Value); + contextVariables[variable.Key] = variable.Value; } - this._logger?.LogDebug("Invoked {FunctionName}. Result: {Result}", targetFunction.Name, result.GetValue()); + if (this._logger?.IsEnabled(LogLevel.Debug) ?? false) + { + this._logger?.LogDebug("Invoked {FunctionName}. Result: {Result}", targetFunction.Name, result.GetValue()); + } return result.GetValue() ?? string.Empty; } - catch (Exception e) when (!e.IsCriticalException()) + catch (Exception e) when (!e.IsNonRetryable()) { this._logger?.LogError(e, "Something went wrong in action step: {0}.{1}. Error: {2}", targetFunction.PluginName, targetFunction.Name, e.Message); return $"Something went wrong in action step: {targetFunction.PluginName}.{targetFunction.Name}. Error: {e.Message} {e.InnerException?.Message}"; } } - private ContextVariables CreateActionContextVariables(Dictionary actionVariables, Kernel kernel, ContextVariables contextVariables) + private KernelArguments CreateActionKernelArguments(Dictionary actionVariables, KernelArguments context) { - var actionContext = contextVariables.Clone(); + var actionContext = new KernelArguments(context); foreach (var kvp in actionVariables) { - actionContext.Set(kvp.Key, kvp.Value); + actionContext[kvp.Key] = kvp.Value; } return actionContext; } - private KernelFunction ImportSemanticFunction(Kernel kernel, string functionName, string promptTemplate, PromptTemplateConfig config) - { - var factory = new KernelPromptTemplateFactory(kernel.LoggerFactory); - var template = factory.Create(promptTemplate, config); - - var plugin = new KernelPlugin(RestrictedPluginName); - - kernel.Plugins.Add(plugin); - - return plugin.AddFunctionFromPrompt(template, config, functionName); - } - private string CreateScratchPad(List stepsTaken) { if (stepsTaken.Count == 0) @@ -266,7 +269,11 @@ internal sealed class ReActEngine { if (scratchPadLines.Count / 4.0 > (this._config.MaxTokens * 0.75)) { - this._logger.LogDebug("Scratchpad is too long, truncating. Skipping {CountSkipped} steps.", i + 1); + if (this._logger.IsEnabled(LogLevel.Debug)) + { + this._logger.LogDebug("Scratchpad is too long, truncating. Skipping {CountSkipped} steps.", i + 1); + } + break; } @@ -412,7 +419,7 @@ internal sealed class ReActEngine { var inputs = string.Join("\n", function.Parameters.Select(parameter => { - var defaultValueString = string.IsNullOrEmpty(parameter.DefaultValue) ? string.Empty : $"(default='{parameter.DefaultValue}')"; + var defaultValueString = parameter.DefaultValue is not string value || string.IsNullOrEmpty(value) ? string.Empty : $"(default='{parameter.DefaultValue}')"; return $" - {parameter.Name}: {parameter.Description} {defaultValueString}"; })); diff --git a/dotnet/src/Experimental/Orchestration.Flow/Extensions/ContextVariablesExtensions.cs b/dotnet/src/Experimental/Orchestration.Flow/Extensions/ContextVariablesExtensions.cs deleted file mode 100644 index d1c740b16b..0000000000 --- a/dotnet/src/Experimental/Orchestration.Flow/Extensions/ContextVariablesExtensions.cs +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Linq; -using Microsoft.SemanticKernel.AI.ChatCompletion; -using Microsoft.SemanticKernel.Experimental.Orchestration.Execution; - -namespace Microsoft.SemanticKernel.Experimental.Orchestration; - -/// -/// Extension methods for -/// -// ReSharper disable once InconsistentNaming -public static class ContextVariablesExtensions -{ - /// - /// Check if we should prompt user for input based on current context. - /// - /// Context variables. - internal static bool IsPromptInput(this ContextVariables variables) - { - return variables.TryGetValue(Constants.ChatPluginVariables.PromptInputName, out string? promptInput) - && promptInput == Constants.ChatPluginVariables.DefaultValue; - } - - /// - /// Check if we should force the next iteration loop based on current context. - /// - /// Context variables. - internal static bool IsContinueLoop(this ContextVariables variables) - { - return variables.TryGetValue(Constants.ChatPluginVariables.ContinueLoopName, out string? continueLoop) - && continueLoop == Constants.ChatPluginVariables.DefaultValue; - } - - /// - /// Check if we should terminate flow based on current context. - /// - /// Context variables. - public static bool IsTerminateFlow(this ContextVariables variables) - { - return variables.TryGetValue(Constants.ChatPluginVariables.StopFlowName, out string? stopFlow) - && stopFlow == Constants.ChatPluginVariables.DefaultValue; - } - - /// - /// Check if all variables to be provided with the flow is available in the context - /// - /// Context variables. - /// flow - /// - public static bool IsComplete(this ContextVariables variables, Flow flow) - { - return flow.Provides.All(variables.ContainsKey); - } - - /// - /// Get from context. - /// - /// Context variables. - /// The chat history - public static ChatHistory? GetChatHistory(this ContextVariables variables) - { - if (variables.TryGetValue(Constants.ActionVariableNames.ChatHistory, out string? chatHistoryText) && !string.IsNullOrEmpty(chatHistoryText)) - { - return ChatHistorySerializer.Deserialize(chatHistoryText); - } - - return null; - } - - /// - /// Get latest chat input from context. - /// - /// Context variables. - /// The latest chat input. - public static string GetChatInput(this ContextVariables variables) - { - if (variables.TryGetValue(Constants.ActionVariableNames.ChatInput, out string? chatInput)) - { - return chatInput; - } - - return string.Empty; - } - - /// - /// Signal the orchestrator to prompt user for input with current function response. - /// - /// Context variables. - public static void PromptInput(this ContextVariables variables) - { - // Cant prompt the user for input and exit the execution at the same time - if (!variables.ContainsKey(Constants.ChatPluginVariables.ExitLoopName)) - { - variables.Set(Constants.ChatPluginVariables.PromptInputName, Constants.ChatPluginVariables.DefaultValue); - } - } - - /// - /// Signal the orchestrator to exit out of the AtLeastOnce or ZeroOrMore loop. If response is non-null, that value will be outputted to the user. - /// - /// Context variables. - /// context - public static void ExitLoop(this ContextVariables variables, string? response = null) - { - // Cant prompt the user for input and exit the execution at the same time - if (!variables.ContainsKey(Constants.ChatPluginVariables.PromptInputName)) - { - variables.Set(Constants.ChatPluginVariables.ExitLoopName, response ?? string.Empty); - } - } - - /// - /// Signal the orchestrator to go to the next iteration of the loop in the AtLeastOnce or ZeroOrMore step. - /// - /// Context variables. - public static void ContinueLoop(this ContextVariables variables) - { - variables.Set(Constants.ChatPluginVariables.ContinueLoopName, Constants.ChatPluginVariables.DefaultValue); - } - - /// - /// Signal the orchestrator to terminate the flow. - /// - /// context - public static void TerminateFlow(this ContextVariables variables) - { - variables.Set(Constants.ChatPluginVariables.StopFlowName, Constants.ChatPluginVariables.DefaultValue); - } -} diff --git a/dotnet/src/Experimental/Orchestration.Flow/Extensions/ExceptionExtensions.cs b/dotnet/src/Experimental/Orchestration.Flow/Extensions/ExceptionExtensions.cs new file mode 100644 index 0000000000..b15e775912 --- /dev/null +++ b/dotnet/src/Experimental/Orchestration.Flow/Extensions/ExceptionExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net; + +namespace Microsoft.SemanticKernel.Experimental.Orchestration.Extensions; + +internal static class ExceptionExtensions +{ + internal static bool IsNonRetryable(this Exception ex) + { + bool isContentFilterException = ex is HttpOperationException + { + StatusCode: HttpStatusCode.BadRequest, InnerException: { } + } hoe && hoe.InnerException.Message.Contains("content_filter"); + + return isContentFilterException || ex.IsCriticalException(); + } +} diff --git a/dotnet/src/Experimental/Orchestration.Flow/Extensions/FunctionResultExtensions.cs b/dotnet/src/Experimental/Orchestration.Flow/Extensions/FunctionResultExtensions.cs new file mode 100644 index 0000000000..0fff5b1fab --- /dev/null +++ b/dotnet/src/Experimental/Orchestration.Flow/Extensions/FunctionResultExtensions.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Experimental.Orchestration.Execution; + +namespace Microsoft.SemanticKernel.Experimental.Orchestration; + +/// +/// Extension methods for +/// +// ReSharper disable once InconsistentNaming +public static class FunctionResultExtensions +{ + /// + /// Check if we should prompt user for input based on function result. + /// + /// Function result. + internal static bool IsPromptInput(this FunctionResult result) + { + return result.Metadata!.TryGetValue(Constants.ChatPluginVariables.PromptInputName, out object? promptInput) + && promptInput is Constants.ChatPluginVariables.DefaultValue; + } + + /// + /// Check if we should force the next iteration loop based on function result. + /// + /// Function result. + internal static bool IsContinueLoop(this FunctionResult result) + { + return result.Metadata!.TryGetValue(Constants.ChatPluginVariables.ContinueLoopName, out object? continueLoop) + && continueLoop is Constants.ChatPluginVariables.DefaultValue; + } + + /// + /// Check if we should exit the loop based on function result. + /// + /// Function result. + /// The response to exit loop + internal static bool TryGetExitLoopResponse(this FunctionResult result, out string? response) + { + if (result.Metadata!.TryGetValue(Constants.ChatPluginVariables.ExitLoopName, out object? exitLoop) + && exitLoop is string exitLoopResponse) + { + response = exitLoopResponse; + return true; + } + + response = null; + return false; + } + + /// + /// Check if we should terminate flow based on function result. + /// + /// Function result. + public static bool IsTerminateFlow(this FunctionResult result) + { + return result.Metadata!.TryGetValue(Constants.ChatPluginVariables.StopFlowName, out object? stopFlow) + && stopFlow is Constants.ChatPluginVariables.DefaultValue; + } + + /// + /// Check if all arguments to be provided with the flow is available in the context + /// + /// Function result. + /// flow + /// + public static bool IsComplete(this FunctionResult result, Flow flow) + { + return flow.Provides.All(result.Metadata!.ContainsKey); + } + + /// + /// Get from context. + /// + /// Function result. + /// The chat history + public static ChatHistory? GetChatHistory(this FunctionResult result) + { + if (result.Metadata!.TryGetValue(Constants.ActionVariableNames.ChatHistory, out object? chatHistory) + && chatHistory is string chatHistoryText + && !string.IsNullOrEmpty(chatHistoryText)) + { + return ChatHistorySerializer.Deserialize(chatHistoryText!); + } + + return null; + } + + /// + /// Get latest chat input from context. + /// + /// Function result. + /// The latest chat input. + public static string GetChatInput(this FunctionResult result) + { + if (result.Metadata!.TryGetValue(Constants.ActionVariableNames.ChatInput, out object? chatInput) + && chatInput is string chatInputString) + { + return chatInputString; + } + + return string.Empty; + } +} diff --git a/dotnet/src/Experimental/Orchestration.Flow/Extensions/KernelArgumentsExtensions.cs b/dotnet/src/Experimental/Orchestration.Flow/Extensions/KernelArgumentsExtensions.cs new file mode 100644 index 0000000000..da41084241 --- /dev/null +++ b/dotnet/src/Experimental/Orchestration.Flow/Extensions/KernelArgumentsExtensions.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Experimental.Orchestration.Execution; + +namespace Microsoft.SemanticKernel.Experimental.Orchestration; + +/// +/// Extension methods for +/// +// ReSharper disable once InconsistentNaming +public static class KernelArgumentsExtensions +{ + /// + /// Check if we should prompt user for input based on current context. + /// + /// Context arguments. + internal static bool IsPromptInput(this KernelArguments variables) + { + return variables.TryGetValue(Constants.ChatPluginVariables.PromptInputName, out object? promptInput) + && promptInput is Constants.ChatPluginVariables.DefaultValue; + } + + /// + /// Check if we should force the next iteration loop based on current context. + /// + /// Context arguments. + internal static bool IsContinueLoop(this KernelArguments arguments) + { + return arguments.TryGetValue(Constants.ChatPluginVariables.ContinueLoopName, out object? continueLoop) + && continueLoop is Constants.ChatPluginVariables.DefaultValue; + } + + /// + /// Check if we should terminate flow based on current context. + /// + /// Context arguments. + public static bool IsTerminateFlow(this KernelArguments arguments) + { + return arguments.TryGetValue(Constants.ChatPluginVariables.StopFlowName, out object? stopFlow) + && stopFlow is Constants.ChatPluginVariables.DefaultValue; + } + + /// + /// Check if all arguments to be provided with the flow is available in the context + /// + /// Context arguments. + /// flow + /// + public static bool IsComplete(this KernelArguments arguments, Flow flow) + { + return flow.Provides.All(arguments.ContainsName); + } + + /// + /// Get from context. + /// + /// Context arguments. + /// The chat history + public static ChatHistory? GetChatHistory(this KernelArguments arguments) + { + if (arguments.TryGetValue(Constants.ActionVariableNames.ChatHistory, out object? chatHistory) + && chatHistory is string chatHistoryText + && !string.IsNullOrEmpty(chatHistoryText)) + { + return ChatHistorySerializer.Deserialize(chatHistoryText!); + } + + return null; + } + + /// + /// Get latest chat input from context. + /// + /// Context arguments. + /// The latest chat input. + public static string GetChatInput(this KernelArguments arguments) + { + if (arguments.TryGetValue(Constants.ActionVariableNames.ChatInput, out object? chatInput) + && chatInput is string chatInputString) + { + return chatInputString; + } + + return string.Empty; + } + + /// + /// Signal the orchestrator to prompt user for input with current function response. + /// + /// Context arguments. + public static void PromptInput(this KernelArguments arguments) + { + // Cant prompt the user for input and exit the execution at the same time + if (!arguments.ContainsName(Constants.ChatPluginVariables.ExitLoopName)) + { + arguments[Constants.ChatPluginVariables.PromptInputName] = Constants.ChatPluginVariables.DefaultValue; + } + } + + /// + /// Signal the orchestrator to exit out of the AtLeastOnce or ZeroOrMore loop. If response is non-null, that value will be outputted to the user. + /// + /// Context arguments. + /// context + public static void ExitLoop(this KernelArguments arguments, string? response = null) + { + // Cant prompt the user for input and exit the execution at the same time + if (!arguments.ContainsName(Constants.ChatPluginVariables.PromptInputName)) + { + arguments[Constants.ChatPluginVariables.ExitLoopName] = response ?? string.Empty; + } + } + + /// + /// Signal the orchestrator to go to the next iteration of the loop in the AtLeastOnce or ZeroOrMore step. + /// + /// Context arguments. + public static void ContinueLoop(this KernelArguments arguments) + { + arguments[Constants.ChatPluginVariables.ContinueLoopName] = Constants.ChatPluginVariables.DefaultValue; + } + + /// + /// Signal the orchestrator to terminate the flow. + /// + /// context + public static void TerminateFlow(this KernelArguments arguments) + { + arguments[Constants.ChatPluginVariables.StopFlowName] = Constants.ChatPluginVariables.DefaultValue; + } +} diff --git a/dotnet/src/Experimental/Orchestration.Flow/Extensions/PromptTemplateConfigExtensions.cs b/dotnet/src/Experimental/Orchestration.Flow/Extensions/PromptTemplateConfigExtensions.cs index f41b889e17..f9c63846d6 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/Extensions/PromptTemplateConfigExtensions.cs +++ b/dotnet/src/Experimental/Orchestration.Flow/Extensions/PromptTemplateConfigExtensions.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel.AI; - namespace Microsoft.SemanticKernel.Experimental.Orchestration; /// @@ -16,11 +14,13 @@ internal static class PromptTemplateConfigExtensions /// Value of max tokens to set internal static void SetMaxTokens(this PromptTemplateConfig config, int maxTokens) { - PromptExecutionSettings executionSettings = config.GetDefaultRequestSettings() ?? new(); - if (config.ModelSettings.Count == 0) + var executionSettings = config.ExecutionSettings; + foreach (var setting in executionSettings) { - config.ModelSettings.Add(executionSettings); + if (setting.Value.ExtensionData != null) + { + setting.Value.ExtensionData["max_tokens"] = maxTokens; + } } - executionSettings.ExtensionData["max_tokens"] = maxTokens; } } diff --git a/dotnet/src/Experimental/Orchestration.Flow/FlowOrchestrator.cs b/dotnet/src/Experimental/Orchestration.Flow/FlowOrchestrator.cs index 1fe0b2a1d3..32cbaa7c0c 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/FlowOrchestrator.cs +++ b/dotnet/src/Experimental/Orchestration.Flow/FlowOrchestrator.cs @@ -33,7 +33,7 @@ public class FlowOrchestrator /// The flow validator. /// Optional configuration object public FlowOrchestrator( - KernelBuilder kernelBuilder, + IKernelBuilder kernelBuilder, IFlowStatusProvider flowStatusProvider, Dictionary? globalPluginCollection = null, IFlowValidator? validator = null, @@ -54,14 +54,14 @@ public class FlowOrchestrator /// goal to achieve /// execution session id /// current input - /// execution context variables - /// ContextVariables, which includes a json array of strings as output. The flow result is also exposed through the context when completes. - public async Task ExecuteFlowAsync( + /// execution kernel arguments + /// KernelArguments, which includes a json array of strings as output. The flow result is also exposed through the context when completes. + public async Task ExecuteFlowAsync( [Description("The flow to execute")] Flow flow, [Description("Execution session id")] string sessionId, [Description("Current input")] string input, - [Description("Execution context variables")] - ContextVariables? contextVariables = null) + [Description("Execution arguments")] + KernelArguments? kernelArguments = null) { try { @@ -73,6 +73,6 @@ public class FlowOrchestrator } var executor = new FlowExecutor(this._kernelBuilder, this._flowStatusProvider, this._globalPluginCollection, this._config); - return await executor.ExecuteAsync(flow, sessionId, input, contextVariables ?? new ContextVariables(null)).ConfigureAwait(false); + return await executor.ExecuteFlowAsync(flow, sessionId, input, kernelArguments ?? new KernelArguments(null)).ConfigureAwait(false); } } diff --git a/dotnet/src/Experimental/Orchestration.Flow/FlowOrchestratorConfig.cs b/dotnet/src/Experimental/Orchestration.Flow/FlowOrchestratorConfig.cs index 88f064cb00..34943decdc 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/FlowOrchestratorConfig.cs +++ b/dotnet/src/Experimental/Orchestration.Flow/FlowOrchestratorConfig.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using Microsoft.SemanticKernel.AI; using Microsoft.SemanticKernel.Experimental.Orchestration.Execution; namespace Microsoft.SemanticKernel.Experimental.Orchestration; diff --git a/dotnet/src/Experimental/Orchestration.Flow/Plugins/CheckRepeatStep/config.json b/dotnet/src/Experimental/Orchestration.Flow/Plugins/CheckRepeatStep/config.json index f92c8a1a96..03f0790bf3 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/Plugins/CheckRepeatStep/config.json +++ b/dotnet/src/Experimental/Orchestration.Flow/Plugins/CheckRepeatStep/config.json @@ -1,32 +1,32 @@ { "schema": 1, + "name": "CheckRepeatStep", "description": "Given the chat history, determine if user would like to execute the previous task for one more time. If not concluded, generate the next message for follow up.", - "type": "completion", - "completion": { - "max_tokens": 400, - "temperature": 0, - "top_p": 1, - "presence_penalty": 0, - "frequency_penalty": 0, - "stop_sequences": [ "[RESPONSE]", "[Goal]" ] + "execution_settings": { + "default": { + "max_tokens": 400, + "temperature": 0, + "top_p": 1, + "presence_penalty": 0.0, + "frequency_penalty": 0.0, + "stop_sequences": ["[RESPONSE]"] + } }, - "input": { - "parameters": [ - { - "name": "goal", - "description": "The goal of proposed step", - "defaultValue": "" - }, - { - "name": "transitionMessage", - "description": "The transition message", - "defaultValue": "Did you want to try the previous step again?" - }, - { - "name": "agentScratchPad", - "description": "The agent's scratch pad", - "defaultValue": "" - } - ] - } + "input_variables": [ + { + "name": "goal", + "description": "The goal of proposed step", + "default": "" + }, + { + "name": "transitionMessage", + "description": "The transition message", + "default": "Did you want to try the previous step again?" + }, + { + "name": "agentScratchPad", + "description": "The agent's scratch pad", + "default": "" + } + ] } diff --git a/dotnet/src/Experimental/Orchestration.Flow/Plugins/CheckStartStep/config.json b/dotnet/src/Experimental/Orchestration.Flow/Plugins/CheckStartStep/config.json index 046da6bec2..b4abbfd9f8 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/Plugins/CheckStartStep/config.json +++ b/dotnet/src/Experimental/Orchestration.Flow/Plugins/CheckStartStep/config.json @@ -1,32 +1,32 @@ { "schema": 1, + "name": "CheckStartStep", "description": "Given the chat history, determine if user would like to begin executing the current task. If not concluded, generate the next message for follow up.", - "type": "completion", - "completion": { - "max_tokens": 400, - "temperature": 0, - "top_p": 1, - "presence_penalty": 0, - "frequency_penalty": 0, - "stop_sequences": ["[RESPONSE]"] + "execution_settings": { + "default": { + "max_tokens": 400, + "temperature": 0, + "top_p": 1, + "presence_penalty": 0.0, + "frequency_penalty": 0.0, + "stop_sequences": ["[RESPONSE]"] + } }, - "input": { - "parameters": [ - { - "name": "goal", - "description": "The goal of proposed step", - "defaultValue": "" - }, - { - "name": "message", - "description": "The message to display to the user", - "defaultValue": "" - }, - { - "name": "agentScratchPad", - "description": "The agent's scratch pad", - "defaultValue": "" - } - ] - } + "input_variables": [ + { + "name": "goal", + "description": "The goal of proposed step", + "default": "" + }, + { + "name": "message", + "description": "The message to display to the user", + "default": "" + }, + { + "name": "agentScratchPad", + "description": "The agent's scratch pad", + "default": "" + } + ] } diff --git a/dotnet/src/Experimental/Orchestration.Flow/Plugins/ReActEngine/config.json b/dotnet/src/Experimental/Orchestration.Flow/Plugins/ReActEngine/config.json index a0a2bd3738..4fe286c7a5 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/Plugins/ReActEngine/config.json +++ b/dotnet/src/Experimental/Orchestration.Flow/Plugins/ReActEngine/config.json @@ -1,32 +1,31 @@ { "schema": 1, "description": "Given a request or command or goal generate multi-step plan to reach the goal. After each step LLM is called to perform the reasoning for the next step.", - "type": "completion", - "completion": { - "max_tokens": 400, - "temperature": 0, - "top_p": 0.1, - "presence_penalty": 0, - "frequency_penalty": 0, - "stop_sequences": [ "[OBSERVATION]", "[Observation]", "[QUESTION]"] + "execution_settings": { + "default": { + "max_tokens": 400, + "temperature": 0.1, + "top_p": 1, + "presence_penalty": 0.0, + "frequency_penalty": 0.0, + "stop_sequences": ["[OBSERVATION]", "[Observation]", "[QUESTION]"] + } }, - "input": { - "parameters": [ - { - "name": "question", - "description": "The question to answer", - "defaultValue": "" - }, - { - "name": "agentScratchPad", - "description": "The agent's scratch pad", - "defaultValue": "" - }, - { - "name": "functionDescriptions", - "description": "The manual of the agent's functions", - "defaultValue": "" - } - ] - } + "input_variables": [ + { + "name": "question", + "description": "The question to answer", + "default": "" + }, + { + "name": "agentScratchPad", + "description": "The agent's scratch pad", + "default": "" + }, + { + "name": "functionDescriptions", + "description": "The manual of the agent's functions", + "default": "" + } + ] } diff --git a/dotnet/src/Experimental/Orchestration.Flow/Plugins/ReActEngine/gpt4/config.json b/dotnet/src/Experimental/Orchestration.Flow/Plugins/ReActEngine/gpt4/config.json index 31eab1607d..79023cde49 100644 --- a/dotnet/src/Experimental/Orchestration.Flow/Plugins/ReActEngine/gpt4/config.json +++ b/dotnet/src/Experimental/Orchestration.Flow/Plugins/ReActEngine/gpt4/config.json @@ -1,32 +1,32 @@ { "schema": 1, - "description": "Given a request or command or goal generate multi-step plan to reach the goal. After each step LLM is called to perform the reasoning for the next step.", - "type": "completion", - "completion": { - "max_tokens": 1024, - "temperature": 0, - "top_p": 1, - "presence_penalty": 0, - "frequency_penalty": 0, - "stop_sequences": ["[OBSERVATION]"] + "description": + "Given a request or command or goal generate multi-step plan to reach the goal. After each step LLM is called to perform the reasoning for the next step.", + "execution_settings": { + "default": { + "max_tokens": 400, + "temperature": 0.1, + "top_p": 1, + "presence_penalty": 0.0, + "frequency_penalty": 0.0, + "stop_sequences": ["[OBSERVATION]"] + } }, - "input": { - "parameters": [ - { - "name": "question", - "description": "The question to answer", - "defaultValue": "" - }, - { - "name": "agentScratchPad", - "description": "The agent's scratch pad", - "defaultValue": "" - }, - { - "name": "functionDescriptions", - "description": "The manual of the agent's functions", - "defaultValue": "" - } - ] - } -} + "input_variables": [ + { + "name": "question", + "description": "The question to answer", + "default": "" + }, + { + "name": "agentScratchPad", + "description": "The agent's scratch pad", + "default": "" + }, + { + "name": "functionDescriptions", + "description": "The manual of the agent's functions", + "default": "" + } + ] +} \ No newline at end of file diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/ChatHistoryTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/ChatHistoryTests.cs index 8d89a1d12e..3a7e2bf8bb 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/ChatHistoryTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/ChatHistoryTests.cs @@ -3,9 +3,7 @@ using System; using System.ComponentModel; using System.Linq; -using System.Net.Http; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection;