-
Notifications
You must be signed in to change notification settings - Fork 3.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
.Net: Feature flow planner #2165
Merged
lemillermicrosoft
merged 25 commits into
microsoft:feature-feature-flow-planner
from
yan-li:feature-flow-planner
Aug 23, 2023
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
698e61a
Introduce FlowPlanner
yan-li 8f7106d
dotnet format
yan-li be6152d
update unittest
yan-li 714dc71
suppress CS1998 in SequentialPlannerUnitTest
yan-li 60d91f9
Support PromptOverride for ReAct engine
yan-li 82be239
update test
yan-li bef0f19
accomodate latest sk package
yan-li f74dc6d
dotnet format
yan-li 8768f6d
Make the step repeatable
yan-li 06b8610
dotnet format and accomodate sdk changes
yan-li 24941b4
fix build warning
yan-li 0dcfc6c
Introduce FlowPlanner
yan-li c5a4243
dotnet format
yan-li 184cb04
update unittest
yan-li e4607c4
suppress CS1998 in SequentialPlannerUnitTest
yan-li 4d46915
Support PromptOverride for ReAct engine
yan-li 30ff5ad
update test
yan-li fa7d9cc
accomodate latest sk package
yan-li 3b0fc3c
dotnet format
yan-li 863d05d
Make the step repeatable
yan-li efa4e2b
dotnet format and accomodate sdk changes
yan-li ba4eb72
fix build warning
yan-li ffa2e08
Merge branch 'feature-flow-planner' of https://github.com/yan-li/sema…
lemillermicrosoft 2a60338
🗑️ Remove Example54 and update Example55 FlowPlanner
lemillermicrosoft 4649dbf
🔄 Update FlowPlanner example and add interactive mode
lemillermicrosoft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
272 changes: 272 additions & 0 deletions
272
dotnet/samples/KernelSyntaxExamples/Example55_FlowPlanner.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
// 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.SemanticKernel; | ||
using Microsoft.SemanticKernel.AI.ChatCompletion; | ||
using Microsoft.SemanticKernel.Memory; | ||
using Microsoft.SemanticKernel.Orchestration; | ||
using Microsoft.SemanticKernel.Planning; | ||
using Microsoft.SemanticKernel.Planning.Flow; | ||
using Microsoft.SemanticKernel.Reliability; | ||
using Microsoft.SemanticKernel.SkillDefinition; | ||
using Microsoft.SemanticKernel.Skills.Core; | ||
using Microsoft.SemanticKernel.Skills.Web; | ||
using Microsoft.SemanticKernel.Skills.Web.Bing; | ||
|
||
/** | ||
* This example shows how to use FlowPlanner to execute a given flow with interaction with client. | ||
*/ | ||
|
||
// ReSharper disable once InconsistentNaming | ||
public static class Example55_FlowPlanner | ||
{ | ||
private static readonly Flow s_flow = FlowSerializer.DeserializeFromYaml(@" | ||
name: FlowPlanner_Example_Flow | ||
goal: answer question and send email | ||
steps: | ||
- goal: Who is the current president of the United States? What is his current age divided by 2 | ||
skills: | ||
- WebSearchEngineSkill | ||
- TimeSkill | ||
provides: | ||
- answer | ||
- goal: Collect email address | ||
skills: | ||
- CollectEmailSkill | ||
provides: | ||
- email_address | ||
- goal: Send email | ||
skills: | ||
- SendEmail | ||
requires: | ||
- email_address | ||
- answer | ||
provides: | ||
"); | ||
|
||
public static Task RunAsync() | ||
{ | ||
return RunExampleAsync(); | ||
// return RunInteractiveAsync(); | ||
} | ||
|
||
public static async Task RunInteractiveAsync() | ||
{ | ||
var bingConnector = new BingConnector(TestConfiguration.Bing.ApiKey); | ||
var webSearchEngineSkill = new WebSearchEngineSkill(bingConnector); | ||
Dictionary<object, string?> skills = new() | ||
{ | ||
{ webSearchEngineSkill, "WebSearch" }, | ||
{ new TimeSkill(), "time" } | ||
}; | ||
|
||
FlowPlanner planner = new(GetKernelBuilder(), new FlowStatusProvider(new VolatileMemoryStore()), skills); | ||
var sessionId = Guid.NewGuid().ToString(); | ||
|
||
Console.WriteLine("*****************************************************"); | ||
Stopwatch sw = new(); | ||
sw.Start(); | ||
Console.WriteLine("Flow: " + s_flow.Name); | ||
SKContext? result = null; | ||
string? input = null;// "Execute the flow";// can this be empty? | ||
do | ||
{ | ||
if (result is not null) | ||
{ | ||
Console.WriteLine("Assistant: " + result.Result); | ||
} | ||
|
||
if (input is null) | ||
{ | ||
input = string.Empty; | ||
} | ||
else if (string.IsNullOrEmpty(input)) | ||
{ | ||
Console.WriteLine("User: "); | ||
input = Console.ReadLine() ?? string.Empty; | ||
} | ||
|
||
result = await planner.ExecuteFlowAsync(s_flow, sessionId, input); | ||
} while (!string.IsNullOrEmpty(result.Result) && result.Result != "[]"); | ||
|
||
Console.WriteLine("Assistant: " + result.Variables["answer"]); | ||
|
||
Console.WriteLine("Time Taken: " + sw.Elapsed); | ||
Console.WriteLine("*****************************************************"); | ||
} | ||
|
||
public static async Task RunExampleAsync() | ||
{ | ||
var bingConnector = new BingConnector(TestConfiguration.Bing.ApiKey); | ||
var webSearchEngineSkill = new WebSearchEngineSkill(bingConnector); | ||
Dictionary<object, string?> skills = new() | ||
{ | ||
{ webSearchEngineSkill, "WebSearch" }, | ||
{ new TimeSkill(), "time" } | ||
}; | ||
|
||
FlowPlanner planner = new(GetKernelBuilder(), new FlowStatusProvider(new VolatileMemoryStore()), skills); | ||
var sessionId = Guid.NewGuid().ToString(); | ||
|
||
Console.WriteLine("*****************************************************"); | ||
Stopwatch sw = new(); | ||
sw.Start(); | ||
Console.WriteLine("Flow: " + s_flow.Name); | ||
var result = await planner.ExecuteFlowAsync(s_flow, sessionId, "Execute the flow"); | ||
Console.WriteLine("Assistant: " + result.Result); | ||
Console.WriteLine("\tAnswer: " + result.Variables["answer"]); | ||
|
||
string input = "my email is bad*email&address"; | ||
Console.WriteLine($"User: {input}"); | ||
result = await planner.ExecuteFlowAsync(s_flow, sessionId, input); | ||
Console.WriteLine("Assistant: " + result.Result); | ||
|
||
input = "my email is sample@xyz.com"; | ||
Console.WriteLine($"User: {input}"); | ||
result = await planner.ExecuteFlowAsync(s_flow, sessionId, input); | ||
Console.WriteLine("\tEmail Address: " + result.Variables["email_address"]); | ||
Console.WriteLine("\tEmail Payload: " + result.Variables["email"]); | ||
|
||
Console.WriteLine("Time Taken: " + sw.Elapsed); | ||
Console.WriteLine("*****************************************************"); | ||
} | ||
|
||
private static KernelBuilder GetKernelBuilder() | ||
{ | ||
var builder = new KernelBuilder(); | ||
builder.WithAzureChatCompletionService( | ||
TestConfiguration.AzureOpenAI.ChatDeploymentName, | ||
TestConfiguration.AzureOpenAI.Endpoint, | ||
TestConfiguration.AzureOpenAI.ApiKey, | ||
alsoAsTextCompletion: true, | ||
setAsDefault: true); | ||
|
||
return builder | ||
.Configure(c => c.SetDefaultHttpRetryConfig(new HttpRetryConfig | ||
{ | ||
MaxRetryCount = 3, | ||
UseExponentialBackoff = true, | ||
MinRetryDelay = TimeSpan.FromSeconds(3), | ||
})); | ||
} | ||
|
||
public sealed class CollectEmailSkill : ChatSkill | ||
{ | ||
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. | ||
"; | ||
|
||
private readonly IChatCompletion _chat; | ||
|
||
private int MaxTokens { get; set; } = 256; | ||
|
||
private readonly ChatRequestSettings _chatRequestSettings; | ||
|
||
public CollectEmailSkill(IKernel kernel) | ||
{ | ||
this._chat = kernel.GetService<IChatCompletion>(); | ||
this._chatRequestSettings = new ChatRequestSettings | ||
{ | ||
MaxTokens = this.MaxTokens, | ||
StopSequences = new List<string>() { "Observation:" }, | ||
Temperature = 0 | ||
}; | ||
} | ||
|
||
[SKFunction] | ||
[Description("This function is used to prompt user to provide a valid email address.")] | ||
[SKName("CollectEmailAddress")] | ||
public async Task<string> CollectEmailAsync( | ||
[SKName("email")] string email, | ||
SKContext context) | ||
{ | ||
var chat = this._chat.CreateNewChat(SystemPrompt); | ||
chat.AddUserMessage(Goal); | ||
|
||
ChatHistory? chatHistory = this.GetChatHistory(context); | ||
if (chatHistory?.Any() ?? false) | ||
{ | ||
chat.Messages.AddRange(chatHistory); | ||
} | ||
|
||
if (!string.IsNullOrEmpty(email) && IsValidEmail(email)) | ||
{ | ||
context.Variables["email_address"] = email; | ||
|
||
return "Thanks for providing the info, the following email would be used in subsequent steps: " + email; | ||
} | ||
|
||
context.Variables["email_address"] = string.Empty; | ||
this.PromptInput(context); | ||
|
||
return await this._chat.GenerateMessageAsync(chat, this._chatRequestSettings).ConfigureAwait(false); | ||
} | ||
|
||
private static bool IsValidEmail(string email) | ||
{ | ||
// check using regex | ||
var regex = new Regex(EmailRegex); | ||
return regex.IsMatch(email); | ||
} | ||
} | ||
|
||
public sealed class SendEmailSkill | ||
{ | ||
[SKFunction] | ||
[Description("Send email")] | ||
[SKName("SendEmail")] | ||
public string SendEmail( | ||
[SKName("email_address")] string emailAddress, | ||
[SKName("answer")] string answer, | ||
SKContext context) | ||
{ | ||
var contract = new Email() | ||
{ | ||
Address = emailAddress, | ||
Content = answer, | ||
}; | ||
|
||
// for demo purpose only | ||
string emailPayload = JsonSerializer.Serialize(contract, new JsonSerializerOptions() { WriteIndented = true }); | ||
context.Variables["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; } | ||
} | ||
} | ||
} | ||
//***************************************************** | ||
//Flow: FlowPlanner_Example_Flow | ||
//Assistant: ["Please provide a valid email address in the following format: example@example.com"] | ||
// Answer: The current president of the United States is Joe Biden. His current age divided by 2 is 39. | ||
//User: my email is bad*email&address | ||
//Assistant: ["The email address you provided is not valid. Please provide a valid email address in the following format: example@example.com"] | ||
//User: my email is sample@xyz.com | ||
// Email Address: The collected email address is sample@xyz.com. | ||
// Email Payload: { | ||
// "Address": "sample@xyz.com", | ||
// "Content": "The current president of the United States is Joe Biden. His current age divided by 2 is 39." | ||
//} | ||
//Time Taken: 00:01:05.3375146 | ||
//***************************************************** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So is the idea that while result.Result is not empty, that message is passed to the user, and user message is fed back into the flow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Here is a high level example to utilize the planner is having a web api which accepts user input and return response.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note for self: stepasync/invokeasync and/or enabling functions in a plan to have control over execution state.