Skip to content

Commit

Permalink
.Net: Fix and add back FlowOrchestrator (microsoft#4307)
Browse files Browse the repository at this point in the history
## 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 😄
  • Loading branch information
yan-li committed Dec 21, 2023
1 parent 3e53e50 commit 240a2cb
Show file tree
Hide file tree
Showing 26 changed files with 757 additions and 513 deletions.
27 changes: 27 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@
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;
Expand All @@ -24,13 +23,13 @@
* 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
Expand Down Expand Up @@ -93,7 +92,7 @@ await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(
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
{
Expand All @@ -108,21 +107,36 @@ await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(

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<List<string>>()!;
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<List<string>>()?.Count > 0);

Console.WriteLine("Time Taken: " + sw.Elapsed);
Console.WriteLine("*****************************************************");
Expand Down Expand Up @@ -159,8 +173,8 @@ await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(
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<List<string>>()!.Single());

string[] userInputs = new[]
{
Expand All @@ -175,16 +189,20 @@ await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(
{
Console.WriteLine($"User: {t}");
result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, t).ConfigureAwait(false);
Console.WriteLine("Assistant: " + result.ToString());
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["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("*****************************************************");
Expand All @@ -200,16 +218,15 @@ private static FlowOrchestratorConfig GetOrchestratorConfig()
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
Expand All @@ -223,18 +240,20 @@ public sealed class ChatPlugin
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;

private readonly PromptExecutionSettings _chatRequestSettings;

public ChatPlugin(Kernel kernel)
{
this._chat = kernel.GetService<IChatCompletion>();
this._chat = kernel.GetRequiredService<IChatCompletionService>();
this._chatRequestSettings = new OpenAIPromptExecutionSettings
{
MaxTokens = this.MaxTokens,
Expand All @@ -247,29 +266,28 @@ public ChatPlugin(Kernel kernel)
[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_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)
Expand All @@ -282,24 +300,26 @@ private static bool IsValidEmail(string email)

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;
}
Expand All @@ -312,48 +332,47 @@ private sealed class Email
}
}
}

//*****************************************************
//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
//*****************************************************

//*****************************************************
//Executing RunInteractiveAsync
//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
//*****************************************************
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
<Compile Remove="Example28_ActionPlanner.cs" />
<Compile Remove="Example31_CustomPlanner.cs" />
<Compile Remove="Example51_StepwisePlanner.cs" />
<Compile Remove="Example63_FlowOrchestrator.cs" />
<Compile Remove="RepoUtils\PlanExtensions.cs" />
</ItemGroup>
<ItemGroup>
Expand Down Expand Up @@ -57,6 +56,7 @@
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Redis\Connectors.Memory.Redis.csproj" />
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Pinecone\Connectors.Memory.Pinecone.csproj" />
<ProjectReference Include="..\..\src\Experimental\Agents\Experimental.Agents.csproj" />
<ProjectReference Include="..\..\src\Experimental\Orchestration.Flow\Experimental.Orchestration.Flow.csproj" />
<ProjectReference Include="..\..\src\Extensions\PromptTemplates.Handlebars\PromptTemplates.Handlebars.csproj" />
<ProjectReference Include="..\..\src\Planners\Planners.Handlebars\Planners.Handlebars.csproj" />
<ProjectReference Include="..\..\src\Planners\Planners.OpenAI\Planners.OpenAI.csproj" />
Expand Down
Loading

0 comments on commit 240a2cb

Please sign in to comment.