mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
### 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]>
301 lines
10 KiB
C#
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
|
|
//*****************************************************
|