Skip to content
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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<PackageVersion Include="protobuf-net" Version="3.2.26" />
<PackageVersion Include="protobuf-net.Reflection" Version="3.2.12" />
<PackageVersion Include="CoreCLR-NCalc" Version="2.2.113" />
<PackageVersion Include="YamlDotNet" Version="13.1.1" />
<!-- Symbols -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<!-- Analyzers -->
Expand Down
9 changes: 9 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationInsightsExample"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.Kusto", "src\Connectors\Connectors.Memory.Kusto\Connectors.Memory.Kusto.csproj", "{E07608CC-D710-4655-BB9E-D22CF3CDD193}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Planning.FlowPlanner", "src\Extensions\Planning.FlowPlanner\Planning.FlowPlanner.csproj", "{B2E86D65-D94B-43B5-A38B-59DF2B90CC5E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -369,6 +371,12 @@ Global
{E07608CC-D710-4655-BB9E-D22CF3CDD193}.Publish|Any CPU.Build.0 = Debug|Any CPU
{E07608CC-D710-4655-BB9E-D22CF3CDD193}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E07608CC-D710-4655-BB9E-D22CF3CDD193}.Release|Any CPU.Build.0 = Release|Any CPU
{B2E86D65-D94B-43B5-A38B-59DF2B90CC5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2E86D65-D94B-43B5-A38B-59DF2B90CC5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2E86D65-D94B-43B5-A38B-59DF2B90CC5E}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
{B2E86D65-D94B-43B5-A38B-59DF2B90CC5E}.Publish|Any CPU.Build.0 = Debug|Any CPU
{B2E86D65-D94B-43B5-A38B-59DF2B90CC5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2E86D65-D94B-43B5-A38B-59DF2B90CC5E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -422,6 +430,7 @@ Global
{4762BCAF-E1C5-4714-B88D-E50FA333C50E} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
{C754950A-E16C-4F96-9CC7-9328E361B5AF} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{E07608CC-D710-4655-BB9E-D22CF3CDD193} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{B2E86D65-D94B-43B5-A38B-59DF2B90CC5E} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
Expand Down
272 changes: 272 additions & 0 deletions dotnet/samples/KernelSyntaxExamples/Example55_FlowPlanner.cs
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:
- email
");

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");
Copy link
Member

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?

Copy link
Contributor Author

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.

[Route("complete/{userInput}")]
public async Task<string[]> Complete(string userInput)
{
  Flow flow = this.GetActiveFlowFromStorage(userContext);
  if (flow == null)
  {
    flow = this.SelectNextFlow(userInput)
  }
  
  var result = await planner.ExecuteFlowAsync(flow, userInput).ConfigureAwait(false);
  if (result.isComplete)
  {
     // mark this as complete in storage
  }

  return JsonSerializer.Deserialize<string[]>(result.Result); 
}

Copy link
Member

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.

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
//*****************************************************
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Weaviate\Connectors.Memory.Weaviate.csproj" />
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Redis\Connectors.Memory.Redis.csproj" />
<ProjectReference Include="..\..\src\Extensions\Planning.ActionPlanner\Planning.ActionPlanner.csproj" />
<ProjectReference Include="..\..\src\Extensions\Planning.FlowPlanner\Planning.FlowPlanner.csproj" />
<ProjectReference Include="..\..\src\Extensions\Planning.SequentialPlanner\Planning.SequentialPlanner.csproj" />
<ProjectReference Include="..\..\src\Extensions\Planning.StepwisePlanner\Planning.StepwisePlanner.csproj" />
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Pinecone\Connectors.Memory.Pinecone.csproj" />
Expand Down
1 change: 1 addition & 0 deletions dotnet/samples/KernelSyntaxExamples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public static async Task Main()
await Example52_ApimAuth.RunAsync().SafeWaitAsync(cancelToken);
await Example53_Kusto.RunAsync().SafeWaitAsync(cancelToken);
await Example54_AzureChatCompletionWithData.RunAsync().SafeWaitAsync(cancelToken);
await Example55_FlowPlanner.RunAsync().SafeWaitAsync(cancelToken);
}

private static void LoadUserSecrets()
Expand Down
3 changes: 2 additions & 1 deletion dotnet/samples/KernelSyntaxExamples/TestConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ private static T LoadSection<T>([CallerMemberName] string? caller = null)
{
throw new ArgumentNullException(nameof(caller));
}

return s_instance._configRoot.GetSection(caller).Get<T>() ??
throw new ConfigurationNotFoundException(section: caller);
throw new ConfigurationNotFoundException(section: caller);
}

#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>SemanticKernel.Extensions.UnitTests</AssemblyName>
Expand Down Expand Up @@ -29,8 +29,18 @@

<ItemGroup>
<ProjectReference Include="..\Planning.ActionPlanner\Planning.ActionPlanner.csproj" />
<ProjectReference Include="..\Planning.FlowPlanner\Planning.FlowPlanner.csproj" />
<ProjectReference Include="..\Planning.SequentialPlanner\Planning.SequentialPlanner.csproj" />
<ProjectReference Include="..\Planning.StepwisePlanner\Planning.StepwisePlanner.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="Planning\**\*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Planning\**\*.yml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Loading
Loading