Files
semantic-kernel/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs
T
Yan LiandDmytro Struk afa187db9a .Net: Update FlowOrchestrator to use yaml plugins and reliability improvement (#4665)
### Motivation and Context

1. Update FlowOrchestrator to use yaml plugin definitions
2. Necessary changes to allow non-chat AI Service for ReAct, for better
reliability
3. Some bug fix for repeatable steps

### Description
as above

### 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 😄

---------

Co-authored-by: Dmytro Struk <[email protected]>
2024-01-24 15:58:40 +00:00

301 lines
10 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.DependencyInjection;
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 Xunit;
using Xunit.Abstractions;
namespace Examples;
// This example shows how to use FlowOrchestrator to execute a given flow with interaction with client.
public class Example74_FlowOrchestrator : BaseTest
{
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
");
[Fact(Skip = "Can take more than 1 minute")]
public Task RunAsync()
{
return RunExampleAsync();
}
private async Task RunExampleAsync()
{
var bingConnector = new BingConnector(TestConfiguration.Bing.ApiKey);
var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector);
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();
WriteLine("*****************************************************");
WriteLine("Executing " + nameof(RunExampleAsync));
Stopwatch sw = new();
sw.Start();
WriteLine("Flow: " + s_flow.Name);
var question = s_flow.Steps.First().Goal;
var result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, question).ConfigureAwait(false);
WriteLine("Question: " + question);
WriteLine("Answer: " + result.Metadata!["answer"]);
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)
{
WriteLine($"User: {t}");
result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, t).ConfigureAwait(false);
var responses = result.GetValue<List<string>>()!;
foreach (var response in responses)
{
WriteLine("Assistant: " + response);
}
if (result.IsComplete(s_flow))
{
break;
}
}
WriteLine("\tEmail Address: " + result.Metadata!["email_addresses"]);
WriteLine("\tEmail Payload: " + result.Metadata!["email"]);
WriteLine("Time Taken: " + sw.Elapsed);
WriteLine("*****************************************************");
}
private static FlowOrchestratorConfig GetOrchestratorConfig()
{
var config = new FlowOrchestratorConfig
{
MaxStepIterations = 20
};
return config;
}
private static IKernelBuilder GetKernelBuilder(ILoggerFactory loggerFactory)
{
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton<ILoggerFactory>(loggerFactory);
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; }
}
}
public Example74_FlowOrchestrator(ITestOutputHelper output) : base(output)
{
}
}
//*****************************************************
//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
//*****************************************************