Files
semantic-kernel/dotnet/samples/KernelSyntaxExamples/Example74_FlowOrchestrator.cs
T
Mark Wallace 1c07c2f9f9 .Net: Remove the NCalc Plugin (#4625)
### Motivation and Context

Plugin is no longer being used

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

- [ ] The code builds clean without any errors or warnings
- [ ] 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
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄
2024-01-17 12:32:28 +00:00

381 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 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();
//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; }
}
}
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
//*****************************************************