mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
## Summary This pull request includes a set of changes to the Orchestration.Flow project. The changes include adding a new project, Experimental.Orchestration.Flow, and updating the Example FlowOrchestrator to use it. Additionally, the email plugin has been refactored and a chat completion service has been added. Unused code has been removed, and integration and unit tests have been updated. The ReActEngine and FlowExecutor classes have been refactored to use KernelArguments and FunctionResult instead of ContextVariables and Dictionary<string, string>. The FlowOrchestrator and related extensions have been added to the Experimental.Orchestration.Flow namespace. Finally, plugin configurations have been updated to use input_variables and execution_settings instead of input.parameters and completion. ## Changes - Added Experimental.Orchestration.Flow project - Added Experimental.Orchestration.Flow.IntegrationTests project - Added Experimental.Orchestration.Flow.UnitTests project - Updated Example FlowOrchestrator to use Experimental.Orchestration.Flow - Refactored email plugin to include a new EmailPluginV2 class and added chat completion service - Removed unused code related to an example of the plugin - Updated SendEmailPlugin class to use KernelArguments instead of ContextVariables - Updated IFlowExecutor interface to use FunctionResult instead of ContextVariables - Updated ChatHistorySerializerTest class to use the ChatCompletion namespace instead of the AI.ChatCompletion namespace - Added new KernelFunctions to FlowExecutor class for executing flows and steps - Refactored FlowExecutor to use KernelFunctionFactory - Refactored FlowExecutor to use KernelArguments and FunctionResult instead of ContextVariables and Dictionary<string, string> - Refactored ReActEngine to use KernelArguments instead of ContextVariablesExtensions - Added extension methods for FunctionResult and KernelArguments - Added FlowOrchestrator class to Experimental.Orchestration.Flow namespace - Modified PromptTemplateConfigExtensions.cs to set max tokens - Modified FlowOrchestrator.cs to use IKernelBuilder and IFlowStatusProvider interfaces - Updated CheckRepeatStep and CheckStartStep plugin configurations to use input_variables - Updated ReActEngine and gpt4 plugin configurations to use execution_settings - Updated ReActEngine GPT4 config.json file --- *Powered by [Microsoft Semantic Kernel](https://github.com/microsoft/semantic-kernel)* ### Motivation and Context Adopt latest interfaces and add back Experimental FlowOrchestrator. <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄
379 lines
13 KiB
C#
379 lines
13 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.SemanticKernel;
|
|
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.Web;
|
|
using Microsoft.SemanticKernel.Plugins.Web.Bing;
|
|
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 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 in Asia? How tall is it divided by 2?
|
|
plugins:
|
|
- WebSearchEnginePlugin
|
|
- LanguageCalculatorPlugin
|
|
provides:
|
|
- answer
|
|
- goal: Collect email address
|
|
plugins:
|
|
- ChatPlugin
|
|
completionType: AtLeastOnce
|
|
transitionMessage: do you want to send it to another email address?
|
|
provides:
|
|
- email_addresses
|
|
|
|
- goal: Send email
|
|
plugins:
|
|
- EmailPluginV2
|
|
requires:
|
|
- email_addresses
|
|
- answer
|
|
provides:
|
|
- email
|
|
|
|
provides:
|
|
- email
|
|
");
|
|
|
|
public static Task RunAsync()
|
|
{
|
|
// Load assemblies for external plugins
|
|
Console.WriteLine("Loading {0}", typeof(SimpleCalculatorPlugin).AssemblyQualifiedName);
|
|
|
|
return RunExampleAsync();
|
|
//return RunInteractiveAsync();
|
|
}
|
|
|
|
private static async Task RunInteractiveAsync()
|
|
{
|
|
var bingConnector = new BingConnector(TestConfiguration.Bing.ApiKey);
|
|
var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector);
|
|
using var loggerFactory = LoggerFactory.Create(loggerBuilder =>
|
|
loggerBuilder
|
|
.AddConsole()
|
|
.AddFilter(null, LogLevel.Error));
|
|
Dictionary<object, string?> plugins = new()
|
|
{
|
|
{ webSearchEnginePlugin, "WebSearch" },
|
|
{ new TimePlugin(), "Time" }
|
|
};
|
|
|
|
FlowOrchestrator orchestrator = new(
|
|
GetKernelBuilder(loggerFactory),
|
|
await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(false),
|
|
plugins,
|
|
config: GetOrchestratorConfig());
|
|
var sessionId = Guid.NewGuid().ToString();
|
|
|
|
Console.WriteLine("*****************************************************");
|
|
Console.WriteLine("Executing {0}", nameof(RunInteractiveAsync));
|
|
Stopwatch sw = new();
|
|
sw.Start();
|
|
Console.WriteLine("Flow: " + s_flow.Name);
|
|
Console.WriteLine("Please type the question you'd like to ask");
|
|
FunctionResult? result = null;
|
|
string? goal = null;
|
|
do
|
|
{
|
|
Console.WriteLine("User: ");
|
|
string input = Console.ReadLine() ?? string.Empty;
|
|
|
|
if (string.IsNullOrEmpty(input))
|
|
{
|
|
Console.WriteLine("No input, exiting");
|
|
break;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(goal))
|
|
{
|
|
goal = input;
|
|
s_flow.Steps.First().Goal = input;
|
|
}
|
|
|
|
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<List<string>>()!;
|
|
foreach (var response in responses)
|
|
{
|
|
Console.WriteLine("Assistant: " + response);
|
|
}
|
|
|
|
if (result.IsComplete(s_flow))
|
|
{
|
|
Console.WriteLine("\tEmail Address: " + result.Metadata!["email_addresses"]);
|
|
Console.WriteLine("\tEmail Payload: " + result.Metadata!["email"]);
|
|
|
|
Console.WriteLine("Flow completed, exiting");
|
|
break;
|
|
}
|
|
} while (result == null || result.GetValue<List<string>>()?.Count > 0);
|
|
|
|
Console.WriteLine("Time Taken: " + sw.Elapsed);
|
|
Console.WriteLine("*****************************************************");
|
|
}
|
|
|
|
private static async Task RunExampleAsync()
|
|
{
|
|
var bingConnector = new BingConnector(TestConfiguration.Bing.ApiKey);
|
|
var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector);
|
|
using var loggerFactory = LoggerFactory.Create(loggerBuilder =>
|
|
loggerBuilder
|
|
.AddConsole()
|
|
.AddFilter(null, LogLevel.Error));
|
|
|
|
Dictionary<object, string?> plugins = new()
|
|
{
|
|
{ webSearchEnginePlugin, "WebSearch" },
|
|
{ new TimePlugin(), "Time" }
|
|
};
|
|
|
|
FlowOrchestrator orchestrator = new(
|
|
GetKernelBuilder(loggerFactory),
|
|
await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(false),
|
|
plugins,
|
|
config: GetOrchestratorConfig());
|
|
var sessionId = Guid.NewGuid().ToString();
|
|
|
|
Console.WriteLine("*****************************************************");
|
|
Console.WriteLine("Executing {0}", nameof(RunExampleAsync));
|
|
Stopwatch sw = new();
|
|
sw.Start();
|
|
Console.WriteLine("Flow: " + s_flow.Name);
|
|
var question = s_flow.Steps.First().Goal;
|
|
var result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, question).ConfigureAwait(false);
|
|
|
|
Console.WriteLine("Question: " + question);
|
|
Console.WriteLine("Answer: " + result.Metadata!["answer"]);
|
|
Console.WriteLine("Assistant: " + result.GetValue<List<string>>()!.Single());
|
|
|
|
string[] userInputs = new[]
|
|
{
|
|
"my email is bad*email&address",
|
|
"my email is [email protected]",
|
|
"yes", // confirm to add another email address
|
|
"I also want to notify [email protected]",
|
|
"no I don't need notify any more address", // end of collect emails
|
|
};
|
|
|
|
foreach (var t in userInputs)
|
|
{
|
|
Console.WriteLine($"User: {t}");
|
|
result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, t).ConfigureAwait(false);
|
|
var responses = result.GetValue<List<string>>()!;
|
|
foreach (var response in responses)
|
|
{
|
|
Console.WriteLine("Assistant: " + response);
|
|
}
|
|
|
|
if (result.IsComplete(s_flow))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
Console.WriteLine("\tEmail Address: " + result.Metadata!["email_addresses"]);
|
|
Console.WriteLine("\tEmail Payload: " + result.Metadata!["email"]);
|
|
|
|
Console.WriteLine("Time Taken: " + sw.Elapsed);
|
|
Console.WriteLine("*****************************************************");
|
|
}
|
|
|
|
private static FlowOrchestratorConfig GetOrchestratorConfig()
|
|
{
|
|
var config = new FlowOrchestratorConfig
|
|
{
|
|
MaxStepIterations = 20
|
|
};
|
|
|
|
return config;
|
|
}
|
|
|
|
private static IKernelBuilder GetKernelBuilder(ILoggerFactory loggerFactory)
|
|
{
|
|
var builder = Kernel.CreateBuilder();
|
|
|
|
return builder
|
|
.AddAzureOpenAIChatCompletion(
|
|
TestConfiguration.AzureOpenAI.ChatDeploymentName,
|
|
TestConfiguration.AzureOpenAI.Endpoint,
|
|
TestConfiguration.AzureOpenAI.ApiKey);
|
|
}
|
|
|
|
public sealed class ChatPlugin
|
|
{
|
|
private const string Goal = "Prompt user to provide a valid email address";
|
|
|
|
private const string EmailRegex = @"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$";
|
|
|
|
private const string SystemPrompt =
|
|
$@"I am AI assistant and will only answer questions related to collect email.
|
|
The email should conform the regex: {EmailRegex}
|
|
|
|
If I cannot answer, say that I don't know.
|
|
|
|
# IMPORTANT
|
|
Do not expose the regex in your response.
|
|
";
|
|
|
|
private readonly IChatCompletionService _chat;
|
|
|
|
private int MaxTokens { get; set; } = 256;
|
|
|
|
private readonly PromptExecutionSettings _chatRequestSettings;
|
|
|
|
public ChatPlugin(Kernel kernel)
|
|
{
|
|
this._chat = kernel.GetRequiredService<IChatCompletionService>();
|
|
this._chatRequestSettings = new OpenAIPromptExecutionSettings
|
|
{
|
|
MaxTokens = this.MaxTokens,
|
|
StopSequences = new List<string>() { "Observation:" },
|
|
Temperature = 0
|
|
};
|
|
}
|
|
|
|
[KernelFunction("ConfigureEmailAddress")]
|
|
[Description("Useful to assist in configuration of email address, must be called after email provided")]
|
|
public async Task<string> CollectEmailAsync(
|
|
[Description("The email address provided by the user, pass no matter what the value is")]
|
|
string email_addresses,
|
|
KernelArguments arguments)
|
|
{
|
|
var chat = new ChatHistory(SystemPrompt);
|
|
chat.AddUserMessage(Goal);
|
|
|
|
ChatHistory? chatHistory = arguments.GetChatHistory();
|
|
if (chatHistory?.Count > 0)
|
|
{
|
|
chat.AddRange(chatHistory);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(email_addresses) && IsValidEmail(email_addresses))
|
|
{
|
|
return "Thanks for providing the info, the following email would be used in subsequent steps: " + email_addresses;
|
|
}
|
|
|
|
arguments["email_addresses"] = string.Empty;
|
|
arguments.PromptInput();
|
|
|
|
var response = await this._chat.GetChatMessageContentAsync(chat).ConfigureAwait(false);
|
|
return response.Content ?? string.Empty;
|
|
}
|
|
|
|
private static bool IsValidEmail(string email)
|
|
{
|
|
// check using regex
|
|
var regex = new Regex(EmailRegex);
|
|
return regex.IsMatch(email);
|
|
}
|
|
}
|
|
|
|
public sealed class EmailPluginV2
|
|
{
|
|
private readonly JsonSerializerOptions _serializerOptions = new() { WriteIndented = true };
|
|
|
|
[KernelFunction]
|
|
[Description("Send email")]
|
|
public string SendEmail(
|
|
[Description("target email addresses")]
|
|
string emailAddresses,
|
|
[Description("answer, which is going to be the email content")]
|
|
string answer,
|
|
KernelArguments arguments)
|
|
{
|
|
var contract = new Email()
|
|
{
|
|
Address = emailAddresses,
|
|
Content = answer,
|
|
};
|
|
|
|
// for demo purpose only
|
|
string emailPayload = JsonSerializer.Serialize(contract, this._serializerOptions);
|
|
arguments["email"] = emailPayload;
|
|
|
|
return "Here's the API contract I will post to mail server: " + emailPayload;
|
|
}
|
|
|
|
private sealed class Email
|
|
{
|
|
public string? Address { get; set; }
|
|
|
|
public string? Content { get; set; }
|
|
}
|
|
}
|
|
}
|
|
|
|
//*****************************************************
|
|
//Executing RunExampleAsync
|
|
//Flow: FlowOrchestrator_Example_Flow
|
|
//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'm sorry, but "bad*email&address" does not conform to the standard email format. Please provide a valid email address.
|
|
//User: my email is [email protected]
|
|
//Assistant: Did the user indicate whether they want to repeat the previous step?
|
|
//User: yes
|
|
//Assistant: Please enter a valid email address.
|
|
//User: I also want to notify [email protected]
|
|
//Assistant: Did the user indicate whether they want to repeat the previous step?
|
|
//User: no I don't need notify any more address
|
|
// Email Address: ["[email protected]","[email protected]"]
|
|
// Email Payload: {
|
|
// "Address": "[\[email protected]\u0022,\[email protected]\u0022]",
|
|
// "Content": "The tallest mountain in Asia is Mount Everest and its height divided by 2 is 14516."
|
|
//}
|
|
//Time Taken: 00:00:21.9681103
|
|
//*****************************************************
|
|
|
|
//*****************************************************
|
|
//Executing RunInteractiveAsync
|
|
//Flow: FlowOrchestrator_Example_Flow
|
|
//Please type the question you'd like to ask
|
|
//User:
|
|
//What is the tallest mountain in Asia? How tall is it divided by 2?
|
|
//Assistant: Please enter a valid email address.
|
|
//User:
|
|
//[email protected]
|
|
//Assistant: Do you want to send it to another email address?
|
|
//User:
|
|
//no I don't
|
|
// Email Address: ["[email protected]"]
|
|
// Email Payload: {
|
|
// "Address": "[\[email protected]\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:01:47.0752303
|
|
//*****************************************************
|