chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
namespace Events;
|
||||
|
||||
/// <summary>
|
||||
/// Processes Events emitted by shared steps.<br/>
|
||||
/// </summary>
|
||||
public static class CommonEvents
|
||||
{
|
||||
public static readonly string UserInputReceived = nameof(UserInputReceived);
|
||||
public static readonly string UserInputComplete = nameof(UserInputComplete);
|
||||
public static readonly string AssistantResponseGenerated = nameof(AssistantResponseGenerated);
|
||||
public static readonly string Exit = nameof(Exit);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>GettingStartedWithProcesses</AssemblyName>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace></RootNamespace>
|
||||
<!-- Suppress: "Declare types in namespaces", "Require ConfigureAwait", "Experimental" -->
|
||||
<NoWarn>
|
||||
$(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0080,SKEXP0081,SKEXP0101,SKEXP0110,OPENAI001
|
||||
</NoWarn>
|
||||
<OutputType>Library</OutputType>
|
||||
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="PuppeteerSharp" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.abstractions" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/src/Schema/*.cs" Link="%(RecursiveDir)/InternalUtilities/Schema/%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Agents\AzureAI\Agents.AzureAI.csproj" />
|
||||
<ProjectReference Include="..\..\src\Agents\Core\Agents.Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\Agents\OpenAI\Agents.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\src\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\src\Experimental\Process.Abstractions\Process.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\src\Experimental\Process.Core\Process.Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\Experimental\Process.LocalRuntime\Process.LocalRuntime.csproj" />
|
||||
<ProjectReference Include="..\..\src\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\src\SemanticKernel.Core\SemanticKernel.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="Xunit.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="$(RepoRoot)/dotnet/samples/LearnResources/Resources/Grimms-The-King-of-the-Golden-Mountain.txt" Link="%(RecursiveDir)Resources/%(Filename)%(Extension)" />
|
||||
<EmbeddedResource Include="$(RepoRoot)/dotnet/samples/LearnResources/Resources/Grimms-The-Water-of-Life.txt" Link="%(RecursiveDir)Resources/%(Filename)%(Extension)" />
|
||||
<EmbeddedResource Include="$(RepoRoot)/dotnet/samples/LearnResources/Resources/Grimms-The-White-Snake.txt" Link="%(RecursiveDir)Resources/%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,488 @@
|
||||
# Semantic Kernel Processes - Getting Started
|
||||
|
||||
This project contains a step by step guide to get started with _Semantic Kernel Processes_.
|
||||
|
||||
|
||||
#### NuGet:
|
||||
- [Microsoft.SemanticKernel.Process.Abstractions](https://www.nuget.org/packages/Microsoft.SemanticKernel.Process.Abstractions)
|
||||
- [Microsoft.SemanticKernel.Process.Core](https://www.nuget.org/packages/Microsoft.SemanticKernel.Process.Core)
|
||||
- [Microsoft.SemanticKernel.Process.LocalRuntime](https://www.nuget.org/packages/Microsoft.SemanticKernel.Process.LocalRuntime)
|
||||
|
||||
#### Sources
|
||||
- [Semantic Kernel Processes - Abstractions](https://github.com/microsoft/semantic-kernel/tree/main/dotnet/src/Experimental/Process.Abstractions)
|
||||
- [Semantic Kernel Processes - Core](https://github.com/microsoft/semantic-kernel/tree/main/dotnet/src/Experimental/Process.Core)
|
||||
- [Semantic Kernel Processes - LocalRuntime](https://github.com/microsoft/semantic-kernel/tree/main/dotnet/src/Experimental/Process.LocalRuntime)
|
||||
|
||||
The examples can be run as integration tests but their code can also be copied to stand-alone programs.
|
||||
|
||||
## Examples
|
||||
|
||||
The getting started with agents examples include:
|
||||
|
||||
Example|Description
|
||||
---|---
|
||||
[Step00_Processes](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step00/Step00_Processes.cs)|How to create the simplest process with minimal code and event wiring
|
||||
[Step01_Processes](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step01/Step01_Processes.cs)|How to create a simple process with a loop and a conditional exit
|
||||
[Step02a_AccountOpening](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step02/Step02a_AccountOpening.cs)|Showcasing processes cycles, fan in, fan out for opening an account.
|
||||
[Step02b_AccountOpening](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step02/Step02b_AccountOpening.cs)|How to refactor processes and make use of smaller processes as steps in larger processes.
|
||||
[Step03a_FoodPreparation](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs)|Showcasing reuse of steps, creation of processes, spawning of multiple events, use of stateful steps with food preparation samples.
|
||||
[Step03b_FoodOrdering](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step03/Step03b_FoodOrdering.cs)|Showcasing use of subprocesses as steps, spawning of multiple events conditionally reusing the food preparation samples.
|
||||
[Step04_AgentOrchestration](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step04/Step04_AgentOrchestration.cs)|Showcasing use of process steps in conjunction with the _Agent Framework_.
|
||||
|
||||
### Step00_Processes
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Start(Start)--> DoSomeWork(DoSomeWork)
|
||||
DoSomeWork--> DoMoreWork(DoMoreWork)
|
||||
DoMoreWork--> End(End)
|
||||
```
|
||||
|
||||
### Step01_Processes
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Intro(Intro)--> UserInput(User Input)
|
||||
UserInput-->|User message == 'exit'| Exit(Exit)
|
||||
UserInput-->|User message| AssistantResponse(Assistant Response)
|
||||
AssistantResponse--> UserInput
|
||||
```
|
||||
|
||||
### Step02_AccountOpening
|
||||
|
||||
The account opening sample has 2 different implementations covering the same scenario, it just uses different SK components to achieve the same goal.
|
||||
|
||||
In addition, the sample introduces the concept of using smaller process as steps to maintain the main process readable and manageble for future improvements and unit testing.
|
||||
Also introduces the use of SK Event Subscribers.
|
||||
|
||||
A process for opening an account for this sample has the following steps:
|
||||
- Fill New User Account Application Form
|
||||
- Verify Applicant Credit Score
|
||||
- Apply Fraud Detection Analysis to the Application Form
|
||||
- Create New Entry in Core System Records
|
||||
- Add new account to Marketing Records
|
||||
- CRM Record Creation
|
||||
- Mail user a user a notification about:
|
||||
- Failure to open a new account due to Credit Score Check
|
||||
- Failure to open a new account due to Fraud Detection Alert
|
||||
- Welcome package including new account details
|
||||
|
||||
A SK process that only connects the steps listed above as is (no use of subprocesses as steps) for opening an account look like this:
|
||||
|
||||
#### Step02a_AccountOpening
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
User(User) -->|Provides user details| FillForm(Fill New <br/> Customer <br/> Form)
|
||||
|
||||
FillForm -->|Need more info| AssistantMessage(Assistant <br/> Message)
|
||||
FillForm -->|Welcome Message| AssistantMessage
|
||||
FillForm --> CompletedForm((Completed Form))
|
||||
AssistantMessage --> User
|
||||
|
||||
CompletedForm --> CreditCheck(Customer <br/> Credit Score <br/> Check)
|
||||
CompletedForm --> Fraud(Fraud Detection)
|
||||
CompletedForm -->|New Customer Form + Conversation Transcript| CoreSystem
|
||||
|
||||
CreditCheck -->|Failed - Notify user about insufficient credit score| Mailer(Mail <br/> Service)
|
||||
CreditCheck -->|Approved| Fraud
|
||||
|
||||
Fraud --> |Failed - Notify user about failure to confirm user identity| Mailer
|
||||
Fraud --> |Passed| CoreSystem(Core System <br/> Record <br/> Creation)
|
||||
|
||||
CoreSystem --> Marketing(New Marketing <br/> Record Creation)
|
||||
CoreSystem --> CRM(CRM Record <br/> Creation)
|
||||
CoreSystem -->|Account Details| Welcome(Welcome <br/> Packet)
|
||||
|
||||
Marketing -->|Success| Welcome
|
||||
CRM -->|Success| Welcome
|
||||
|
||||
Welcome -->|Success: Notify User about Account Creation| Mailer
|
||||
Mailer -->|End of Interaction| User
|
||||
```
|
||||
|
||||
#### Step02b_AccountOpening
|
||||
|
||||
After grouping steps that have a common theme/dependencies, and creating smaller subprocesses and using them as steps,
|
||||
the root process looks like this:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
User(User)
|
||||
FillForm(Chat With User <br/> to Fill New <br/> Customer Form)
|
||||
NewAccountVerification[[New Account Verification<br/> Process]]
|
||||
NewAccountCreation[[New Account Creation<br/> Process]]
|
||||
Mailer(Mail <br/> Service)
|
||||
|
||||
User<-->|Provides user details|FillForm
|
||||
FillForm-->|New User Form|NewAccountVerification
|
||||
NewAccountVerification-->|Account Credit Check<br/> Verification Failed|Mailer
|
||||
NewAccountVerification-->|Account Fraud<br/> Detection Failed|Mailer
|
||||
NewAccountVerification-->|Account Verification <br/> Succeeded|NewAccountCreation
|
||||
NewAccountCreation-->|Account Creation <br/> Succeeded|Mailer
|
||||
```
|
||||
|
||||
Where processes used as steps, which are reusing the same steps used [`Step02a_AccountOpening`](#step02a_accountopening), are:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
NewUserForm([New User Form])
|
||||
NewUserFormConv([Form Filling Interaction])
|
||||
|
||||
subgraph AccountCreation[Account Creation Process]
|
||||
direction LR
|
||||
AccountValidation([Account Verification Passed])
|
||||
NewUser1([New User Form])
|
||||
NewUserFormConv1([Form Filling Interaction])
|
||||
|
||||
CoreSystem(Core System <br/> Record <br/> Creation)
|
||||
Marketing(New Marketing <br/> Record Creation)
|
||||
CRM(CRM Record <br/> Creation)
|
||||
Welcome(Welcome <br/> Packet)
|
||||
NewAccountCreation([New Account Success])
|
||||
|
||||
NewUser1-->CoreSystem
|
||||
NewUserFormConv1-->CoreSystem
|
||||
|
||||
AccountValidation-->CoreSystem
|
||||
CoreSystem-->CRM-->|Success|Welcome
|
||||
CoreSystem-->Marketing-->|Success|Welcome
|
||||
CoreSystem-->|Account Details|Welcome
|
||||
|
||||
Welcome-->NewAccountCreation
|
||||
end
|
||||
|
||||
subgraph AccountVerification[Account Verification Process]
|
||||
direction LR
|
||||
NewUser2([New User Form])
|
||||
CreditScoreCheck[Credit Check <br/> Step]
|
||||
FraudCheck[Fraud Detection <br/> Step]
|
||||
AccountVerificationPass([Account Verification Passed])
|
||||
AccountCreditCheckFail([Credit Check Failed])
|
||||
AccountFraudCheckFail([Fraud Check Failed])
|
||||
|
||||
|
||||
NewUser2-->CreditScoreCheck-->|Credit Score <br/> Check Passed|FraudCheck
|
||||
FraudCheck-->AccountVerificationPass
|
||||
|
||||
CreditScoreCheck-->AccountCreditCheckFail
|
||||
FraudCheck-->AccountFraudCheckFail
|
||||
end
|
||||
|
||||
AccountVerificationPass-->AccountValidation
|
||||
NewUserForm-->NewUser1
|
||||
NewUserForm-->NewUser2
|
||||
NewUserFormConv-->NewUserFormConv1
|
||||
|
||||
```
|
||||
|
||||
### Step03a_FoodPreparation
|
||||
|
||||
This tutorial contains a set of food recipes associated with the Food Preparation Processes of a restaurant.
|
||||
|
||||
The following recipes for preparation of Order Items are defined as SK Processes:
|
||||
|
||||
#### Product Preparation Processes
|
||||
|
||||
##### Stateless Product Preparation Processes
|
||||
|
||||
###### Potato Fries Preparation Process
|
||||
|
||||
``` mermaid
|
||||
flowchart LR
|
||||
PreparePotatoFriesEvent([Prepare Potato <br/> Fries Event])
|
||||
PotatoFriesReadyEvent([Potato Fries <br/> Ready Event])
|
||||
|
||||
GatherIngredientsStep[Gather Ingredients <br/> Step]
|
||||
CutStep[Cut Food <br/> Step]
|
||||
FryStep[Fry Food <br/> Step]
|
||||
|
||||
PreparePotatoFriesEvent --> GatherIngredientsStep -->| Slice Potatoes <br/> _Ingredients Gathered_ | CutStep --> |**Potato Sliced Ready** <br/> _Food Sliced Ready_ | FryStep --> |_Fried Food Ready_|PotatoFriesReadyEvent
|
||||
FryStep -->|Fried Potato Ruined <br/> _Fried Food Ruined_| GatherIngredientsStep
|
||||
```
|
||||
|
||||
###### Fried Fish Preparation Process
|
||||
|
||||
``` mermaid
|
||||
flowchart LR
|
||||
PrepareFriedFishEvent([Prepare Fried <br/> Fish Event])
|
||||
FriedFishReadyEvent([Fried Fish <br/> Ready Event])
|
||||
|
||||
GatherIngredientsStep[Gather Ingredients <br/> Step]
|
||||
CutStep[Cut Food <br/> Step]
|
||||
FryStep[Fry Food <br/> Step]
|
||||
|
||||
PrepareFriedFishEvent --> GatherIngredientsStep -->| Chop Fish <br/> _Ingredients Gathered_ | CutStep --> |**Fish Chopped Ready** <br/> _Food Chopped Ready_| FryStep --> |_Fried Food Ready_ | FriedFishReadyEvent
|
||||
FryStep -->|**Fried Fish Ruined** <br/> _Fried Food Ruined_| GatherIngredientsStep
|
||||
```
|
||||
|
||||
###### Fish Sandwich Preparation Process
|
||||
|
||||
``` mermaid
|
||||
flowchart LR
|
||||
PrepareFishSandwichEvent([Prepare Fish <br/> Sandwich Event])
|
||||
FishSandwichReadyEvent([Fish Sandwich <br/> Ready Event])
|
||||
|
||||
FriedFishStep[[Fried Fish <br/> Process Step]]
|
||||
AddBunsStep[Add Buns <br/> Step]
|
||||
AddSpecialSauceStep[Add Special <br/> Sauce Step]
|
||||
|
||||
PrepareFishSandwichEvent -->|Prepare Fried Fish| FriedFishStep -->|Fried Fish Ready| AddBunsStep --> |Buns Added | AddSpecialSauceStep --> |Special Sauce Added | FishSandwichReadyEvent
|
||||
```
|
||||
|
||||
###### Fish And Chips Preparation Process
|
||||
|
||||
``` mermaid
|
||||
flowchart LR
|
||||
PrepareFishAndChipsEvent([Prepare <br/> Fish And Chips <br/> Event])
|
||||
FishAndChipsReadyEvent([Fish And Chips <br/> Ready Event])
|
||||
|
||||
FriedFishStep[[Fried Fish <br/> Process Step]]
|
||||
PotatoFriesStep[[Potato Fries <br/> Process Step]]
|
||||
AddCondiments[Add Condiments <br/> Step ]
|
||||
|
||||
PrepareFishAndChipsEvent -->|Prepare Fried Fish| FriedFishStep --> |Fried Fish Ready| AddCondiments
|
||||
PrepareFishAndChipsEvent -->|Prepare Potato Fries| PotatoFriesStep -->|Potato Fries Ready| AddCondiments
|
||||
AddCondiments -->|Condiments Added| FishAndChipsReadyEvent
|
||||
```
|
||||
|
||||
##### Stateful Product Preparation Processes
|
||||
|
||||
The processes in this subsection contain the following modifications/additions to previously used food preparation processes:
|
||||
|
||||
- The `Gather Ingredients Step` is now stateful and has a predefined number of initial ingredients that are used as orders are prepared. When there are no ingredients left, it emits the `Out of Stock Event`.
|
||||
- The `Cut Food Step` is now a stateful component which has a `Knife Sharpness State` that tracks the Knife Sharpness.
|
||||
- As the `Slice Food` and `Chop Food` Functions get invoked, the Knife Sharpness deteriorates.
|
||||
- The `Cut Food Step` has an additional input function `Sharpen Knife Function`.
|
||||
- The new `Sharpen Knife Function` sharpens the knife and increases the Knife Sharpness - Knife Sharpness State.
|
||||
- From time to time, the `Cut Food Step`'s functions `SliceFood` and `ChopFood` will fail and emit a `Knife Needs Sharpening Event` that then triggers the `Sharpen Knife Function`.
|
||||
|
||||
|
||||
###### Potato Fries Preparation With Knife Sharpening and Ingredient Stock Process
|
||||
|
||||
The following processes is a modification on the process [Potato Fries Preparation](#potato-fries-preparation-process)
|
||||
with the the stateful steps mentioned previously.
|
||||
|
||||
``` mermaid
|
||||
flowchart LR
|
||||
PreparePotatoFriesEvent([Prepare Potato <br/> Fries Event])
|
||||
PotatoFriesReadyEvent([Potato Fries <br/> Ready Event])
|
||||
OutOfStock([Ingredients <br/> Out of Stock <br/> Event])
|
||||
|
||||
FryStep[Fry Food <br/> Step]
|
||||
|
||||
subgraph GatherIngredientsStep[Gather Ingredients Step]
|
||||
GatherIngredientsFunction[Gather Potato <br/> Function]
|
||||
IngredientsState[(Ingredients <br/> Stock <br/> State)]
|
||||
end
|
||||
subgraph CutStep ["Cut Food Step"]
|
||||
direction LR
|
||||
SliceFoodFunction[Slice Food <br/> Function]
|
||||
SharpenKnifeFunction[Sharpen Knife <br/> Function]
|
||||
CutState[(Knife <br/> Sharpness <br/> State)]
|
||||
end
|
||||
|
||||
CutStep --> |**Potato Sliced Ready** <br/> _Food Sliced Ready_ | FryStep --> |_Fried Food Ready_|PotatoFriesReadyEvent
|
||||
FryStep -->|Fried Potato Ruined <br/> _Fried Food Ruined_| GatherIngredientsStep
|
||||
GatherIngredientsStep --> OutOfStock
|
||||
|
||||
SliceFoodFunction --> |Knife Needs Sharpening| SharpenKnifeFunction
|
||||
SharpenKnifeFunction --> |Knife Sharpened| SliceFoodFunction
|
||||
|
||||
GatherIngredientsStep -->| Slice Potatoes <br/> _Ingredients Gathered_ | CutStep
|
||||
PreparePotatoFriesEvent --> GatherIngredientsStep
|
||||
```
|
||||
|
||||
###### Fried Fish Preparation With Knife Sharpening and Ingredient Stock Process
|
||||
|
||||
The following process is a modification on the process [Fried Fish Preparation](#fried-fish-preparation-process)
|
||||
with the the stateful steps mentioned previously.
|
||||
|
||||
``` mermaid
|
||||
flowchart LR
|
||||
PrepareFriedFishEvent([Prepare Fried <br/> Fish Event])
|
||||
FriedFishReadyEvent([Fried Fish <br/> Ready Event])
|
||||
OutOfStock([Ingredients <br/> Out of Stock <br/> Event])
|
||||
|
||||
FryStep[Fry Food <br/> Step]
|
||||
|
||||
subgraph GatherIngredientsStep[Gather Ingredients Step]
|
||||
GatherIngredientsFunction[Gather Fish <br/> Function]
|
||||
IngredientsState[(Ingredients <br/> Stock <br/> State)]
|
||||
end
|
||||
subgraph CutStep ["Cut Food Step"]
|
||||
direction LR
|
||||
ChopFoodFunction[Chop Food <br/> Function]
|
||||
SharpenKnifeFunction[Sharpen Knife <br/> Function]
|
||||
CutState[(Knife <br/> Sharpness <br/> State)]
|
||||
end
|
||||
|
||||
CutStep --> |**Fish Chopped Ready** <br/> _Food Chopped Ready_| FryStep --> |_Fried Food Ready_|FriedFishReadyEvent
|
||||
FryStep -->|**Fried Fish Ruined** <br/> _Fried Food Ruined_| GatherIngredientsStep
|
||||
GatherIngredientsStep --> OutOfStock
|
||||
|
||||
ChopFoodFunction --> |Knife Needs Sharpening| SharpenKnifeFunction
|
||||
SharpenKnifeFunction --> |Knife Sharpened| ChopFoodFunction
|
||||
|
||||
GatherIngredientsStep -->| Chop Fish <br/> _Ingredients Gathered_ | CutStep
|
||||
PrepareFriedFishEvent --> GatherIngredientsStep
|
||||
```
|
||||
|
||||
### Step03b_FoodOrdering
|
||||
|
||||
#### Single Order Preparation Process
|
||||
|
||||
Now with the existing product preparation processes, they can be used to create an even more complex process that can decide what product order to dispatch.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
PrepareSingleOrderEvent([Prepare Single Order <br/> Event])
|
||||
SingleOrderReadyEvent([Single Order <br/> Ready Event])
|
||||
OrderPackedEvent([Order Packed <br/> Event])
|
||||
|
||||
DispatchOrderStep{{Dispatch <br/> Order Step}}
|
||||
FriedFishStep[[Fried Fish <br/> Process Step]]
|
||||
PotatoFriesStep[[Potato Fries <br/> Process Step]]
|
||||
FishSandwichStep[[Fish Sandwich <br/> Process Step]]
|
||||
FishAndChipsStep[[Fish & Chips <br/> Process Step]]
|
||||
|
||||
PackFoodStep[Pack Food <br/> Step]
|
||||
|
||||
PrepareSingleOrderEvent -->|Order Received| DispatchOrderStep
|
||||
DispatchOrderStep -->|Prepare Fried Fish| FriedFishStep -->|Fried Fish Ready| SingleOrderReadyEvent
|
||||
DispatchOrderStep -->|Prepare Potato Fries| PotatoFriesStep -->|Potato Fries Ready| SingleOrderReadyEvent
|
||||
DispatchOrderStep -->|Prepare Fish Sandwich| FishSandwichStep -->|Fish Sandwich Ready| SingleOrderReadyEvent
|
||||
DispatchOrderStep -->|Prepare Fish & Chips| FishAndChipsStep -->|Fish & Chips Ready| SingleOrderReadyEvent
|
||||
|
||||
SingleOrderReadyEvent-->PackFoodStep --> OrderPackedEvent
|
||||
```
|
||||
|
||||
### Step04_AgentOrchestration
|
||||
|
||||
This tutorial demonstrates integrating the _Agent Framework_ with processes.
|
||||
This includes both direct _agent_ interaction as well as making use of _AgentGroupChat_.
|
||||
|
||||
```mermaid
|
||||
flowchart RL
|
||||
O --> A
|
||||
O((Start))
|
||||
A[User] -->|input| B[ManagerAgent]
|
||||
A --> F((Done))
|
||||
B --> |response|A
|
||||
B --> |delegate| G
|
||||
G --> |response|B
|
||||
subgraph G[GroupChat]
|
||||
direction LR
|
||||
D[Agent1] --> E
|
||||
E[Agent2] --> D
|
||||
end
|
||||
```
|
||||
|
||||
## Concepts
|
||||
|
||||
### Components
|
||||
|
||||
- **Process:** A sequence of steps designed to achieve a specific goal. These steps are interconnected in such a way that they can communicate by sending and receiving events. The connections between the steps are established during the process creation.
|
||||
- **Steps:** Individual activities within a process, each with defined inputs and outputs, contributing to the overall objective. Existing processes can be utilized as steps within another process. There are two main types of steps:
|
||||
- _Stateless Steps_: These steps do not retain any information between executions. They operate independently without the need to store state data.
|
||||
- _Stateful Steps_: These steps maintain a state that can be persisted, allowing the state to be reused and updated in subsequent runs of the process. The state of these steps can be stored and serialized.
|
||||
|
||||
In general, both processes and steps are designed to be reusable across different processes.
|
||||
|
||||
### Versioning
|
||||
|
||||
Once stateful steps/processes have been deployed, versioning becomes a crucial aspect to understand.
|
||||
It enables you to tweak and improve processes while maintaining the ability to read step states generated by previous versions of the steps.
|
||||
|
||||
Stateful processes involve steps that maintain state information.
|
||||
When these processes are updated, it's important to manage versioning effectively to ensure continuity and compatibility with previously saved states.
|
||||
|
||||
There are two primary scenarios to consider when addressing process state versioning:
|
||||
|
||||
1. **Minor SK Process Improvements/Changes:**
|
||||
|
||||
In this scenario, the root process remains conceptually the same, but with some modifications:
|
||||
|
||||
- _Step Renaming:_ Some step names may have been changed.
|
||||
- _Step Version Updates:_ New versions of one or more steps used by the root process or any steps in a subprocess may be introduced.
|
||||
|
||||
**Considerations:**
|
||||
|
||||
- Ensure backward compatibility by mapping old step names to new step names.
|
||||
- Validate that the new step versions can read and interpret the state data generated by previous versions.
|
||||
|
||||
**Related Samples:**
|
||||
|
||||
- `Step03a_FoodPreparation.cs/RunStatefulFriedFishV2ProcessWithLowStockV1StateFromFileAsync`
|
||||
- `Step03a_FoodPreparation.cs/RunStatefulFishSandwichV2ProcessWithLowStockV1StateFromFileAsync`
|
||||
|
||||
2. **Major SK Process Improvements/Changes:**
|
||||
|
||||
This scenario involves significant modifications to the root process, which may include:
|
||||
|
||||
- _Step Refactoring_: Multiple steps may be refactored and replaced. However, some properties of the replaced steps can be used to set properties of the new steps.
|
||||
- _Custom Mappings:_ Custom equivalent mappings may be required to translate the previous stored state to the current process state.
|
||||
|
||||
**Considerations:**
|
||||
|
||||
- Develop a detailed mapping strategy to align old and new process states.
|
||||
- Implement and test custom mappings to ensure data integrity and process continuity.
|
||||
|
||||
## Running Examples with Filters
|
||||
Examples may be explored and ran within _Visual Studio_ using _Test Explorer_.
|
||||
|
||||
You can also run specific examples via the command-line by using test filters (`dotnet test --filter`). Type `dotnet test --help` at the command line for more details.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
dotnet test --filter Step01_Processes
|
||||
```
|
||||
|
||||
## Configuring Secrets
|
||||
|
||||
Each example requires secrets / credentials to access OpenAI or Azure OpenAI.
|
||||
|
||||
We suggest using .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) to avoid the risk of leaking secrets into the repository, branches and pull requests. You can also use environment variables if you prefer.
|
||||
|
||||
To set your secrets with .NET Secret Manager:
|
||||
|
||||
1. Navigate the console to the project folder:
|
||||
|
||||
```
|
||||
cd dotnet/samples/GettingStartedWithProcesses
|
||||
```
|
||||
|
||||
2. Examine existing secret definitions:
|
||||
|
||||
```
|
||||
dotnet user-secrets list
|
||||
```
|
||||
|
||||
3. If needed, perform first time initialization:
|
||||
|
||||
```
|
||||
dotnet user-secrets init
|
||||
```
|
||||
|
||||
4. Define secrets for either Open AI:
|
||||
|
||||
```
|
||||
dotnet user-secrets set "OpenAI:ChatModelId" "..."
|
||||
dotnet user-secrets set "OpenAI:ApiKey" "..."
|
||||
```
|
||||
|
||||
5. Or Azure Open AI:
|
||||
|
||||
```
|
||||
dotnet user-secrets set "AzureOpenAI:DeploymentName" "..."
|
||||
dotnet user-secrets set "AzureOpenAI:ChatDeploymentName" "..."
|
||||
dotnet user-secrets set "AzureOpenAI:Endpoint" "https://... .openai.azure.com/"
|
||||
dotnet user-secrets set "AzureOpenAI:ApiKey" "..."
|
||||
```
|
||||
|
||||
> NOTE: Azure secrets will take precedence, if both Open AI and Azure Open AI secrets are defined, unless `ForceOpenAI` is set:
|
||||
|
||||
```
|
||||
protected override bool ForceOpenAI => true;
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Events;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace SharedSteps;
|
||||
|
||||
/// <summary>
|
||||
/// Step used in the Processes Samples:
|
||||
/// - Step_02_AccountOpening.cs
|
||||
/// </summary>
|
||||
public class DisplayAssistantMessageStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string DisplayAssistantMessage = nameof(DisplayAssistantMessage);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.DisplayAssistantMessage)]
|
||||
public async ValueTask DisplayAssistantMessageAsync(KernelProcessStepContext context, string assistantMessage)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Blue;
|
||||
Console.WriteLine($"ASSISTANT: {assistantMessage}\n");
|
||||
Console.ResetColor();
|
||||
|
||||
// Emit the assistantMessageGenerated
|
||||
await context.EmitEventAsync(new() { Id = CommonEvents.AssistantResponseGenerated, Data = assistantMessage });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Events;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace SharedSteps;
|
||||
|
||||
/// <summary>
|
||||
/// A step that elicits user input.
|
||||
///
|
||||
/// Step used in the Processes Samples:
|
||||
/// - Step01_Processes.cs
|
||||
/// - Step02_AccountOpening.cs
|
||||
/// - Step04_AgentOrchestration
|
||||
/// </summary>
|
||||
public class ScriptedUserInputStep : KernelProcessStep<UserInputState>
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string GetUserInput = nameof(GetUserInput);
|
||||
}
|
||||
|
||||
protected bool SuppressOutput { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The state object for the user input step. This object holds the user inputs and the current input index.
|
||||
/// </summary>
|
||||
private UserInputState? _state;
|
||||
|
||||
/// <summary>
|
||||
/// Method to be overridden by the user to populate with custom user messages
|
||||
/// </summary>
|
||||
/// <param name="state">The initialized state object for the step.</param>
|
||||
public virtual void PopulateUserInputs(UserInputState state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the user input step by initializing the state object. This method is called when the process is started
|
||||
/// and before any of the KernelFunctions are invoked.
|
||||
/// </summary>
|
||||
/// <param name="state">The state object for the step.</param>
|
||||
/// <returns>A <see cref="ValueTask"/></returns>
|
||||
public override ValueTask ActivateAsync(KernelProcessStepState<UserInputState> state)
|
||||
{
|
||||
_state = state.State;
|
||||
|
||||
PopulateUserInputs(_state!);
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
internal string GetNextUserMessage()
|
||||
{
|
||||
if (_state != null && _state.CurrentInputIndex >= 0 && _state.CurrentInputIndex < this._state.UserInputs.Count)
|
||||
{
|
||||
var userMessage = this._state!.UserInputs[_state.CurrentInputIndex];
|
||||
_state.CurrentInputIndex++;
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"USER: {userMessage}");
|
||||
Console.ResetColor();
|
||||
|
||||
return userMessage;
|
||||
}
|
||||
|
||||
Console.WriteLine("SCRIPTED_USER_INPUT: No more scripted user messages defined, returning empty string as user message");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user input.
|
||||
/// Could be overridden to customize the output events to be emitted
|
||||
/// </summary>
|
||||
/// <param name="context">An instance of <see cref="KernelProcessStepContext"/> which can be
|
||||
/// used to emit events from within a KernelFunction.</param>
|
||||
/// <returns>A <see cref="ValueTask"/></returns>
|
||||
[KernelFunction(ProcessStepFunctions.GetUserInput)]
|
||||
public virtual async ValueTask GetUserInputAsync(KernelProcessStepContext context)
|
||||
{
|
||||
var userMessage = this.GetNextUserMessage();
|
||||
// Emit the user input
|
||||
if (string.IsNullOrEmpty(userMessage))
|
||||
{
|
||||
await context.EmitEventAsync(new() { Id = CommonEvents.Exit });
|
||||
return;
|
||||
}
|
||||
|
||||
await context.EmitEventAsync(new() { Id = CommonEvents.UserInputReceived, Data = userMessage });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The state object for the <see cref="ScriptedUserInputStep"/>
|
||||
/// </summary>
|
||||
public record UserInputState
|
||||
{
|
||||
public List<string> UserInputs { get; init; } = [];
|
||||
|
||||
public int CurrentInputIndex { get; set; } = 0;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step00.Steps;
|
||||
|
||||
namespace Step00;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate creation of the simplest <see cref="KernelProcess"/> and
|
||||
/// eliciting its response to three explicit user messages.
|
||||
/// </summary>
|
||||
public class Step00_Processes(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true)
|
||||
{
|
||||
public static class ProcessEvents
|
||||
{
|
||||
public const string StartProcess = nameof(StartProcess);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates the creation of the simplest possible process with multiple steps
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/></returns>
|
||||
[Fact]
|
||||
public async Task UseSimplestProcessAsync()
|
||||
{
|
||||
// Create a simple kernel
|
||||
Kernel kernel = Kernel.CreateBuilder()
|
||||
.Build();
|
||||
|
||||
// Create a process that will interact with the chat completion service
|
||||
ProcessBuilder process = new("ChatBot");
|
||||
var startStep = process.AddStepFromType<StartStep>();
|
||||
var doSomeWorkStep = process.AddStepFromType<DoSomeWorkStep>();
|
||||
var doMoreWorkStep = process.AddStepFromType<DoMoreWorkStep>();
|
||||
var lastStep = process.AddStepFromType<LastStep>();
|
||||
|
||||
// Define the process flow
|
||||
process
|
||||
.OnInputEvent(ProcessEvents.StartProcess)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(startStep));
|
||||
|
||||
startStep
|
||||
.OnFunctionResult()
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(doSomeWorkStep));
|
||||
|
||||
doSomeWorkStep
|
||||
.OnFunctionResult()
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(doMoreWorkStep));
|
||||
|
||||
doMoreWorkStep
|
||||
.OnFunctionResult()
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(lastStep));
|
||||
|
||||
lastStep
|
||||
.OnFunctionResult()
|
||||
.StopProcess();
|
||||
|
||||
// Build the process to get a handle that can be started
|
||||
KernelProcess kernelProcess = process.Build();
|
||||
|
||||
// Start the process with an initial external event
|
||||
await using var runningProcess = await kernelProcess.StartAsync(
|
||||
kernel,
|
||||
new KernelProcessEvent()
|
||||
{
|
||||
Id = ProcessEvents.StartProcess,
|
||||
Data = null
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace Step00.Steps;
|
||||
|
||||
public sealed class DoMoreWorkStep : KernelProcessStep
|
||||
{
|
||||
[KernelFunction]
|
||||
public async ValueTask ExecuteAsync(KernelProcessStepContext context)
|
||||
{
|
||||
Console.WriteLine("Step 3 - Doing Yet More Work...\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace Step00.Steps;
|
||||
|
||||
public sealed class DoSomeWorkStep : KernelProcessStep
|
||||
{
|
||||
[KernelFunction]
|
||||
public async ValueTask ExecuteAsync(KernelProcessStepContext context)
|
||||
{
|
||||
Console.WriteLine("Step 2 - Doing Some Work...\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace Step00.Steps;
|
||||
|
||||
public sealed class LastStep : KernelProcessStep
|
||||
{
|
||||
[KernelFunction]
|
||||
public async ValueTask ExecuteAsync(KernelProcessStepContext context)
|
||||
{
|
||||
Console.WriteLine("Step 4 - This is the Final Step...\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace Step00.Steps;
|
||||
|
||||
public sealed class StartStep : KernelProcessStep
|
||||
{
|
||||
[KernelFunction]
|
||||
public async ValueTask ExecuteAsync(KernelProcessStepContext context)
|
||||
{
|
||||
Console.WriteLine("Step 1 - Start\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Events;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Process.Tools;
|
||||
using SharedSteps;
|
||||
using Utilities;
|
||||
|
||||
namespace Step01;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate creation of <see cref="KernelProcess"/> and
|
||||
/// eliciting its response to three explicit user messages.
|
||||
/// </summary>
|
||||
public class Step01_Processes(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true)
|
||||
{
|
||||
/// <summary>
|
||||
/// Demonstrates the creation of a simple process that has multiple steps, takes
|
||||
/// user input, interacts with the chat completion service, and demonstrates cycles
|
||||
/// in the process.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/></returns>
|
||||
[Fact]
|
||||
public async Task UseSimpleProcessAsync()
|
||||
{
|
||||
// Create a kernel with a chat completion service
|
||||
Kernel kernel = Kernel.CreateBuilder()
|
||||
.AddOpenAIChatCompletion(
|
||||
modelId: TestConfiguration.OpenAI.ChatModelId,
|
||||
apiKey: TestConfiguration.OpenAI.ApiKey)
|
||||
.Build();
|
||||
|
||||
// Create a process that will interact with the chat completion service
|
||||
ProcessBuilder process = new("ChatBot");
|
||||
var introStep = process.AddStepFromType<IntroStep>();
|
||||
var userInputStep = process.AddStepFromType<ChatUserInputStep>();
|
||||
var responseStep = process.AddStepFromType<ChatBotResponseStep>();
|
||||
|
||||
// Define the behavior when the process receives an external event
|
||||
process
|
||||
.OnInputEvent(ChatBotEvents.StartProcess)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(introStep));
|
||||
|
||||
// When the intro is complete, notify the userInput step
|
||||
introStep
|
||||
.OnFunctionResult()
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep));
|
||||
|
||||
// When the userInput step emits an exit event, send it to the end step
|
||||
userInputStep
|
||||
.OnEvent(ChatBotEvents.Exit)
|
||||
.StopProcess();
|
||||
|
||||
// When the userInput step emits a user input event, send it to the assistantResponse step
|
||||
userInputStep
|
||||
.OnEvent(CommonEvents.UserInputReceived)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(responseStep, parameterName: "userMessage"));
|
||||
|
||||
// When the assistantResponse step emits a response, send it to the userInput step
|
||||
responseStep
|
||||
.OnEvent(ChatBotEvents.AssistantResponseGenerated)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep));
|
||||
|
||||
// Build the process to get a handle that can be started
|
||||
KernelProcess kernelProcess = process.Build();
|
||||
|
||||
// Generate a Mermaid diagram for the process and print it to the console
|
||||
string mermaidGraph = kernelProcess.ToMermaid();
|
||||
Console.WriteLine($"=== Start - Mermaid Diagram for '{process.Name}' ===");
|
||||
Console.WriteLine(mermaidGraph);
|
||||
Console.WriteLine($"=== End - Mermaid Diagram for '{process.Name}' ===");
|
||||
|
||||
// Generate an image from the Mermaid diagram
|
||||
string generatedImagePath = await MermaidRenderer.GenerateMermaidImageAsync(mermaidGraph, "ChatBotProcess.png");
|
||||
Console.WriteLine($"Diagram generated at: {generatedImagePath}");
|
||||
|
||||
// Start the process with an initial external event
|
||||
await using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = ChatBotEvents.StartProcess, Data = null });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The simplest implementation of a process step. IntroStep
|
||||
/// </summary>
|
||||
private sealed class IntroStep : KernelProcessStep
|
||||
{
|
||||
/// <summary>
|
||||
/// Prints an introduction message to the console.
|
||||
/// </summary>
|
||||
[KernelFunction]
|
||||
public void PrintIntroMessage()
|
||||
{
|
||||
System.Console.WriteLine("Welcome to Processes in Semantic Kernel.\n");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A step that elicits user input.
|
||||
/// </summary>
|
||||
private sealed class ChatUserInputStep : ScriptedUserInputStep
|
||||
{
|
||||
public override void PopulateUserInputs(UserInputState state)
|
||||
{
|
||||
state.UserInputs.Add("Hello");
|
||||
state.UserInputs.Add("How tall is the tallest mountain?");
|
||||
state.UserInputs.Add("How low is the lowest valley?");
|
||||
state.UserInputs.Add("How wide is the widest river?");
|
||||
state.UserInputs.Add("exit");
|
||||
state.UserInputs.Add("This text will be ignored because exit process condition was already met at this point.");
|
||||
}
|
||||
|
||||
public override async ValueTask GetUserInputAsync(KernelProcessStepContext context)
|
||||
{
|
||||
var userMessage = this.GetNextUserMessage();
|
||||
|
||||
if (string.Equals(userMessage, "exit", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// exit condition met, emitting exit event
|
||||
await context.EmitEventAsync(new() { Id = ChatBotEvents.Exit, Data = userMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
// emitting userInputReceived event
|
||||
await context.EmitEventAsync(new() { Id = CommonEvents.UserInputReceived, Data = userMessage });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A step that takes the user input from a previous step and generates a response from the chat completion service.
|
||||
/// </summary>
|
||||
private sealed class ChatBotResponseStep : KernelProcessStep<ChatBotState>
|
||||
{
|
||||
public static class ProcessFunctions
|
||||
{
|
||||
public const string GetChatResponse = nameof(GetChatResponse);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The internal state object for the chat bot response step.
|
||||
/// </summary>
|
||||
internal ChatBotState? _state;
|
||||
|
||||
/// <summary>
|
||||
/// ActivateAsync is the place to initialize the state object for the step.
|
||||
/// </summary>
|
||||
/// <param name="state">An instance of <see cref="ChatBotState"/></param>
|
||||
/// <returns>A <see cref="ValueTask"/></returns>
|
||||
public override ValueTask ActivateAsync(KernelProcessStepState<ChatBotState> state)
|
||||
{
|
||||
_state = state.State;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a response from the chat completion service.
|
||||
/// </summary>
|
||||
/// <param name="context">The context for the current step and process. <see cref="KernelProcessStepContext"/></param>
|
||||
/// <param name="userMessage">The user message from a previous step.</param>
|
||||
/// <param name="_kernel">A <see cref="Kernel"/> instance.</param>
|
||||
/// <returns></returns>
|
||||
[KernelFunction(ProcessFunctions.GetChatResponse)]
|
||||
public async Task GetChatResponseAsync(KernelProcessStepContext context, string userMessage, Kernel _kernel)
|
||||
{
|
||||
_state!.ChatMessages.Add(new(AuthorRole.User, userMessage));
|
||||
IChatCompletionService chatService = _kernel.Services.GetRequiredService<IChatCompletionService>();
|
||||
ChatMessageContent response = await chatService.GetChatMessageContentAsync(_state.ChatMessages).ConfigureAwait(false);
|
||||
if (response == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to get a response from the chat completion service.");
|
||||
}
|
||||
|
||||
System.Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
System.Console.WriteLine($"ASSISTANT: {response.Content}");
|
||||
System.Console.ResetColor();
|
||||
|
||||
// Update state with the response
|
||||
_state.ChatMessages.Add(response);
|
||||
|
||||
// emit event: assistantResponse
|
||||
await context.EmitEventAsync(new KernelProcessEvent { Id = ChatBotEvents.AssistantResponseGenerated, Data = response });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The state object for the <see cref="ChatBotResponseStep"/>.
|
||||
/// </summary>
|
||||
private sealed class ChatBotState
|
||||
{
|
||||
internal ChatHistory ChatMessages { get; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class that defines the events that can be emitted by the chat bot process. This is
|
||||
/// not required but used to ensure that the event names are consistent.
|
||||
/// </summary>
|
||||
private static class ChatBotEvents
|
||||
{
|
||||
public const string StartProcess = "startProcess";
|
||||
public const string IntroComplete = "introComplete";
|
||||
public const string AssistantResponseGenerated = "assistantResponseGenerated";
|
||||
public const string Exit = "exit";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Step02.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data structure for a form capturing details of a new customer, including personal information, contact details, account id and account type.<br/>
|
||||
/// Class used in <see cref="Step02a_AccountOpening"/>, <see cref="Step02b_AccountOpening"/> samples
|
||||
/// </summary>
|
||||
public class AccountDetails : NewCustomerForm
|
||||
{
|
||||
public Guid AccountId { get; set; }
|
||||
public AccountType AccountType { get; set; }
|
||||
}
|
||||
|
||||
public enum AccountType
|
||||
{
|
||||
PrimeABC,
|
||||
Other,
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
namespace Step02.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Processes Events related to Account Opening scenarios.<br/>
|
||||
/// Class used in <see cref="Step02a_AccountOpening"/>, <see cref="Step02b_AccountOpening"/> samples
|
||||
/// </summary>
|
||||
public static class AccountOpeningEvents
|
||||
{
|
||||
public static readonly string StartProcess = nameof(StartProcess);
|
||||
|
||||
public static readonly string NewCustomerFormWelcomeMessageComplete = nameof(NewCustomerFormWelcomeMessageComplete);
|
||||
public static readonly string NewCustomerFormCompleted = nameof(NewCustomerFormCompleted);
|
||||
public static readonly string NewCustomerFormNeedsMoreDetails = nameof(NewCustomerFormNeedsMoreDetails);
|
||||
public static readonly string CustomerInteractionTranscriptReady = nameof(CustomerInteractionTranscriptReady);
|
||||
|
||||
public static readonly string NewAccountVerificationCheckPassed = nameof(NewAccountVerificationCheckPassed);
|
||||
|
||||
public static readonly string CreditScoreCheckApproved = nameof(CreditScoreCheckApproved);
|
||||
public static readonly string CreditScoreCheckRejected = nameof(CreditScoreCheckRejected);
|
||||
|
||||
public static readonly string FraudDetectionCheckPassed = nameof(FraudDetectionCheckPassed);
|
||||
public static readonly string FraudDetectionCheckFailed = nameof(FraudDetectionCheckFailed);
|
||||
|
||||
public static readonly string NewAccountDetailsReady = nameof(NewAccountDetailsReady);
|
||||
|
||||
public static readonly string NewMarketingRecordInfoReady = nameof(NewMarketingRecordInfoReady);
|
||||
public static readonly string NewMarketingEntryCreated = nameof(NewMarketingEntryCreated);
|
||||
public static readonly string CRMRecordInfoReady = nameof(CRMRecordInfoReady);
|
||||
public static readonly string CRMRecordInfoEntryCreated = nameof(CRMRecordInfoEntryCreated);
|
||||
|
||||
public static readonly string WelcomePacketCreated = nameof(WelcomePacketCreated);
|
||||
|
||||
public static readonly string MailServiceSent = nameof(MailServiceSent);
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace Step02.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the details of interactions between a user and service, including a unique identifier for the account,
|
||||
/// a transcript of conversation with the user, and the type of user interaction.<br/>
|
||||
/// Class used in <see cref="Step02a_AccountOpening"/>, <see cref="Step02b_AccountOpening"/> samples
|
||||
/// </summary>
|
||||
public record AccountUserInteractionDetails
|
||||
{
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
public List<ChatMessageContent> InteractionTranscript { get; set; } = [];
|
||||
|
||||
public UserInteractionType UserInteractionType { get; set; }
|
||||
}
|
||||
|
||||
public enum UserInteractionType
|
||||
{
|
||||
Complaint,
|
||||
AccountInfoRequest,
|
||||
OpeningNewAccount
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Step02.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Holds details for a new entry in a marketing database, including the account identifier, contact name, phone number, and email address.<br/>
|
||||
/// Class used in <see cref="Step02a_AccountOpening"/>, <see cref="Step02b_AccountOpening"/> samples
|
||||
/// </summary>
|
||||
public record MarketingNewEntryDetails
|
||||
{
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string PhoneNumber { get; set; }
|
||||
|
||||
public string Email { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Step02.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data structure for a form capturing details of a new customer, including personal information and contact details.<br/>
|
||||
/// Class used in <see cref="Step02a_AccountOpening"/>, <see cref="Step02b_AccountOpening"/> samples
|
||||
/// </summary>
|
||||
public class NewCustomerForm
|
||||
{
|
||||
[JsonPropertyName("userFirstName")]
|
||||
public string UserFirstName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("userLastName")]
|
||||
public string UserLastName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("userDateOfBirth")]
|
||||
public string UserDateOfBirth { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("userState")]
|
||||
public string UserState { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("userPhoneNumber")]
|
||||
public string UserPhoneNumber { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("userId")]
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("userEmail")]
|
||||
public string UserEmail { get; set; } = string.Empty;
|
||||
|
||||
public NewCustomerForm CopyWithDefaultValues(string defaultStringValue = "Unanswered")
|
||||
{
|
||||
NewCustomerForm copy = new();
|
||||
PropertyInfo[] properties = typeof(NewCustomerForm).GetProperties();
|
||||
|
||||
foreach (PropertyInfo property in properties)
|
||||
{
|
||||
// Get the value of the property
|
||||
string? value = property.GetValue(this) as string;
|
||||
|
||||
// Check if the value is an empty string
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
property.SetValue(copy, defaultStringValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
property.SetValue(copy, value);
|
||||
}
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
public bool IsFormCompleted()
|
||||
{
|
||||
return !string.IsNullOrEmpty(UserFirstName) &&
|
||||
!string.IsNullOrEmpty(UserLastName) &&
|
||||
!string.IsNullOrEmpty(UserId) &&
|
||||
!string.IsNullOrEmpty(UserDateOfBirth) &&
|
||||
!string.IsNullOrEmpty(UserState) &&
|
||||
!string.IsNullOrEmpty(UserEmail) &&
|
||||
!string.IsNullOrEmpty(UserPhoneNumber);
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step02.Models;
|
||||
using Step02.Steps;
|
||||
|
||||
namespace Step02.Processes;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate creation of <see cref="KernelProcess"/> and
|
||||
/// eliciting its response to five explicit user messages.<br/>
|
||||
/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.<br/>
|
||||
/// For visual reference of the process check the <see href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step02b_accountOpening" >diagram</see>.
|
||||
/// </summary>
|
||||
public static class NewAccountCreationProcess
|
||||
{
|
||||
public static ProcessBuilder CreateProcess()
|
||||
{
|
||||
ProcessBuilder process = new("AccountCreationProcess");
|
||||
|
||||
var coreSystemRecordCreationStep = process.AddStepFromType<NewAccountStep>();
|
||||
var marketingRecordCreationStep = process.AddStepFromType<NewMarketingEntryStep>();
|
||||
var crmRecordStep = process.AddStepFromType<CRMRecordCreationStep>();
|
||||
var welcomePacketStep = process.AddStepFromType<WelcomePacketStep>();
|
||||
|
||||
// When the newCustomerForm is completed...
|
||||
process
|
||||
.OnInputEvent(AccountOpeningEvents.NewCustomerFormCompleted)
|
||||
// The information gets passed to the core system record creation step
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(coreSystemRecordCreationStep, functionName: NewAccountStep.ProcessStepFunctions.CreateNewAccount, parameterName: "customerDetails"));
|
||||
|
||||
// When the newCustomerForm is completed, the user interaction transcript with the user is passed to the core system record creation step
|
||||
process
|
||||
.OnInputEvent(AccountOpeningEvents.CustomerInteractionTranscriptReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(coreSystemRecordCreationStep, functionName: NewAccountStep.ProcessStepFunctions.CreateNewAccount, parameterName: "interactionTranscript"));
|
||||
|
||||
// When the fraudDetectionCheck step passes, the information gets to core system record creation step to kickstart this step
|
||||
process
|
||||
.OnInputEvent(AccountOpeningEvents.NewAccountVerificationCheckPassed)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(coreSystemRecordCreationStep, functionName: NewAccountStep.ProcessStepFunctions.CreateNewAccount, parameterName: "previousCheckSucceeded"));
|
||||
|
||||
// When the coreSystemRecordCreation step successfully creates a new accountId, it will trigger the creation of a new marketing entry through the marketingRecordCreation step
|
||||
coreSystemRecordCreationStep
|
||||
.OnEvent(AccountOpeningEvents.NewMarketingRecordInfoReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(marketingRecordCreationStep, functionName: NewMarketingEntryStep.ProcessStepFunctions.CreateNewMarketingEntry, parameterName: "userDetails"));
|
||||
|
||||
// When the coreSystemRecordCreation step successfully creates a new accountId, it will trigger the creation of a new CRM entry through the crmRecord step
|
||||
coreSystemRecordCreationStep
|
||||
.OnEvent(AccountOpeningEvents.CRMRecordInfoReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(crmRecordStep, functionName: CRMRecordCreationStep.ProcessStepFunctions.CreateCRMEntry, parameterName: "userInteractionDetails"));
|
||||
|
||||
// ParameterName is necessary when the step has multiple input arguments like welcomePacketStep.CreateWelcomePacketAsync
|
||||
// When the coreSystemRecordCreation step successfully creates a new accountId, it will pass the account information details to the welcomePacket step
|
||||
coreSystemRecordCreationStep
|
||||
.OnEvent(AccountOpeningEvents.NewAccountDetailsReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(welcomePacketStep, parameterName: "accountDetails"));
|
||||
|
||||
// When the marketingRecordCreation step successfully creates a new marketing entry, it will notify the welcomePacket step it is ready
|
||||
marketingRecordCreationStep
|
||||
.OnEvent(AccountOpeningEvents.NewMarketingEntryCreated)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(welcomePacketStep, parameterName: "marketingEntryCreated"));
|
||||
|
||||
// When the crmRecord step successfully creates a new CRM entry, it will notify the welcomePacket step it is ready
|
||||
crmRecordStep
|
||||
.OnEvent(AccountOpeningEvents.CRMRecordInfoEntryCreated)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(welcomePacketStep, parameterName: "crmRecordCreated"));
|
||||
|
||||
return process;
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step02.Models;
|
||||
using Step02.Steps;
|
||||
|
||||
namespace Step02.Processes;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate creation of <see cref="KernelProcess"/> and
|
||||
/// eliciting its response to five explicit user messages.<br/>
|
||||
/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.<br/>
|
||||
/// For visual reference of the process check the <see href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step02b_accountOpening" >diagram</see>.
|
||||
/// </summary>
|
||||
public static class NewAccountVerificationProcess
|
||||
{
|
||||
public static ProcessBuilder CreateProcess()
|
||||
{
|
||||
ProcessBuilder process = new("AccountVerificationProcess");
|
||||
|
||||
var customerCreditCheckStep = process.AddStepFromType<CreditScoreCheckStep>();
|
||||
var fraudDetectionCheckStep = process.AddStepFromType<FraudDetectionStep>();
|
||||
|
||||
// When the newCustomerForm is completed...
|
||||
process
|
||||
.OnInputEvent(AccountOpeningEvents.NewCustomerFormCompleted)
|
||||
// The information gets passed to the core system record creation step
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(customerCreditCheckStep, functionName: CreditScoreCheckStep.ProcessStepFunctions.DetermineCreditScore, parameterName: "customerDetails"))
|
||||
// The information gets passed to the fraud detection step for validation
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(fraudDetectionCheckStep, functionName: FraudDetectionStep.ProcessStepFunctions.FraudDetectionCheck, parameterName: "customerDetails"));
|
||||
|
||||
// When the creditScoreCheck step results in Approval, the information gets to the fraudDetection step to kickstart this step
|
||||
customerCreditCheckStep
|
||||
.OnEvent(AccountOpeningEvents.CreditScoreCheckApproved)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(fraudDetectionCheckStep, functionName: FraudDetectionStep.ProcessStepFunctions.FraudDetectionCheck, parameterName: "previousCheckSucceeded"));
|
||||
|
||||
return process;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Events;
|
||||
using Microsoft.SemanticKernel;
|
||||
using SharedSteps;
|
||||
using Step02.Models;
|
||||
using Step02.Steps;
|
||||
|
||||
namespace Step02;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate creation of <see cref="KernelProcess"/> and
|
||||
/// eliciting its response to five explicit user messages.<br/>
|
||||
/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.<br/>
|
||||
/// For visual reference of the process check the <see href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step02a_accountOpening" >diagram</see>.
|
||||
/// </summary>
|
||||
public class Step02a_AccountOpening(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true)
|
||||
{
|
||||
// Target Open AI Services
|
||||
protected override bool ForceOpenAI => true;
|
||||
|
||||
private KernelProcess SetupAccountOpeningProcess<TUserInputStep>() where TUserInputStep : ScriptedUserInputStep
|
||||
{
|
||||
ProcessBuilder process = new("AccountOpeningProcess");
|
||||
var newCustomerFormStep = process.AddStepFromType<CompleteNewCustomerFormStep>();
|
||||
var userInputStep = process.AddStepFromType<TUserInputStep>();
|
||||
var displayAssistantMessageStep = process.AddStepFromType<DisplayAssistantMessageStep>();
|
||||
var customerCreditCheckStep = process.AddStepFromType<CreditScoreCheckStep>();
|
||||
var fraudDetectionCheckStep = process.AddStepFromType<FraudDetectionStep>();
|
||||
var mailServiceStep = process.AddStepFromType<MailServiceStep>();
|
||||
var coreSystemRecordCreationStep = process.AddStepFromType<NewAccountStep>();
|
||||
var marketingRecordCreationStep = process.AddStepFromType<NewMarketingEntryStep>();
|
||||
var crmRecordStep = process.AddStepFromType<CRMRecordCreationStep>();
|
||||
var welcomePacketStep = process.AddStepFromType<WelcomePacketStep>();
|
||||
|
||||
process.OnInputEvent(AccountOpeningEvents.StartProcess)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(newCustomerFormStep, CompleteNewCustomerFormStep.ProcessStepFunctions.NewAccountWelcome));
|
||||
|
||||
// When the welcome message is generated, send message to displayAssistantMessageStep
|
||||
newCustomerFormStep
|
||||
.OnEvent(AccountOpeningEvents.NewCustomerFormWelcomeMessageComplete)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(displayAssistantMessageStep, DisplayAssistantMessageStep.ProcessStepFunctions.DisplayAssistantMessage));
|
||||
|
||||
// When the userInput step emits a user input event, send it to the newCustomerForm step
|
||||
// Function names are necessary when the step has multiple public functions like CompleteNewCustomerFormStep: NewAccountWelcome and NewAccountProcessUserInfo
|
||||
userInputStep
|
||||
.OnEvent(CommonEvents.UserInputReceived)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(newCustomerFormStep, CompleteNewCustomerFormStep.ProcessStepFunctions.NewAccountProcessUserInfo, "userMessage"));
|
||||
|
||||
userInputStep
|
||||
.OnEvent(CommonEvents.Exit)
|
||||
.StopProcess();
|
||||
|
||||
// When the newCustomerForm step emits needs more details, send message to displayAssistantMessage step
|
||||
newCustomerFormStep
|
||||
.OnEvent(AccountOpeningEvents.NewCustomerFormNeedsMoreDetails)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(displayAssistantMessageStep, DisplayAssistantMessageStep.ProcessStepFunctions.DisplayAssistantMessage));
|
||||
|
||||
// After any assistant message is displayed, user input is expected to the next step is the userInputStep
|
||||
displayAssistantMessageStep
|
||||
.OnEvent(CommonEvents.AssistantResponseGenerated)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep, ScriptedUserInputStep.ProcessStepFunctions.GetUserInput));
|
||||
|
||||
// When the newCustomerForm is completed...
|
||||
newCustomerFormStep
|
||||
.OnEvent(AccountOpeningEvents.NewCustomerFormCompleted)
|
||||
// The information gets passed to the core system record creation step
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(customerCreditCheckStep, functionName: CreditScoreCheckStep.ProcessStepFunctions.DetermineCreditScore, parameterName: "customerDetails"))
|
||||
// The information gets passed to the fraud detection step for validation
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(fraudDetectionCheckStep, functionName: FraudDetectionStep.ProcessStepFunctions.FraudDetectionCheck, parameterName: "customerDetails"))
|
||||
// The information gets passed to the core system record creation step
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(coreSystemRecordCreationStep, functionName: NewAccountStep.ProcessStepFunctions.CreateNewAccount, parameterName: "customerDetails"));
|
||||
|
||||
// When the newCustomerForm is completed, the user interaction transcript with the user is passed to the core system record creation step
|
||||
newCustomerFormStep
|
||||
.OnEvent(AccountOpeningEvents.CustomerInteractionTranscriptReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(coreSystemRecordCreationStep, functionName: NewAccountStep.ProcessStepFunctions.CreateNewAccount, parameterName: "interactionTranscript"));
|
||||
|
||||
// When the creditScoreCheck step results in Rejection, the information gets to the mailService step to notify the user about the state of the application and the reasons
|
||||
customerCreditCheckStep
|
||||
.OnEvent(AccountOpeningEvents.CreditScoreCheckRejected)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep, functionName: MailServiceStep.ProcessStepFunctions.SendMailToUserWithDetails, parameterName: "message"));
|
||||
|
||||
// When the creditScoreCheck step results in Approval, the information gets to the fraudDetection step to kickstart this step
|
||||
customerCreditCheckStep
|
||||
.OnEvent(AccountOpeningEvents.CreditScoreCheckApproved)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(fraudDetectionCheckStep, functionName: FraudDetectionStep.ProcessStepFunctions.FraudDetectionCheck, parameterName: "previousCheckSucceeded"));
|
||||
|
||||
// When the fraudDetectionCheck step fails, the information gets to the mailService step to notify the user about the state of the application and the reasons
|
||||
fraudDetectionCheckStep
|
||||
.OnEvent(AccountOpeningEvents.FraudDetectionCheckFailed)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep, functionName: MailServiceStep.ProcessStepFunctions.SendMailToUserWithDetails, parameterName: "message"));
|
||||
|
||||
// When the fraudDetectionCheck step passes, the information gets to core system record creation step to kickstart this step
|
||||
fraudDetectionCheckStep
|
||||
.OnEvent(AccountOpeningEvents.FraudDetectionCheckPassed)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(coreSystemRecordCreationStep, functionName: NewAccountStep.ProcessStepFunctions.CreateNewAccount, parameterName: "previousCheckSucceeded"));
|
||||
|
||||
// When the coreSystemRecordCreation step successfully creates a new accountId, it will trigger the creation of a new marketing entry through the marketingRecordCreation step
|
||||
coreSystemRecordCreationStep
|
||||
.OnEvent(AccountOpeningEvents.NewMarketingRecordInfoReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(marketingRecordCreationStep, functionName: NewMarketingEntryStep.ProcessStepFunctions.CreateNewMarketingEntry, parameterName: "userDetails"));
|
||||
|
||||
// When the coreSystemRecordCreation step successfully creates a new accountId, it will trigger the creation of a new CRM entry through the crmRecord step
|
||||
coreSystemRecordCreationStep
|
||||
.OnEvent(AccountOpeningEvents.CRMRecordInfoReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(crmRecordStep, functionName: CRMRecordCreationStep.ProcessStepFunctions.CreateCRMEntry, parameterName: "userInteractionDetails"));
|
||||
|
||||
// ParameterName is necessary when the step has multiple input arguments like welcomePacketStep.CreateWelcomePacketAsync
|
||||
// When the coreSystemRecordCreation step successfully creates a new accountId, it will pass the account information details to the welcomePacket step
|
||||
coreSystemRecordCreationStep
|
||||
.OnEvent(AccountOpeningEvents.NewAccountDetailsReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(welcomePacketStep, parameterName: "accountDetails"));
|
||||
|
||||
// When the marketingRecordCreation step successfully creates a new marketing entry, it will notify the welcomePacket step it is ready
|
||||
marketingRecordCreationStep
|
||||
.OnEvent(AccountOpeningEvents.NewMarketingEntryCreated)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(welcomePacketStep, parameterName: "marketingEntryCreated"));
|
||||
|
||||
// When the crmRecord step successfully creates a new CRM entry, it will notify the welcomePacket step it is ready
|
||||
crmRecordStep
|
||||
.OnEvent(AccountOpeningEvents.CRMRecordInfoEntryCreated)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(welcomePacketStep, parameterName: "crmRecordCreated"));
|
||||
|
||||
// After crmRecord and marketing gets created, a welcome packet is created to then send information to the user with the mailService step
|
||||
welcomePacketStep
|
||||
.OnEvent(AccountOpeningEvents.WelcomePacketCreated)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep, functionName: MailServiceStep.ProcessStepFunctions.SendMailToUserWithDetails, parameterName: "message"));
|
||||
|
||||
// All possible paths end up with the user being notified about the account creation decision throw the mailServiceStep completion
|
||||
mailServiceStep
|
||||
.OnEvent(AccountOpeningEvents.MailServiceSent)
|
||||
.StopProcess();
|
||||
|
||||
KernelProcess kernelProcess = process.Build();
|
||||
|
||||
return kernelProcess;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This test uses a specific userId and DOB that makes the creditScore and Fraud detection to pass
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UseAccountOpeningProcessSuccessfulInteractionAsync()
|
||||
{
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputSuccessfulInteractionStep>();
|
||||
await using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This test uses a specific DOB that makes the creditScore to fail
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UseAccountOpeningProcessFailureDueToCreditScoreFailureAsync()
|
||||
{
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputCreditScoreFailureInteractionStep>();
|
||||
await using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This test uses a specific userId that makes the fraudDetection to fail
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UseAccountOpeningProcessFailureDueToFraudFailureAsync()
|
||||
{
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputFraudFailureInteractionStep>();
|
||||
await using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Events;
|
||||
using Microsoft.SemanticKernel;
|
||||
using SharedSteps;
|
||||
using Step02.Models;
|
||||
using Step02.Processes;
|
||||
using Step02.Steps;
|
||||
|
||||
namespace Step02;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate creation of <see cref="KernelProcess"/> and
|
||||
/// eliciting its response to five explicit user messages.<br/>
|
||||
/// For each test there is a different set of user messages that will cause different steps to be triggered using the same pipeline.<br/>
|
||||
/// For visual reference of the process check the <see href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step02a_accountOpening" >diagram</see>.
|
||||
/// </summary>
|
||||
public class Step02b_AccountOpening(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true)
|
||||
{
|
||||
// Target Open AI Services
|
||||
protected override bool ForceOpenAI => true;
|
||||
|
||||
private KernelProcess SetupAccountOpeningProcess<TUserInputStep>() where TUserInputStep : ScriptedUserInputStep
|
||||
{
|
||||
ProcessBuilder process = new("AccountOpeningProcessWithSubprocesses");
|
||||
var newCustomerFormStep = process.AddStepFromType<CompleteNewCustomerFormStep>();
|
||||
var userInputStep = process.AddStepFromType<TUserInputStep>();
|
||||
var displayAssistantMessageStep = process.AddStepFromType<DisplayAssistantMessageStep>();
|
||||
|
||||
var accountVerificationStep = process.AddStepFromProcess(NewAccountVerificationProcess.CreateProcess());
|
||||
var accountCreationStep = process.AddStepFromProcess(NewAccountCreationProcess.CreateProcess());
|
||||
|
||||
var mailServiceStep = process.AddStepFromType<MailServiceStep>();
|
||||
|
||||
process
|
||||
.OnInputEvent(AccountOpeningEvents.StartProcess)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(newCustomerFormStep, CompleteNewCustomerFormStep.ProcessStepFunctions.NewAccountWelcome));
|
||||
|
||||
// When the welcome message is generated, send message to displayAssistantMessageStep
|
||||
newCustomerFormStep
|
||||
.OnEvent(AccountOpeningEvents.NewCustomerFormWelcomeMessageComplete)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(displayAssistantMessageStep, DisplayAssistantMessageStep.ProcessStepFunctions.DisplayAssistantMessage));
|
||||
|
||||
// When the userInput step emits a user input event, send it to the newCustomerForm step
|
||||
// Function names are necessary when the step has multiple public functions like CompleteNewCustomerFormStep: NewAccountWelcome and NewAccountProcessUserInfo
|
||||
userInputStep
|
||||
.OnEvent(CommonEvents.UserInputReceived)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(newCustomerFormStep, CompleteNewCustomerFormStep.ProcessStepFunctions.NewAccountProcessUserInfo, "userMessage"));
|
||||
|
||||
userInputStep
|
||||
.OnEvent(CommonEvents.Exit)
|
||||
.StopProcess();
|
||||
|
||||
// When the newCustomerForm step emits needs more details, send message to displayAssistantMessage step
|
||||
newCustomerFormStep
|
||||
.OnEvent(AccountOpeningEvents.NewCustomerFormNeedsMoreDetails)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(displayAssistantMessageStep, DisplayAssistantMessageStep.ProcessStepFunctions.DisplayAssistantMessage));
|
||||
|
||||
// After any assistant message is displayed, user input is expected to the next step is the userInputStep
|
||||
displayAssistantMessageStep
|
||||
.OnEvent(CommonEvents.AssistantResponseGenerated)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep, ScriptedUserInputStep.ProcessStepFunctions.GetUserInput));
|
||||
|
||||
// When the newCustomerForm is completed...
|
||||
newCustomerFormStep
|
||||
.OnEvent(AccountOpeningEvents.NewCustomerFormCompleted)
|
||||
// The information gets passed to the account verificatino step
|
||||
.SendEventTo(accountVerificationStep.WhereInputEventIs(AccountOpeningEvents.NewCustomerFormCompleted))
|
||||
// The information gets passed to the validation process step
|
||||
.SendEventTo(accountCreationStep.WhereInputEventIs(AccountOpeningEvents.NewCustomerFormCompleted));
|
||||
|
||||
// When the newCustomerForm is completed, the user interaction transcript with the user is passed to the core system record creation step
|
||||
newCustomerFormStep
|
||||
.OnEvent(AccountOpeningEvents.CustomerInteractionTranscriptReady)
|
||||
.SendEventTo(accountCreationStep.WhereInputEventIs(AccountOpeningEvents.CustomerInteractionTranscriptReady));
|
||||
|
||||
// When the creditScoreCheck step results in Rejection, the information gets to the mailService step to notify the user about the state of the application and the reasons
|
||||
accountVerificationStep
|
||||
.OnEvent(AccountOpeningEvents.CreditScoreCheckRejected)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep));
|
||||
|
||||
// When the fraudDetectionCheck step fails, the information gets to the mailService step to notify the user about the state of the application and the reasons
|
||||
accountVerificationStep
|
||||
.OnEvent(AccountOpeningEvents.FraudDetectionCheckFailed)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep));
|
||||
|
||||
// When the fraudDetectionCheck step passes, the information gets to core system record creation step to kickstart this step
|
||||
accountVerificationStep
|
||||
.OnEvent(AccountOpeningEvents.FraudDetectionCheckPassed)
|
||||
.SendEventTo(accountCreationStep.WhereInputEventIs(AccountOpeningEvents.NewAccountVerificationCheckPassed));
|
||||
|
||||
// After crmRecord and marketing gets created, a welcome packet is created to then send information to the user with the mailService step
|
||||
accountCreationStep
|
||||
.OnEvent(AccountOpeningEvents.WelcomePacketCreated)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(mailServiceStep));
|
||||
|
||||
// All possible paths end up with the user being notified about the account creation decision throw the mailServiceStep completion
|
||||
mailServiceStep
|
||||
.OnEvent(AccountOpeningEvents.MailServiceSent)
|
||||
.StopProcess();
|
||||
|
||||
KernelProcess kernelProcess = process.Build();
|
||||
|
||||
return kernelProcess;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This test uses a specific userId and DOB that makes the creditScore and Fraud detection to pass
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UseAccountOpeningProcessSuccessfulInteractionAsync()
|
||||
{
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputSuccessfulInteractionStep>();
|
||||
await using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This test uses a specific DOB that makes the creditScore to fail
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UseAccountOpeningProcessFailureDueToCreditScoreFailureAsync()
|
||||
{
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputCreditScoreFailureInteractionStep>();
|
||||
await using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This test uses a specific userId that makes the fraudDetection to fail
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UseAccountOpeningProcessFailureDueToFraudFailureAsync()
|
||||
{
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
KernelProcess kernelProcess = SetupAccountOpeningProcess<UserInputFraudFailureInteractionStep>();
|
||||
await using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent() { Id = AccountOpeningEvents.StartProcess, Data = null });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step02.Models;
|
||||
|
||||
namespace Step02.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Mock step that emulates the creation of a new CRM entry
|
||||
/// </summary>
|
||||
public class CRMRecordCreationStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string CreateCRMEntry = nameof(CreateCRMEntry);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.CreateCRMEntry)]
|
||||
public async Task CreateCRMEntryAsync(KernelProcessStepContext context, AccountUserInteractionDetails userInteractionDetails, Kernel _kernel)
|
||||
{
|
||||
Console.WriteLine($"[CRM ENTRY CREATION] New Account {userInteractionDetails.AccountId} created");
|
||||
|
||||
// Placeholder for a call to API to create new CRM entry
|
||||
await context.EmitEventAsync(new() { Id = AccountOpeningEvents.CRMRecordInfoEntryCreated, Data = true });
|
||||
}
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
using Step02.Models;
|
||||
|
||||
namespace Step02.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Step that is helps the user fill up a new account form.<br/>
|
||||
/// Also provides a welcome message for the user.
|
||||
/// </summary>
|
||||
public class CompleteNewCustomerFormStep : KernelProcessStep<NewCustomerFormState>
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string NewAccountProcessUserInfo = nameof(NewAccountProcessUserInfo);
|
||||
public const string NewAccountWelcome = nameof(NewAccountWelcome);
|
||||
}
|
||||
|
||||
internal NewCustomerFormState? _state;
|
||||
|
||||
internal string _formCompletionSystemPrompt = """
|
||||
The goal is to fill up all the fields needed for a form.
|
||||
The user may provide information to fill up multiple fields of the form in one message.
|
||||
The user needs to fill up a form, all the fields of the form are necessary
|
||||
|
||||
<CURRENT_FORM_STATE>
|
||||
{{current_form_state}}
|
||||
<CURRENT_FORM_STATE>
|
||||
|
||||
GUIDANCE:
|
||||
- If there are missing details, give the user a useful message that will help fill up the remaining fields.
|
||||
- Your goal is to help guide the user to provide the missing details on the current form.
|
||||
- Encourage the user to provide the remainingdetails with examples if necessary.
|
||||
- Fields with value 'Unanswered' need to be answered by the user.
|
||||
- Format phone numbers and user ids correctly if the user does not provide the expected format.
|
||||
- If the user does not make use of parenthesis in the phone number, add them.
|
||||
- For date fields, confirm with the user first if the date format is not clear. Example 02/03 03/02 could be March 2nd or February 3rd.
|
||||
""";
|
||||
|
||||
internal string _welcomeMessage = """
|
||||
Hello there, I can help you out fill out the information needed to open a new account with us.
|
||||
Please provide some personal information like first name and last name to get started.
|
||||
""";
|
||||
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never
|
||||
};
|
||||
|
||||
public override ValueTask ActivateAsync(KernelProcessStepState<NewCustomerFormState> state)
|
||||
{
|
||||
_state = state.State;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.NewAccountWelcome)]
|
||||
public async Task NewAccountWelcomeMessageAsync(KernelProcessStepContext context, Kernel _kernel)
|
||||
{
|
||||
_state?.conversation.Add(new ChatMessageContent { Role = AuthorRole.Assistant, Content = _welcomeMessage });
|
||||
await context.EmitEventAsync(new() { Id = AccountOpeningEvents.NewCustomerFormWelcomeMessageComplete, Data = _welcomeMessage });
|
||||
}
|
||||
|
||||
private Kernel CreateNewCustomerFormKernel(Kernel _baseKernel)
|
||||
{
|
||||
// Creating another kernel that only makes use private functions to fill up the new customer form
|
||||
Kernel kernel = new(_baseKernel.Services);
|
||||
kernel.ImportPluginFromFunctions("FillForm", [
|
||||
KernelFunctionFactory.CreateFromMethod(OnUserProvidedFirstName, functionName: nameof(OnUserProvidedFirstName)),
|
||||
KernelFunctionFactory.CreateFromMethod(OnUserProvidedLastName, functionName: nameof(OnUserProvidedLastName)),
|
||||
KernelFunctionFactory.CreateFromMethod(OnUserProvidedDOBDetails, functionName: nameof(OnUserProvidedDOBDetails)),
|
||||
KernelFunctionFactory.CreateFromMethod(OnUserProvidedStateOfResidence, functionName: nameof(OnUserProvidedStateOfResidence)),
|
||||
KernelFunctionFactory.CreateFromMethod(OnUserProvidedPhoneNumber, functionName: nameof(OnUserProvidedPhoneNumber)),
|
||||
KernelFunctionFactory.CreateFromMethod(OnUserProvidedUserId, functionName: nameof(OnUserProvidedUserId)),
|
||||
KernelFunctionFactory.CreateFromMethod(OnUserProvidedEmailAddress, functionName: nameof(OnUserProvidedEmailAddress)),
|
||||
]);
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.NewAccountProcessUserInfo)]
|
||||
public async Task CompleteNewCustomerFormAsync(KernelProcessStepContext context, string userMessage, Kernel _kernel)
|
||||
{
|
||||
// Keeping track of all user interactions
|
||||
_state?.conversation.Add(new ChatMessageContent { Role = AuthorRole.User, Content = userMessage });
|
||||
|
||||
Kernel kernel = CreateNewCustomerFormKernel(_kernel);
|
||||
|
||||
OpenAIPromptExecutionSettings settings = new()
|
||||
{
|
||||
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
|
||||
Temperature = 0.7,
|
||||
MaxTokens = 2048
|
||||
};
|
||||
|
||||
ChatHistory chatHistory = [];
|
||||
chatHistory.AddSystemMessage(_formCompletionSystemPrompt
|
||||
.Replace("{{current_form_state}}", JsonSerializer.Serialize(_state!.newCustomerForm.CopyWithDefaultValues(), _jsonOptions)));
|
||||
chatHistory.AddRange(_state.conversation);
|
||||
IChatCompletionService chatService = kernel.Services.GetRequiredService<IChatCompletionService>();
|
||||
ChatMessageContent response = await chatService.GetChatMessageContentAsync(chatHistory, settings, kernel).ConfigureAwait(false);
|
||||
var assistantResponse = "";
|
||||
|
||||
if (response != null)
|
||||
{
|
||||
assistantResponse = response.Items[0].ToString();
|
||||
// Keeping track of all assistant interactions
|
||||
_state?.conversation.Add(new ChatMessageContent { Role = AuthorRole.Assistant, Content = assistantResponse });
|
||||
}
|
||||
|
||||
if (_state?.newCustomerForm != null && _state.newCustomerForm.IsFormCompleted())
|
||||
{
|
||||
Console.WriteLine($"[NEW_USER_FORM_COMPLETED]: {JsonSerializer.Serialize(_state?.newCustomerForm)}");
|
||||
// All user information is gathered to proceed to the next step
|
||||
await context.EmitEventAsync(new() { Id = AccountOpeningEvents.NewCustomerFormCompleted, Data = _state?.newCustomerForm, Visibility = KernelProcessEventVisibility.Public });
|
||||
await context.EmitEventAsync(new() { Id = AccountOpeningEvents.CustomerInteractionTranscriptReady, Data = _state?.conversation, Visibility = KernelProcessEventVisibility.Public });
|
||||
return;
|
||||
}
|
||||
|
||||
// emit event: NewCustomerFormNeedsMoreDetails
|
||||
await context.EmitEventAsync(new() { Id = AccountOpeningEvents.NewCustomerFormNeedsMoreDetails, Data = assistantResponse });
|
||||
}
|
||||
|
||||
[Description("User provided details of first name")]
|
||||
private Task OnUserProvidedFirstName(string firstName)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(firstName) && _state != null)
|
||||
{
|
||||
_state.newCustomerForm.UserFirstName = firstName;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Description("User provided details of last name")]
|
||||
private Task OnUserProvidedLastName(string lastName)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(lastName) && _state != null)
|
||||
{
|
||||
_state.newCustomerForm.UserLastName = lastName;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Description("User provided details of USA State the user lives in, must be in 2-letter Uppercase State Abbreviation format")]
|
||||
private Task OnUserProvidedStateOfResidence(string stateAbbreviation)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stateAbbreviation) && _state != null)
|
||||
{
|
||||
_state.newCustomerForm.UserState = stateAbbreviation;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Description("User provided details of date of birth, must be in the format MM/DD/YYYY")]
|
||||
private Task OnUserProvidedDOBDetails(string date)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(date) && _state != null)
|
||||
{
|
||||
_state.newCustomerForm.UserDateOfBirth = date;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Description("User provided details of phone number, must be in the format (\\d{3})-\\d{3}-\\d{4}")]
|
||||
private Task OnUserProvidedPhoneNumber(string phoneNumber)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(phoneNumber) && _state != null)
|
||||
{
|
||||
_state.newCustomerForm.UserPhoneNumber = phoneNumber;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Description("User provided details of userId, must be in the format \\d{3}-\\d{3}-\\d{4}")]
|
||||
private Task OnUserProvidedUserId(string userId)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(userId) && _state != null)
|
||||
{
|
||||
_state.newCustomerForm.UserId = userId;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Description("User provided email address, must be in the an email valid format")]
|
||||
private Task OnUserProvidedEmailAddress(string emailAddress)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(emailAddress) && _state != null)
|
||||
{
|
||||
_state.newCustomerForm.UserEmail = emailAddress;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The state object for the <see cref="CompleteNewCustomerFormStep"/>
|
||||
/// </summary>
|
||||
public class NewCustomerFormState
|
||||
{
|
||||
internal NewCustomerForm newCustomerForm { get; set; } = new();
|
||||
internal List<ChatMessageContent> conversation { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step02.Models;
|
||||
|
||||
namespace Step02.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Mock step that emulates User Credit Score check, based on the date of birth the score will be enough or insufficient
|
||||
/// </summary>
|
||||
public class CreditScoreCheckStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string DetermineCreditScore = nameof(DetermineCreditScore);
|
||||
}
|
||||
|
||||
private const int MinCreditScore = 600;
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.DetermineCreditScore)]
|
||||
public async Task DetermineCreditScoreAsync(KernelProcessStepContext context, NewCustomerForm customerDetails, Kernel _kernel)
|
||||
{
|
||||
// Placeholder for a call to API to validate credit score with customerDetails
|
||||
var creditScore = customerDetails.UserDateOfBirth == "02/03/1990" ? 700 : 500;
|
||||
|
||||
if (creditScore >= MinCreditScore)
|
||||
{
|
||||
Console.WriteLine("[CREDIT CHECK] Credit Score Check Passed");
|
||||
await context.EmitEventAsync(new() { Id = AccountOpeningEvents.CreditScoreCheckApproved, Data = true });
|
||||
return;
|
||||
}
|
||||
Console.WriteLine("[CREDIT CHECK] Credit Score Check Failed");
|
||||
await context.EmitEventAsync(new()
|
||||
{
|
||||
Id = AccountOpeningEvents.CreditScoreCheckRejected,
|
||||
Data = $"We regret to inform you that your credit score of {creditScore} is insufficient to apply for an account of the type PRIME ABC",
|
||||
Visibility = KernelProcessEventVisibility.Public,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step02.Models;
|
||||
|
||||
namespace Step02.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Mock step that emulates a Fraud detection check, based on the userId the fraud detection will pass or fail.
|
||||
/// </summary>
|
||||
public class FraudDetectionStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string FraudDetectionCheck = nameof(FraudDetectionCheck);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.FraudDetectionCheck)]
|
||||
public async Task FraudDetectionCheckAsync(KernelProcessStepContext context, bool previousCheckSucceeded, NewCustomerForm customerDetails, Kernel _kernel)
|
||||
{
|
||||
// Placeholder for a call to API to validate user details for fraud detection
|
||||
if (customerDetails.UserId == "123-456-7890")
|
||||
{
|
||||
Console.WriteLine("[FRAUD CHECK] Fraud Check Failed");
|
||||
await context.EmitEventAsync(new()
|
||||
{
|
||||
Id = AccountOpeningEvents.FraudDetectionCheckFailed,
|
||||
Data = "We regret to inform you that we found some inconsistent details regarding the information you provided regarding the new account of the type PRIME ABC you applied.",
|
||||
Visibility = KernelProcessEventVisibility.Public,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("[FRAUD CHECK] Fraud Check Passed");
|
||||
await context.EmitEventAsync(new() { Id = AccountOpeningEvents.FraudDetectionCheckPassed, Data = true, Visibility = KernelProcessEventVisibility.Public });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step02.Models;
|
||||
|
||||
namespace Step02.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Mock step that emulates Mail Service with a message for the user.
|
||||
/// </summary>
|
||||
public class MailServiceStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string SendMailToUserWithDetails = nameof(SendMailToUserWithDetails);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.SendMailToUserWithDetails)]
|
||||
public async Task SendMailServiceAsync(KernelProcessStepContext context, string message)
|
||||
{
|
||||
Console.WriteLine("======== MAIL SERVICE ======== ");
|
||||
Console.WriteLine(message);
|
||||
Console.WriteLine("============================== ");
|
||||
|
||||
await context.EmitEventAsync(new() { Id = AccountOpeningEvents.MailServiceSent, Data = message });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step02.Models;
|
||||
|
||||
namespace Step02.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Mock step that emulates the creation of a new account that triggers other services after a new account id creation
|
||||
/// </summary>
|
||||
public class NewAccountStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string CreateNewAccount = nameof(CreateNewAccount);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.CreateNewAccount)]
|
||||
public async Task CreateNewAccountAsync(KernelProcessStepContext context, bool previousCheckSucceeded, NewCustomerForm customerDetails, List<ChatMessageContent> interactionTranscript, Kernel _kernel)
|
||||
{
|
||||
// Placeholder for a call to API to create new account for user
|
||||
var accountId = new Guid();
|
||||
AccountDetails accountDetails = new()
|
||||
{
|
||||
UserDateOfBirth = customerDetails.UserDateOfBirth,
|
||||
UserFirstName = customerDetails.UserFirstName,
|
||||
UserLastName = customerDetails.UserLastName,
|
||||
UserId = customerDetails.UserId,
|
||||
UserPhoneNumber = customerDetails.UserPhoneNumber,
|
||||
UserState = customerDetails.UserState,
|
||||
UserEmail = customerDetails.UserEmail,
|
||||
AccountId = accountId,
|
||||
AccountType = AccountType.PrimeABC,
|
||||
};
|
||||
|
||||
Console.WriteLine($"[ACCOUNT CREATION] New Account {accountId} created");
|
||||
|
||||
await context.EmitEventAsync(new()
|
||||
{
|
||||
Id = AccountOpeningEvents.NewMarketingRecordInfoReady,
|
||||
Data = new MarketingNewEntryDetails
|
||||
{
|
||||
AccountId = accountId,
|
||||
Name = $"{customerDetails.UserFirstName} {customerDetails.UserLastName}",
|
||||
PhoneNumber = customerDetails.UserPhoneNumber,
|
||||
Email = customerDetails.UserEmail,
|
||||
}
|
||||
});
|
||||
|
||||
await context.EmitEventAsync(new()
|
||||
{
|
||||
Id = AccountOpeningEvents.CRMRecordInfoReady,
|
||||
Data = new AccountUserInteractionDetails
|
||||
{
|
||||
AccountId = accountId,
|
||||
UserInteractionType = UserInteractionType.OpeningNewAccount,
|
||||
InteractionTranscript = interactionTranscript
|
||||
}
|
||||
});
|
||||
|
||||
await context.EmitEventAsync(new()
|
||||
{
|
||||
Id = AccountOpeningEvents.NewAccountDetailsReady,
|
||||
Data = accountDetails,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step02.Models;
|
||||
|
||||
namespace Step02.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Mock step that emulates the creation a new marketing user entry.
|
||||
/// </summary>
|
||||
public class NewMarketingEntryStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string CreateNewMarketingEntry = nameof(CreateNewMarketingEntry);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.CreateNewMarketingEntry)]
|
||||
public async Task CreateNewMarketingEntryAsync(KernelProcessStepContext context, MarketingNewEntryDetails userDetails, Kernel _kernel)
|
||||
{
|
||||
Console.WriteLine($"[MARKETING ENTRY CREATION] New Account {userDetails.AccountId} created");
|
||||
|
||||
// Placeholder for a call to API to create new entry of user for marketing purposes
|
||||
await context.EmitEventAsync(new() { Id = AccountOpeningEvents.NewMarketingEntryCreated, Data = true });
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SharedSteps;
|
||||
|
||||
namespace Step02.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ScriptedUserInputStep"/> Step with interactions that makes the Process fail due credit score failure
|
||||
/// </summary>
|
||||
public sealed class UserInputCreditScoreFailureInteractionStep : ScriptedUserInputStep
|
||||
{
|
||||
public override void PopulateUserInputs(UserInputState state)
|
||||
{
|
||||
state.UserInputs.Add("I would like to open an account");
|
||||
state.UserInputs.Add("My name is John Contoso, dob 01/01/1990");
|
||||
state.UserInputs.Add("I live in Washington and my phone number es 222-222-1234");
|
||||
state.UserInputs.Add("My userId is 987-654-3210");
|
||||
state.UserInputs.Add("My email is john.contoso@contoso.com, what else do you need?");
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SharedSteps;
|
||||
|
||||
namespace Step02.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ScriptedUserInputStep"/> Step with interactions that makes the Process fail due fraud detection failure
|
||||
/// </summary>
|
||||
public sealed class UserInputFraudFailureInteractionStep : ScriptedUserInputStep
|
||||
{
|
||||
public override void PopulateUserInputs(UserInputState state)
|
||||
{
|
||||
state.UserInputs.Add("I would like to open an account");
|
||||
state.UserInputs.Add("My name is John Contoso, dob 02/03/1990");
|
||||
state.UserInputs.Add("I live in Washington and my phone number es 222-222-1234");
|
||||
state.UserInputs.Add("My userId is 123-456-7890");
|
||||
state.UserInputs.Add("My email is john.contoso@contoso.com, what else do you need?");
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SharedSteps;
|
||||
|
||||
namespace Step02.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ScriptedUserInputStep"/> Step with interactions that makes the Process pass all steps and successfully open a new account
|
||||
/// </summary>
|
||||
public sealed class UserInputSuccessfulInteractionStep : ScriptedUserInputStep
|
||||
{
|
||||
public override void PopulateUserInputs(UserInputState state)
|
||||
{
|
||||
state.UserInputs.Add("I would like to open an account");
|
||||
state.UserInputs.Add("My name is John Contoso, dob 02/03/1990");
|
||||
state.UserInputs.Add("I live in Washington and my phone number es 222-222-1234");
|
||||
state.UserInputs.Add("My userId is 987-654-3210");
|
||||
state.UserInputs.Add("My email is john.contoso@contoso.com, what else do you need?");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step02.Models;
|
||||
|
||||
namespace Step02.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Mock step that emulates the creation of a Welcome Packet for a new user after account creation
|
||||
/// </summary>
|
||||
public class WelcomePacketStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string CreateWelcomePacket = nameof(CreateWelcomePacket);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.CreateWelcomePacket)]
|
||||
public async Task CreateWelcomePacketAsync(KernelProcessStepContext context, bool marketingEntryCreated, bool crmRecordCreated, AccountDetails accountDetails, Kernel _kernel)
|
||||
{
|
||||
Console.WriteLine($"[WELCOME PACKET] New Account {accountDetails.AccountId} created");
|
||||
|
||||
var mailMessage = $"""
|
||||
Dear {accountDetails.UserFirstName} {accountDetails.UserLastName}
|
||||
We are thrilled to inform you that you have successfully created a new PRIME ABC Account with us!
|
||||
|
||||
Account Details:
|
||||
Account Number: {accountDetails.AccountId}
|
||||
Account Type: {accountDetails.AccountType}
|
||||
|
||||
Please keep this confidential for security purposes.
|
||||
|
||||
Here is the contact information we have in file:
|
||||
|
||||
Email: {accountDetails.UserEmail}
|
||||
Phone: {accountDetails.UserPhoneNumber}
|
||||
|
||||
Thank you for opening an account with us!
|
||||
""";
|
||||
|
||||
await context.EmitEventAsync(new()
|
||||
{
|
||||
Id = AccountOpeningEvents.WelcomePacketCreated,
|
||||
Data = mailMessage,
|
||||
Visibility = KernelProcessEventVisibility.Public,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Step03.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Food Ingredients used in steps such GatherIngredientStep, CutFoodStep, FryFoodStep
|
||||
/// </summary>
|
||||
public enum FoodIngredients
|
||||
{
|
||||
Pototoes,
|
||||
Fish,
|
||||
Buns,
|
||||
Sauce,
|
||||
Condiments,
|
||||
None
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extensions to have access to friendly string names for <see cref="FoodIngredients"/>
|
||||
/// </summary>
|
||||
public static class FoodIngredientsExtensions
|
||||
{
|
||||
private static readonly Dictionary<FoodIngredients, string> s_foodIngredientsStrings = new()
|
||||
{
|
||||
{ FoodIngredients.Pototoes, "Potatoes" },
|
||||
{ FoodIngredients.Fish, "Fish" },
|
||||
{ FoodIngredients.Buns, "Buns" },
|
||||
{ FoodIngredients.Sauce, "Sauce" },
|
||||
{ FoodIngredients.Condiments, "Condiments" },
|
||||
{ FoodIngredients.None, "None" }
|
||||
};
|
||||
|
||||
public static string ToFriendlyString(this FoodIngredients ingredient)
|
||||
{
|
||||
return s_foodIngredientsStrings[ingredient];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Step03.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Food Items that can be prepared by the PrepareSingleFoodItemProcess
|
||||
/// </summary>
|
||||
public enum FoodItem
|
||||
{
|
||||
PotatoFries,
|
||||
FriedFish,
|
||||
FishSandwich,
|
||||
FishAndChips
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extensions to have access to friendly string names for <see cref="FoodItem"/>
|
||||
/// </summary>
|
||||
public static class FoodItemExtensions
|
||||
{
|
||||
private static readonly Dictionary<FoodItem, string> s_foodItemsStrings = new()
|
||||
{
|
||||
{ FoodItem.PotatoFries, "Potato Fries" },
|
||||
{ FoodItem.FriedFish, "Fried Fish" },
|
||||
{ FoodItem.FishSandwich, "Fish Sandwich" },
|
||||
{ FoodItem.FishAndChips, "Fish & Chips" },
|
||||
};
|
||||
|
||||
public static string ToFriendlyString(this FoodItem item)
|
||||
{
|
||||
return s_foodItemsStrings[item];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step03.Models;
|
||||
using Step03.Steps;
|
||||
|
||||
namespace Step03.Processes;
|
||||
|
||||
/// <summary>
|
||||
/// Sample process that showcases how to create a process with a fan in/fan out behavior and use of existing processes as steps.<br/>
|
||||
/// Visual reference of this process can be found in the <see href="https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/README.md#fish-and-chips-preparation-process" >diagram</see>
|
||||
/// </summary>
|
||||
public static class FishAndChipsProcess
|
||||
{
|
||||
public static class ProcessEvents
|
||||
{
|
||||
public const string PrepareFishAndChips = nameof(PrepareFishAndChips);
|
||||
public const string FishAndChipsReady = nameof(FishAndChipsReady);
|
||||
public const string FishAndChipsIngredientOutOfStock = nameof(FishAndChipsIngredientOutOfStock);
|
||||
}
|
||||
|
||||
public static ProcessBuilder CreateProcess(string processName = "FishAndChipsProcess")
|
||||
{
|
||||
var processBuilder = new ProcessBuilder(processName);
|
||||
var makeFriedFishStep = processBuilder.AddStepFromProcess(FriedFishProcess.CreateProcess());
|
||||
var makePotatoFriesStep = processBuilder.AddStepFromProcess(PotatoFriesProcess.CreateProcess());
|
||||
var addCondimentsStep = processBuilder.AddStepFromType<AddFishAndChipsCondimentsStep>();
|
||||
// An additional step that is the only one that emits an public event in a process can be added to maintain event names unique
|
||||
var externalStep = processBuilder.AddStepFromType<ExternalFishAndChipsStep>();
|
||||
|
||||
processBuilder
|
||||
.OnInputEvent(ProcessEvents.PrepareFishAndChips)
|
||||
.SendEventTo(makeFriedFishStep.WhereInputEventIs(FriedFishProcess.ProcessEvents.PrepareFriedFish))
|
||||
.SendEventTo(makePotatoFriesStep.WhereInputEventIs(PotatoFriesProcess.ProcessEvents.PreparePotatoFries));
|
||||
|
||||
makeFriedFishStep
|
||||
.OnEvent(FriedFishProcess.ProcessEvents.FriedFishReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(addCondimentsStep, parameterName: "fishActions"));
|
||||
|
||||
makePotatoFriesStep
|
||||
.OnEvent(PotatoFriesProcess.ProcessEvents.PotatoFriesReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(addCondimentsStep, parameterName: "potatoActions"));
|
||||
|
||||
addCondimentsStep
|
||||
.OnEvent(AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(externalStep));
|
||||
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
public static ProcessBuilder CreateProcessWithStatefulSteps(string processName = "FishAndChipsWithStatefulStepsProcess")
|
||||
{
|
||||
var processBuilder = new ProcessBuilder(processName);
|
||||
var makeFriedFishStep = processBuilder.AddStepFromProcess(FriedFishProcess.CreateProcessWithStatefulStepsV1());
|
||||
var makePotatoFriesStep = processBuilder.AddStepFromProcess(PotatoFriesProcess.CreateProcessWithStatefulSteps());
|
||||
var addCondimentsStep = processBuilder.AddStepFromType<AddFishAndChipsCondimentsStep>();
|
||||
// An additional step that is the only one that emits an public event in a process can be added to maintain event names unique
|
||||
var externalStep = processBuilder.AddStepFromType<ExternalFishAndChipsStep>();
|
||||
|
||||
processBuilder
|
||||
.OnInputEvent(ProcessEvents.PrepareFishAndChips)
|
||||
.SendEventTo(makeFriedFishStep.WhereInputEventIs(FriedFishProcess.ProcessEvents.PrepareFriedFish))
|
||||
.SendEventTo(makePotatoFriesStep.WhereInputEventIs(PotatoFriesProcess.ProcessEvents.PreparePotatoFries));
|
||||
|
||||
makeFriedFishStep
|
||||
.OnEvent(FriedFishProcess.ProcessEvents.FriedFishReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(addCondimentsStep, parameterName: "fishActions"));
|
||||
|
||||
makePotatoFriesStep
|
||||
.OnEvent(PotatoFriesProcess.ProcessEvents.PotatoFriesReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(addCondimentsStep, parameterName: "potatoActions"));
|
||||
|
||||
addCondimentsStep
|
||||
.OnEvent(AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(externalStep));
|
||||
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
private sealed class AddFishAndChipsCondimentsStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessFunctions
|
||||
{
|
||||
public const string AddCondiments = nameof(AddCondiments);
|
||||
}
|
||||
|
||||
public static class OutputEvents
|
||||
{
|
||||
public const string CondimentsAdded = nameof(CondimentsAdded);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessFunctions.AddCondiments)]
|
||||
public async Task AddCondimentsAsync(KernelProcessStepContext context, List<string> fishActions, List<string> potatoActions)
|
||||
{
|
||||
Console.WriteLine($"ADD_CONDIMENTS: Added condiments to Fish & Chips - Fish: {JsonSerializer.Serialize(fishActions)}, Potatoes: {JsonSerializer.Serialize(potatoActions)}");
|
||||
fishActions.AddRange(potatoActions);
|
||||
fishActions.Add(FoodIngredients.Condiments.ToFriendlyString());
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.CondimentsAdded, Data = fishActions });
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ExternalFishAndChipsStep : ExternalStep
|
||||
{
|
||||
public ExternalFishAndChipsStep() : base(ProcessEvents.FishAndChipsReady) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step03.Models;
|
||||
using Step03.Steps;
|
||||
|
||||
namespace Step03.Processes;
|
||||
|
||||
/// <summary>
|
||||
/// Sample process that showcases how to create a process with sequential steps and use of existing processes as steps.<br/>
|
||||
/// Visual reference of this process can be found in the <see href="https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/README.md#fish-sandwich-preparation-process" >diagram</see>
|
||||
/// </summary>
|
||||
public static class FishSandwichProcess
|
||||
{
|
||||
public static class ProcessEvents
|
||||
{
|
||||
public const string PrepareFishSandwich = nameof(PrepareFishSandwich);
|
||||
public const string FishSandwichReady = nameof(FishSandwichReady);
|
||||
}
|
||||
|
||||
public static ProcessBuilder CreateProcess(string processName = "FishSandwichProcess")
|
||||
{
|
||||
var processBuilder = new ProcessBuilder(processName);
|
||||
var makeFriedFishStep = processBuilder.AddStepFromProcess(FriedFishProcess.CreateProcess());
|
||||
var addBunsStep = processBuilder.AddStepFromType<AddBunsStep>();
|
||||
var addSpecialSauceStep = processBuilder.AddStepFromType<AddSpecialSauceStep>();
|
||||
// An additional step that is the only one that emits an public event in a process can be added to maintain event names unique
|
||||
var externalStep = processBuilder.AddStepFromType<ExternalFriedFishStep>();
|
||||
|
||||
processBuilder
|
||||
.OnInputEvent(ProcessEvents.PrepareFishSandwich)
|
||||
.SendEventTo(makeFriedFishStep.WhereInputEventIs(FriedFishProcess.ProcessEvents.PrepareFriedFish));
|
||||
|
||||
makeFriedFishStep
|
||||
.OnEvent(FriedFishProcess.ProcessEvents.FriedFishReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(addBunsStep));
|
||||
|
||||
addBunsStep
|
||||
.OnEvent(AddBunsStep.OutputEvents.BunsAdded)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(addSpecialSauceStep));
|
||||
|
||||
addSpecialSauceStep
|
||||
.OnEvent(AddSpecialSauceStep.OutputEvents.SpecialSauceAdded)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(externalStep));
|
||||
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
public static ProcessBuilder CreateProcessWithStatefulStepsV1(string processName = "FishSandwichWithStatefulStepsProcess")
|
||||
{
|
||||
var processBuilder = new ProcessBuilder(processName) { Version = "FishSandwich.V1" };
|
||||
var makeFriedFishStep = processBuilder.AddStepFromProcess(FriedFishProcess.CreateProcessWithStatefulStepsV1());
|
||||
var addBunsStep = processBuilder.AddStepFromType<AddBunsStep>();
|
||||
var addSpecialSauceStep = processBuilder.AddStepFromType<AddSpecialSauceStep>();
|
||||
// An additional step that is the only one that emits an public event in a process can be added to maintain event names unique
|
||||
var externalStep = processBuilder.AddStepFromType<ExternalFriedFishStep>();
|
||||
|
||||
processBuilder
|
||||
.OnInputEvent(ProcessEvents.PrepareFishSandwich)
|
||||
.SendEventTo(makeFriedFishStep.WhereInputEventIs(FriedFishProcess.ProcessEvents.PrepareFriedFish));
|
||||
|
||||
makeFriedFishStep
|
||||
.OnEvent(FriedFishProcess.ProcessEvents.FriedFishReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(addBunsStep));
|
||||
|
||||
addBunsStep
|
||||
.OnEvent(AddBunsStep.OutputEvents.BunsAdded)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(addSpecialSauceStep));
|
||||
|
||||
addSpecialSauceStep
|
||||
.OnEvent(AddSpecialSauceStep.OutputEvents.SpecialSauceAdded)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(externalStep));
|
||||
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName = "FishSandwichWithStatefulStepsProcess")
|
||||
{
|
||||
var processBuilder = new ProcessBuilder(processName) { Version = "FishSandwich.V2" };
|
||||
var makeFriedFishStep = processBuilder.AddStepFromProcess(FriedFishProcess.CreateProcessWithStatefulStepsV2("FriedFishStep"), aliases: ["FriedFishWithStatefulStepsProcess"]);
|
||||
var addBunsStep = processBuilder.AddStepFromType<AddBunsStep>();
|
||||
var addSpecialSauceStep = processBuilder.AddStepFromType<AddSpecialSauceStep>();
|
||||
// An additional step that is the only one that emits an public event in a process can be added to maintain event names unique
|
||||
var externalStep = processBuilder.AddStepFromType<ExternalFriedFishStep>();
|
||||
|
||||
processBuilder
|
||||
.OnInputEvent(ProcessEvents.PrepareFishSandwich)
|
||||
.SendEventTo(makeFriedFishStep.WhereInputEventIs(FriedFishProcess.ProcessEvents.PrepareFriedFish));
|
||||
|
||||
makeFriedFishStep
|
||||
.OnEvent(FriedFishProcess.ProcessEvents.FriedFishReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(addBunsStep));
|
||||
|
||||
addBunsStep
|
||||
.OnEvent(AddBunsStep.OutputEvents.BunsAdded)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(addSpecialSauceStep));
|
||||
|
||||
addSpecialSauceStep
|
||||
.OnEvent(AddSpecialSauceStep.OutputEvents.SpecialSauceAdded)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(externalStep));
|
||||
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
private sealed class AddBunsStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessFunctions
|
||||
{
|
||||
public const string AddBuns = nameof(AddBuns);
|
||||
}
|
||||
|
||||
public static class OutputEvents
|
||||
{
|
||||
public const string BunsAdded = nameof(BunsAdded);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessFunctions.AddBuns)]
|
||||
public async Task SliceFoodAsync(KernelProcessStepContext context, List<string> foodActions)
|
||||
{
|
||||
Console.WriteLine($"BUNS_ADDED_STEP: Buns added to ingredient {foodActions.First()}");
|
||||
foodActions.Add(FoodIngredients.Buns.ToFriendlyString());
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.BunsAdded, Data = foodActions });
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class AddSpecialSauceStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessFunctions
|
||||
{
|
||||
public const string AddSpecialSauce = nameof(AddSpecialSauce);
|
||||
}
|
||||
|
||||
public static class OutputEvents
|
||||
{
|
||||
public const string SpecialSauceAdded = nameof(SpecialSauceAdded);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessFunctions.AddSpecialSauce)]
|
||||
public async Task SliceFoodAsync(KernelProcessStepContext context, List<string> foodActions)
|
||||
{
|
||||
Console.WriteLine($"SPECIAL_SAUCE_ADDED: Special sauce added to ingredient {foodActions.First()}");
|
||||
foodActions.Add(FoodIngredients.Sauce.ToFriendlyString());
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.SpecialSauceAdded, Data = foodActions, Visibility = KernelProcessEventVisibility.Public });
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ExternalFriedFishStep : ExternalStep
|
||||
{
|
||||
public ExternalFriedFishStep() : base(ProcessEvents.FishSandwichReady) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Process;
|
||||
using Step03.Models;
|
||||
using Step03.Steps;
|
||||
namespace Step03.Processes;
|
||||
|
||||
/// <summary>
|
||||
/// Sample process that showcases how to create a process with sequential steps and reuse of existing steps.<br/>
|
||||
/// </summary>
|
||||
public static class FriedFishProcess
|
||||
{
|
||||
public static class ProcessEvents
|
||||
{
|
||||
public const string PrepareFriedFish = nameof(PrepareFriedFish);
|
||||
// When multiple processes use the same final step, the should event marked as public
|
||||
// so that the step event can be used as the output event of the process too.
|
||||
// In these samples both fried fish and potato fries end with FryStep success
|
||||
public const string FriedFishReady = FryFoodStep.OutputEvents.FriedFoodReady;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a visual reference of the FriedFishProcess check this
|
||||
/// <see href="https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/README.md#fried-fish-preparation-process" >diagram</see>
|
||||
/// </summary>
|
||||
/// <param name="processName">name of the process</param>
|
||||
/// <returns><see cref="ProcessBuilder"/></returns>
|
||||
public static ProcessBuilder CreateProcess(string processName = "FriedFishProcess")
|
||||
{
|
||||
var processBuilder = new ProcessBuilder(processName);
|
||||
|
||||
var gatherIngredientsStep = processBuilder.AddStepFromType<GatherFriedFishIngredientsStep>();
|
||||
var chopStep = processBuilder.AddStepFromType<CutFoodStep>();
|
||||
var fryStep = processBuilder.AddStepFromType<FryFoodStep>();
|
||||
|
||||
processBuilder
|
||||
.OnInputEvent(ProcessEvents.PrepareFriedFish)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
|
||||
|
||||
gatherIngredientsStep
|
||||
.OnEvent(GatherFriedFishIngredientsStep.OutputEvents.IngredientsGathered)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodStep.ProcessStepFunctions.ChopFood));
|
||||
|
||||
chopStep
|
||||
.OnEvent(CutFoodStep.OutputEvents.ChoppingReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(fryStep));
|
||||
|
||||
fryStep
|
||||
.OnEvent(FryFoodStep.OutputEvents.FoodRuined)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
|
||||
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
public static ProcessBuilder CreateProcessWithStatefulStepsV1(string processName = "FriedFishWithStatefulStepsProcess")
|
||||
{
|
||||
// It is recommended to specify process version in case this process is used as a step by another process
|
||||
var processBuilder = new ProcessBuilder(processName) { Version = "FriedFishProcess.v1" }; ;
|
||||
|
||||
var gatherIngredientsStep = processBuilder.AddStepFromType<GatherFriedFishIngredientsWithStockStep>();
|
||||
var chopStep = processBuilder.AddStepFromType<CutFoodStep>();
|
||||
var fryStep = processBuilder.AddStepFromType<FryFoodStep>();
|
||||
|
||||
processBuilder
|
||||
.OnInputEvent(ProcessEvents.PrepareFriedFish)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
|
||||
|
||||
gatherIngredientsStep
|
||||
.OnEvent(GatherFriedFishIngredientsWithStockStep.OutputEvents.IngredientsGathered)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodWithSharpeningStep.ProcessStepFunctions.ChopFood));
|
||||
|
||||
chopStep
|
||||
.OnEvent(CutFoodWithSharpeningStep.OutputEvents.ChoppingReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(fryStep));
|
||||
|
||||
fryStep
|
||||
.OnEvent(FryFoodStep.OutputEvents.FoodRuined)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
|
||||
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a visual reference of the FriedFishProcess with stateful steps check this
|
||||
/// <see href="https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/README.md#fried-fish-preparation-with-knife-sharpening-and-ingredient-stock-process" >diagram</see>
|
||||
/// </summary>
|
||||
/// <param name="processName">name of the process</param>
|
||||
/// <returns><see cref="ProcessBuilder"/></returns>
|
||||
public static ProcessBuilder CreateProcessWithStatefulStepsV2(string processName = "FriedFishWithStatefulStepsProcess")
|
||||
{
|
||||
// It is recommended to specify process version in case this process is used as a step by another process
|
||||
var processBuilder = new ProcessBuilder(processName) { Version = "FriedFishProcess.v2" };
|
||||
|
||||
var gatherIngredientsStep = processBuilder.AddStepFromType<GatherFriedFishIngredientsWithStockStep>(id: "gatherFishIngredientStep", aliases: ["GatherFriedFishIngredientsWithStockStep"]);
|
||||
var chopStep = processBuilder.AddStepFromType<CutFoodWithSharpeningStep>(id: "chopFishStep", aliases: ["CutFoodStep"]);
|
||||
var fryStep = processBuilder.AddStepFromType<FryFoodStep>(id: "fryFishStep", aliases: ["FryFoodStep"]);
|
||||
|
||||
processBuilder
|
||||
.OnInputEvent(ProcessEvents.PrepareFriedFish)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
|
||||
|
||||
gatherIngredientsStep
|
||||
.OnEvent(GatherFriedFishIngredientsWithStockStep.OutputEvents.IngredientsGathered)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodWithSharpeningStep.ProcessStepFunctions.ChopFood));
|
||||
|
||||
gatherIngredientsStep
|
||||
.OnEvent(GatherFriedFishIngredientsWithStockStep.OutputEvents.IngredientsOutOfStock)
|
||||
.StopProcess();
|
||||
|
||||
chopStep
|
||||
.OnEvent(CutFoodWithSharpeningStep.OutputEvents.ChoppingReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(fryStep));
|
||||
|
||||
chopStep
|
||||
.OnEvent(CutFoodWithSharpeningStep.OutputEvents.KnifeNeedsSharpening)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodWithSharpeningStep.ProcessStepFunctions.SharpenKnife));
|
||||
|
||||
chopStep
|
||||
.OnEvent(CutFoodWithSharpeningStep.OutputEvents.KnifeSharpened)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodWithSharpeningStep.ProcessStepFunctions.ChopFood));
|
||||
|
||||
fryStep
|
||||
.OnEvent(FryFoodStep.OutputEvents.FoodRuined)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
|
||||
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
[KernelProcessStepMetadata("GatherFishIngredient.V1")]
|
||||
private sealed class GatherFriedFishIngredientsStep : GatherIngredientsStep
|
||||
{
|
||||
public GatherFriedFishIngredientsStep() : base(FoodIngredients.Fish) { }
|
||||
}
|
||||
|
||||
[KernelProcessStepMetadata("GatherFishIngredient.V2")]
|
||||
private sealed class GatherFriedFishIngredientsWithStockStep : GatherIngredientsWithStockStep
|
||||
{
|
||||
public GatherFriedFishIngredientsWithStockStep() : base(FoodIngredients.Fish) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step03.Models;
|
||||
using Step03.Steps;
|
||||
|
||||
namespace Step03.Processes;
|
||||
|
||||
/// <summary>
|
||||
/// Sample process that showcases how to create a process with sequential steps and reuse of existing steps.<br/>
|
||||
/// </summary>
|
||||
public static class PotatoFriesProcess
|
||||
{
|
||||
public static class ProcessEvents
|
||||
{
|
||||
public const string PreparePotatoFries = nameof(PreparePotatoFries);
|
||||
// When multiple processes use the same final step, the should event marked as public
|
||||
// so that the step event can be used as the output event of the process too.
|
||||
// In these samples both fried fish and potato fries end with FryStep success
|
||||
public const string PotatoFriesReady = nameof(FryFoodStep.OutputEvents.FriedFoodReady);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a visual reference of the PotatoFriesProcess check this
|
||||
/// <see href="https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/README.md#potato-fries-preparation-process" >diagram</see>
|
||||
/// </summary>
|
||||
/// <param name="processName">name of the process</param>
|
||||
/// <returns><see cref="ProcessBuilder"/></returns>
|
||||
public static ProcessBuilder CreateProcess(string processName = "PotatoFriesProcess")
|
||||
{
|
||||
var processBuilder = new ProcessBuilder(processName);
|
||||
|
||||
var gatherIngredientsStep = processBuilder.AddStepFromType<GatherPotatoFriesIngredientsStep>();
|
||||
var sliceStep = processBuilder.AddStepFromType<CutFoodStep>("sliceStep");
|
||||
var fryStep = processBuilder.AddStepFromType<FryFoodStep>();
|
||||
|
||||
processBuilder
|
||||
.OnInputEvent(ProcessEvents.PreparePotatoFries)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
|
||||
|
||||
gatherIngredientsStep
|
||||
.OnEvent(GatherPotatoFriesIngredientsStep.OutputEvents.IngredientsGathered)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(sliceStep, functionName: CutFoodStep.ProcessStepFunctions.SliceFood));
|
||||
|
||||
sliceStep
|
||||
.OnEvent(CutFoodStep.OutputEvents.SlicingReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(fryStep));
|
||||
|
||||
fryStep
|
||||
.OnEvent(FryFoodStep.OutputEvents.FoodRuined)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
|
||||
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a visual reference of the PotatoFriesProcess with stateful steps check this
|
||||
/// <see href="https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/README.md#potato-fries-preparation-with-knife-sharpening-and-ingredient-stock-process" >diagram</see>
|
||||
/// </summary>
|
||||
/// <param name="processName">name of the process</param>
|
||||
/// <returns><see cref="ProcessBuilder"/></returns>
|
||||
public static ProcessBuilder CreateProcessWithStatefulSteps(string processName = "PotatoFriesWithStatefulStepsProcess")
|
||||
{
|
||||
var processBuilder = new ProcessBuilder(processName);
|
||||
|
||||
var gatherIngredientsStep = processBuilder.AddStepFromType<GatherPotatoFriesIngredientsWithStockStep>();
|
||||
var sliceStep = processBuilder.AddStepFromType<CutFoodWithSharpeningStep>("sliceStep");
|
||||
var fryStep = processBuilder.AddStepFromType<FryFoodStep>();
|
||||
|
||||
processBuilder
|
||||
.OnInputEvent(ProcessEvents.PreparePotatoFries)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
|
||||
|
||||
gatherIngredientsStep
|
||||
.OnEvent(GatherPotatoFriesIngredientsWithStockStep.OutputEvents.IngredientsGathered)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(sliceStep, functionName: CutFoodWithSharpeningStep.ProcessStepFunctions.SliceFood));
|
||||
|
||||
gatherIngredientsStep
|
||||
.OnEvent(GatherPotatoFriesIngredientsWithStockStep.OutputEvents.IngredientsOutOfStock)
|
||||
.StopProcess();
|
||||
|
||||
sliceStep
|
||||
.OnEvent(CutFoodWithSharpeningStep.OutputEvents.SlicingReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(fryStep));
|
||||
|
||||
sliceStep
|
||||
.OnEvent(CutFoodWithSharpeningStep.OutputEvents.KnifeNeedsSharpening)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(sliceStep, functionName: CutFoodWithSharpeningStep.ProcessStepFunctions.SharpenKnife));
|
||||
|
||||
sliceStep
|
||||
.OnEvent(CutFoodWithSharpeningStep.OutputEvents.KnifeSharpened)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(sliceStep, functionName: CutFoodWithSharpeningStep.ProcessStepFunctions.SliceFood));
|
||||
|
||||
fryStep
|
||||
.OnEvent(FryFoodStep.OutputEvents.FoodRuined)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
|
||||
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
private sealed class GatherPotatoFriesIngredientsStep : GatherIngredientsStep
|
||||
{
|
||||
public GatherPotatoFriesIngredientsStep() : base(FoodIngredients.Pototoes) { }
|
||||
}
|
||||
|
||||
private sealed class GatherPotatoFriesIngredientsWithStockStep : GatherIngredientsWithStockStep
|
||||
{
|
||||
public GatherPotatoFriesIngredientsWithStockStep() : base(FoodIngredients.Pototoes) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step03.Models;
|
||||
using Step03.Steps;
|
||||
|
||||
namespace Step03.Processes;
|
||||
|
||||
/// <summary>
|
||||
/// Sample process that showcases how to create a selecting fan out process
|
||||
/// For a visual reference of the FriedFishProcess check this <see href="https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/README.md#single-order-preparation-process" >diagram</see>
|
||||
/// </summary>
|
||||
public static class SingleFoodItemProcess
|
||||
{
|
||||
public static class ProcessEvents
|
||||
{
|
||||
public const string SingleOrderReceived = nameof(SingleOrderReceived);
|
||||
public const string SingleOrderReady = nameof(SingleOrderReady);
|
||||
}
|
||||
|
||||
public static ProcessBuilder CreateProcess(string processName = "SingleFoodItemProcess")
|
||||
{
|
||||
var processBuilder = new ProcessBuilder(processName);
|
||||
|
||||
var dispatchOrderStep = processBuilder.AddStepFromType<DispatchSingleOrderStep>();
|
||||
var makeFriedFishStep = processBuilder.AddStepFromProcess(FriedFishProcess.CreateProcess());
|
||||
var makePotatoFriesStep = processBuilder.AddStepFromProcess(PotatoFriesProcess.CreateProcess());
|
||||
var makeFishSandwichStep = processBuilder.AddStepFromProcess(FishSandwichProcess.CreateProcess());
|
||||
var makeFishAndChipsStep = processBuilder.AddStepFromProcess(FishAndChipsProcess.CreateProcess());
|
||||
var packOrderStep = processBuilder.AddStepFromType<PackOrderStep>();
|
||||
var externalStep = processBuilder.AddStepFromType<ExternalSingleOrderStep>();
|
||||
|
||||
processBuilder
|
||||
.OnInputEvent(ProcessEvents.SingleOrderReceived)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(dispatchOrderStep));
|
||||
|
||||
dispatchOrderStep
|
||||
.OnEvent(DispatchSingleOrderStep.OutputEvents.PrepareFriedFish)
|
||||
.SendEventTo(makeFriedFishStep.WhereInputEventIs(FriedFishProcess.ProcessEvents.PrepareFriedFish));
|
||||
|
||||
dispatchOrderStep
|
||||
.OnEvent(DispatchSingleOrderStep.OutputEvents.PrepareFries)
|
||||
.SendEventTo(makePotatoFriesStep.WhereInputEventIs(PotatoFriesProcess.ProcessEvents.PreparePotatoFries));
|
||||
|
||||
dispatchOrderStep
|
||||
.OnEvent(DispatchSingleOrderStep.OutputEvents.PrepareFishSandwich)
|
||||
.SendEventTo(makeFishSandwichStep.WhereInputEventIs(FishSandwichProcess.ProcessEvents.PrepareFishSandwich));
|
||||
|
||||
dispatchOrderStep
|
||||
.OnEvent(DispatchSingleOrderStep.OutputEvents.PrepareFishAndChips)
|
||||
.SendEventTo(makeFishAndChipsStep.WhereInputEventIs(FishAndChipsProcess.ProcessEvents.PrepareFishAndChips));
|
||||
|
||||
makeFriedFishStep
|
||||
.OnEvent(FriedFishProcess.ProcessEvents.FriedFishReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(packOrderStep));
|
||||
|
||||
makePotatoFriesStep
|
||||
.OnEvent(PotatoFriesProcess.ProcessEvents.PotatoFriesReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(packOrderStep));
|
||||
|
||||
makeFishSandwichStep
|
||||
.OnEvent(FishSandwichProcess.ProcessEvents.FishSandwichReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(packOrderStep));
|
||||
|
||||
makeFishAndChipsStep
|
||||
.OnEvent(FishAndChipsProcess.ProcessEvents.FishAndChipsReady)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(packOrderStep));
|
||||
|
||||
packOrderStep
|
||||
.OnEvent(PackOrderStep.OutputEvents.FoodPacked)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(externalStep));
|
||||
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
private sealed class DispatchSingleOrderStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessFunctions
|
||||
{
|
||||
public const string PrepareSingleOrder = nameof(PrepareSingleOrder);
|
||||
}
|
||||
|
||||
public static class OutputEvents
|
||||
{
|
||||
public const string PrepareFries = nameof(PrepareFries);
|
||||
public const string PrepareFriedFish = nameof(PrepareFriedFish);
|
||||
public const string PrepareFishSandwich = nameof(PrepareFishSandwich);
|
||||
public const string PrepareFishAndChips = nameof(PrepareFishAndChips);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessFunctions.PrepareSingleOrder)]
|
||||
public async Task DispatchSingleOrderAsync(KernelProcessStepContext context, FoodItem foodItem)
|
||||
{
|
||||
var foodName = foodItem.ToFriendlyString();
|
||||
Console.WriteLine($"DISPATCH_SINGLE_ORDER: Dispatching '{foodName}'!");
|
||||
var foodActions = new List<string>();
|
||||
|
||||
switch (foodItem)
|
||||
{
|
||||
case FoodItem.PotatoFries:
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.PrepareFries, Data = foodActions });
|
||||
break;
|
||||
case FoodItem.FriedFish:
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.PrepareFriedFish, Data = foodActions });
|
||||
break;
|
||||
case FoodItem.FishSandwich:
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.PrepareFishSandwich, Data = foodActions });
|
||||
break;
|
||||
case FoodItem.FishAndChips:
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.PrepareFishAndChips, Data = foodActions });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PackOrderStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessFunctions
|
||||
{
|
||||
public const string PackFood = nameof(PackFood);
|
||||
}
|
||||
public static class OutputEvents
|
||||
{
|
||||
public const string FoodPacked = nameof(FoodPacked);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessFunctions.PackFood)]
|
||||
public async Task PackFoodAsync(KernelProcessStepContext context, List<string> foodActions)
|
||||
{
|
||||
Console.WriteLine($"PACKING_FOOD: Food {foodActions.First()} Packed! - {JsonSerializer.Serialize(foodActions)}");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.FoodPacked });
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ExternalSingleOrderStep : ExternalStep
|
||||
{
|
||||
public ExternalSingleOrderStep() : base(ProcessEvents.SingleOrderReady) { }
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"stepsState": {
|
||||
"FriedFishWithStatefulStepsProcess": {
|
||||
"$type": "Process",
|
||||
"stepsState": {
|
||||
"GatherFriedFishIngredientsWithStockStep": {
|
||||
"$type": "Step",
|
||||
"id": "7d4c02d000a744f490f2b5f9bad721fb",
|
||||
"name": "GatherFriedFishIngredientsWithStockStep",
|
||||
"versionInfo": "GatherFishIngredient.V2",
|
||||
"state": {
|
||||
"IngredientsStock": 2
|
||||
}
|
||||
},
|
||||
"CutFoodStep": {
|
||||
"$type": "Step",
|
||||
"id": "f147010a57d34587a3dc1ed4677e5163",
|
||||
"name": "CutFoodStep",
|
||||
"versionInfo": "CutFoodStep.V1"
|
||||
},
|
||||
"FryFoodStep": {
|
||||
"$type": "Step",
|
||||
"id": "78cc5af4106549afb74d7a6813016f87",
|
||||
"name": "FryFoodStep",
|
||||
"versionInfo": "FryFoodStep.V1"
|
||||
}
|
||||
},
|
||||
"id": "282717158b9f49e5b1acce81429610e0",
|
||||
"name": "FriedFishWithStatefulStepsProcess",
|
||||
"versionInfo": "FriedFishProcess.v1"
|
||||
},
|
||||
"AddBunsStep": {
|
||||
"$type": "Step",
|
||||
"id": "31e953154e574470911d168a39588ed8",
|
||||
"name": "AddBunsStep",
|
||||
"versionInfo": "v1"
|
||||
},
|
||||
"AddSpecialSauceStep": {
|
||||
"$type": "Step",
|
||||
"id": "67ee29ff28e4446d8046417675ec21e8",
|
||||
"name": "AddSpecialSauceStep",
|
||||
"versionInfo": "v1"
|
||||
},
|
||||
"ExternalFriedFishStep": {
|
||||
"$type": "Step",
|
||||
"id": "873b1c8dee45412e975a5e8db2ed0b43",
|
||||
"name": "ExternalFriedFishStep",
|
||||
"versionInfo": "v1"
|
||||
}
|
||||
},
|
||||
"id": "af40089f-e57b-46d1-a15b-40c0d7f3800f",
|
||||
"name": "FishSandwichWithStatefulStepsProcess",
|
||||
"versionInfo": "FishSandwich.V1"
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"$type": "Process",
|
||||
"stepsState": {
|
||||
"FriedFishWithStatefulStepsProcess": {
|
||||
"$type": "Process",
|
||||
"stepsState": {
|
||||
"GatherFriedFishIngredientsWithStockStep": {
|
||||
"$type": "Step",
|
||||
"id": "2908f8c88cf0476a8e0075c3a8020d5d",
|
||||
"name": "GatherFriedFishIngredientsWithStockStep",
|
||||
"versionInfo": "GatherFishIngredient.V2",
|
||||
"state": {
|
||||
"IngredientsStock": 1
|
||||
}
|
||||
},
|
||||
"CutFoodStep": {
|
||||
"$type": "Step",
|
||||
"id": "014388cf0bbd41119b8730dfc4b0b459",
|
||||
"name": "CutFoodStep",
|
||||
"versionInfo": "CutFoodStep.V1"
|
||||
},
|
||||
"FryFoodStep": {
|
||||
"$type": "Step",
|
||||
"id": "c55af0425d864c4e97b6ae67bd715480",
|
||||
"name": "FryFoodStep",
|
||||
"versionInfo": "FryFoodStep.V1"
|
||||
}
|
||||
},
|
||||
"id": "cab89a17aeae4b9a97568967dbf1ea47",
|
||||
"name": "FriedFishWithStatefulStepsProcess",
|
||||
"versionInfo": "FriedFishProcess.v1"
|
||||
},
|
||||
"AddBunsStep": {
|
||||
"$type": "Step",
|
||||
"id": "35d09b83dea24ddf8e0c24fbe6a3746c",
|
||||
"name": "AddBunsStep",
|
||||
"versionInfo": "v1"
|
||||
},
|
||||
"AddSpecialSauceStep": {
|
||||
"$type": "Step",
|
||||
"id": "aa0d408976574afea94387e3da7ca111",
|
||||
"name": "AddSpecialSauceStep",
|
||||
"versionInfo": "v1"
|
||||
},
|
||||
"ExternalFriedFishStep": {
|
||||
"$type": "Step",
|
||||
"id": "2eda38b8ee8745a4ab8b21f4fa01d173",
|
||||
"name": "ExternalFriedFishStep",
|
||||
"versionInfo": "v1"
|
||||
}
|
||||
},
|
||||
"id": "973b06f1-a522-4d2d-9e1c-ec45a07e275c",
|
||||
"name": "FishSandwichWithStatefulStepsProcess",
|
||||
"versionInfo": "FishSandwich.V1"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"stepsState": {
|
||||
"GatherFriedFishIngredientsWithStockStep": {
|
||||
"$type": "Step",
|
||||
"id": "77c2f967cd354e66a51828e9755d2f07",
|
||||
"name": "GatherFriedFishIngredientsWithStockStep",
|
||||
"versionInfo": "GatherFishIngredient.V2",
|
||||
"state": {
|
||||
"IngredientsStock": 4
|
||||
}
|
||||
},
|
||||
"CutFoodStep": {
|
||||
"$type": "Step",
|
||||
"id": "9276d03e64c44a6792d5fd81bd0dc143",
|
||||
"name": "CutFoodStep",
|
||||
"versionInfo": "CutFoodStep.V1"
|
||||
},
|
||||
"FryFoodStep": {
|
||||
"$type": "Step",
|
||||
"id": "af2a00be4fe2408181ab5654318ed56b",
|
||||
"name": "FryFoodStep",
|
||||
"versionInfo": "FryFoodStep.V1"
|
||||
}
|
||||
},
|
||||
"id": "2050a24b-3e9d-418a-8413-74cadf4f6b4c",
|
||||
"name": "FriedFishWithStatefulStepsProcess",
|
||||
"versionInfo": "FriedFishProcess.v1"
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$type": "Process",
|
||||
"stepsState": {
|
||||
"GatherFriedFishIngredientsWithStockStep": {
|
||||
"$type": "Step",
|
||||
"id": "92a4cda38c7248648b0aa7ffaaa57f21",
|
||||
"name": "GatherFriedFishIngredientsWithStockStep",
|
||||
"versionInfo": "GatherFishIngredient.V2",
|
||||
"state": {
|
||||
"IngredientsStock": 1
|
||||
}
|
||||
},
|
||||
"CutFoodStep": {
|
||||
"$type": "Step",
|
||||
"id": "7ace89e38e1c48b0b3a700b40d160c68",
|
||||
"name": "CutFoodStep",
|
||||
"versionInfo": "CutFoodStep.V1"
|
||||
},
|
||||
"FryFoodStep": {
|
||||
"$type": "Step",
|
||||
"id": "09bc39ba6d9745439c7c792b8dac0af7",
|
||||
"name": "FryFoodStep",
|
||||
"versionInfo": "FryFoodStep.V1"
|
||||
}
|
||||
},
|
||||
"id": "669c5850-9efc-4585-b3f0-9291a4471887",
|
||||
"name": "FriedFishWithStatefulStepsProcess",
|
||||
"versionInfo": "FriedFishProcess.v1"
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$type": "Process",
|
||||
"stepsState": {
|
||||
"GatherFriedFishIngredientsWithStockStep": {
|
||||
"$type": "Step",
|
||||
"id": "92a4cda38c7248648b0aa7ffaaa57f21",
|
||||
"name": "GatherFriedFishIngredientsWithStockStep",
|
||||
"versionInfo": "GatherFishIngredient.V2",
|
||||
"state": {
|
||||
"IngredientsStock": 0
|
||||
}
|
||||
},
|
||||
"CutFoodStep": {
|
||||
"$type": "Step",
|
||||
"id": "7ace89e38e1c48b0b3a700b40d160c68",
|
||||
"name": "CutFoodStep",
|
||||
"versionInfo": "CutFoodStep.V1"
|
||||
},
|
||||
"FryFoodStep": {
|
||||
"$type": "Step",
|
||||
"id": "09bc39ba6d9745439c7c792b8dac0af7",
|
||||
"name": "FryFoodStep",
|
||||
"versionInfo": "FryFoodStep.V1"
|
||||
}
|
||||
},
|
||||
"id": "669c5850-9efc-4585-b3f0-9291a4471887",
|
||||
"name": "FriedFishWithStatefulStepsProcess",
|
||||
"versionInfo": "FriedFishProcess.v1"
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Process.Models;
|
||||
using Microsoft.SemanticKernel.Process.Tools;
|
||||
using Step03.Processes;
|
||||
using Utilities;
|
||||
|
||||
namespace Step03;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate creation of <see cref="KernelProcess"/> and
|
||||
/// eliciting different food related events.
|
||||
/// For visual reference of the processes used here check the diagram in: https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step03a_foodPreparation
|
||||
/// </summary>
|
||||
public class Step03a_FoodPreparation(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true)
|
||||
{
|
||||
// Target Open AI Services
|
||||
protected override bool ForceOpenAI => true;
|
||||
|
||||
#region Stateless Processes
|
||||
[Fact]
|
||||
public async Task UsePrepareFriedFishProcessAsync()
|
||||
{
|
||||
var process = FriedFishProcess.CreateProcess();
|
||||
await UsePrepareSpecificProductAsync(process, FriedFishProcess.ProcessEvents.PrepareFriedFish);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsePreparePotatoFriesProcessAsync()
|
||||
{
|
||||
var process = PotatoFriesProcess.CreateProcess();
|
||||
await UsePrepareSpecificProductAsync(process, PotatoFriesProcess.ProcessEvents.PreparePotatoFries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsePrepareFishSandwichProcessAsync()
|
||||
{
|
||||
var process = FishSandwichProcess.CreateProcess();
|
||||
|
||||
string mermaidGraph = process.ToMermaid(1);
|
||||
Console.WriteLine($"=== Start - Mermaid Diagram for '{process.Name}' ===");
|
||||
Console.WriteLine(mermaidGraph);
|
||||
Console.WriteLine($"=== End - Mermaid Diagram for '{process.Name}' ===");
|
||||
|
||||
await UsePrepareSpecificProductAsync(process, FishSandwichProcess.ProcessEvents.PrepareFishSandwich);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsePrepareFishAndChipsProcessAsync()
|
||||
{
|
||||
var process = FishAndChipsProcess.CreateProcess();
|
||||
await UsePrepareSpecificProductAsync(process, FishAndChipsProcess.ProcessEvents.PrepareFishAndChips);
|
||||
}
|
||||
#endregion
|
||||
#region Stateful Processes
|
||||
/// <summary>
|
||||
/// Test case that showcase when the same process is build multiple times, it will have different initial states
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Fact]
|
||||
public async Task UsePrepareStatefulFriedFishProcessNoSharedStateAsync()
|
||||
{
|
||||
var processBuilder = FriedFishProcess.CreateProcessWithStatefulStepsV1();
|
||||
var externalTriggerEvent = FriedFishProcess.ProcessEvents.PrepareFriedFish;
|
||||
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
|
||||
// Assert
|
||||
Console.WriteLine($"=== Start SK Process '{processBuilder.Name}' ===");
|
||||
await ExecuteProcessWithStateAsync(processBuilder.Build(), kernel, externalTriggerEvent, "Order 1");
|
||||
await ExecuteProcessWithStateAsync(processBuilder.Build(), kernel, externalTriggerEvent, "Order 2");
|
||||
Console.WriteLine($"=== End SK Process '{processBuilder.Name}' ===");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case that showcase when the same process is build once and used multiple times, it will have share the state
|
||||
/// and the state of the steps will become the initial state of the next running process
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Fact]
|
||||
public async Task UsePrepareStatefulFriedFishProcessSharedStateAsync()
|
||||
{
|
||||
var processBuilder = FriedFishProcess.CreateProcessWithStatefulStepsV2();
|
||||
var externalTriggerEvent = FriedFishProcess.ProcessEvents.PrepareFriedFish;
|
||||
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
KernelProcess kernelProcess = processBuilder.Build();
|
||||
|
||||
Console.WriteLine($"=== Start SK Process '{processBuilder.Name}' ===");
|
||||
await ExecuteProcessWithStateAsync(kernelProcess, kernel, externalTriggerEvent, "Order 1");
|
||||
await ExecuteProcessWithStateAsync(kernelProcess, kernel, externalTriggerEvent, "Order 2");
|
||||
await ExecuteProcessWithStateAsync(kernelProcess, kernel, externalTriggerEvent, "Order 3");
|
||||
Console.WriteLine($"=== End SK Process '{processBuilder.Name}' ===");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsePrepareStatefulPotatoFriesProcessSharedStateAsync()
|
||||
{
|
||||
var processBuilder = PotatoFriesProcess.CreateProcessWithStatefulSteps();
|
||||
var externalTriggerEvent = PotatoFriesProcess.ProcessEvents.PreparePotatoFries;
|
||||
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
KernelProcess kernelProcess = processBuilder.Build();
|
||||
|
||||
Console.WriteLine($"=== Start SK Process '{processBuilder.Name}' ===");
|
||||
await ExecuteProcessWithStateAsync(kernelProcess, kernel, externalTriggerEvent, "Order 1");
|
||||
await ExecuteProcessWithStateAsync(kernelProcess, kernel, externalTriggerEvent, "Order 2");
|
||||
await ExecuteProcessWithStateAsync(kernelProcess, kernel, externalTriggerEvent, "Order 3");
|
||||
Console.WriteLine($"=== End SK Process '{processBuilder.Name}' ===");
|
||||
}
|
||||
|
||||
private async Task<KernelProcess> ExecuteProcessWithStateAsync(KernelProcess process, Kernel kernel, string externalTriggerEvent, string orderLabel = "Order 1")
|
||||
{
|
||||
Console.WriteLine($"=== {orderLabel} ===");
|
||||
var runningProcess = await process.StartAsync(kernel, new KernelProcessEvent()
|
||||
{
|
||||
Id = externalTriggerEvent,
|
||||
Data = new List<string>()
|
||||
});
|
||||
return await runningProcess.GetStateAsync();
|
||||
}
|
||||
|
||||
#region Running processes and saving Process State Metadata in a file locally
|
||||
[Fact]
|
||||
public async Task RunAndStoreStatefulFriedFishProcessStateAsync()
|
||||
{
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
ProcessBuilder builder = FriedFishProcess.CreateProcessWithStatefulStepsV1();
|
||||
KernelProcess friedFishProcess = builder.Build();
|
||||
|
||||
var executedProcess = await ExecuteProcessWithStateAsync(friedFishProcess, kernel, externalTriggerEvent: FriedFishProcess.ProcessEvents.PrepareFriedFish);
|
||||
var processState = executedProcess.ToProcessStateMetadata();
|
||||
DumpProcessStateMetadataLocally(processState, _statefulFriedFishProcessFilename);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAndStoreStatefulFishSandwichProcessStateAsync()
|
||||
{
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
ProcessBuilder builder = FishSandwichProcess.CreateProcessWithStatefulStepsV1();
|
||||
KernelProcess friedFishProcess = builder.Build();
|
||||
|
||||
var executedProcess = await ExecuteProcessWithStateAsync(friedFishProcess, kernel, externalTriggerEvent: FishSandwichProcess.ProcessEvents.PrepareFishSandwich);
|
||||
var processState = executedProcess.ToProcessStateMetadata();
|
||||
DumpProcessStateMetadataLocally(processState, _statefulFishSandwichProcessFilename);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Reading State from local file and apply to existing ProcessBuilder
|
||||
[Fact]
|
||||
public async Task RunStatefulFriedFishProcessFromFileAsync()
|
||||
{
|
||||
var processState = LoadProcessStateMetadata(this._statefulFriedFishProcessFilename);
|
||||
Assert.NotNull(processState);
|
||||
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
ProcessBuilder processBuilder = FriedFishProcess.CreateProcessWithStatefulStepsV1();
|
||||
KernelProcess processFromFile = processBuilder.Build(processState);
|
||||
|
||||
await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FriedFishProcess.ProcessEvents.PrepareFriedFish);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStatefulFriedFishProcessWithLowStockFromFileAsync()
|
||||
{
|
||||
var processState = LoadProcessStateMetadata(this._statefulFriedFishLowStockProcessFilename);
|
||||
Assert.NotNull(processState);
|
||||
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
ProcessBuilder processBuilder = FriedFishProcess.CreateProcessWithStatefulStepsV1();
|
||||
KernelProcess processFromFile = processBuilder.Build(processState);
|
||||
|
||||
await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FriedFishProcess.ProcessEvents.PrepareFriedFish);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStatefulFriedFishProcessWithNoStockFromFileAsync()
|
||||
{
|
||||
var processState = LoadProcessStateMetadata(this._statefulFriedFishNoStockProcessFilename);
|
||||
Assert.NotNull(processState);
|
||||
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
ProcessBuilder processBuilder = FriedFishProcess.CreateProcessWithStatefulStepsV1();
|
||||
KernelProcess processFromFile = processBuilder.Build(processState);
|
||||
|
||||
await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FriedFishProcess.ProcessEvents.PrepareFriedFish);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStatefulFishSandwichProcessFromFileAsync()
|
||||
{
|
||||
var processState = LoadProcessStateMetadata(this._statefulFishSandwichProcessFilename);
|
||||
Assert.NotNull(processState);
|
||||
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
ProcessBuilder processBuilder = FishSandwichProcess.CreateProcessWithStatefulStepsV1();
|
||||
KernelProcess processFromFile = processBuilder.Build(processState);
|
||||
|
||||
await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FishSandwichProcess.ProcessEvents.PrepareFishSandwich);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStatefulFishSandwichProcessWithLowStockFromFileAsync()
|
||||
{
|
||||
var processState = LoadProcessStateMetadata(this._statefulFishSandwichLowStockProcessFilename);
|
||||
Assert.NotNull(processState);
|
||||
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
ProcessBuilder processBuilder = FishSandwichProcess.CreateProcessWithStatefulStepsV1();
|
||||
KernelProcess processFromFile = processBuilder.Build(processState);
|
||||
|
||||
await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FishSandwichProcess.ProcessEvents.PrepareFishSandwich);
|
||||
}
|
||||
|
||||
#region Versioning compatibiily scenarios: Loading State generated with previous version of process
|
||||
[Fact]
|
||||
public async Task RunStatefulFriedFishV2ProcessWithLowStockV1StateFromFileAsync()
|
||||
{
|
||||
var processState = LoadProcessStateMetadata(this._statefulFriedFishLowStockProcessFilename);
|
||||
Assert.NotNull(processState);
|
||||
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
ProcessBuilder processBuilder = FriedFishProcess.CreateProcessWithStatefulStepsV2();
|
||||
KernelProcess processFromFile = processBuilder.Build(processState);
|
||||
|
||||
await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FriedFishProcess.ProcessEvents.PrepareFriedFish);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunStatefulFishSandwichV2ProcessWithLowStockV1StateFromFileAsync()
|
||||
{
|
||||
var processState = LoadProcessStateMetadata(this._statefulFishSandwichLowStockProcessFilename);
|
||||
Assert.NotNull(processState);
|
||||
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
ProcessBuilder processBuilder = FishSandwichProcess.CreateProcessWithStatefulStepsV2();
|
||||
KernelProcess processFromFile = processBuilder.Build(processState);
|
||||
|
||||
await ExecuteProcessWithStateAsync(processFromFile, kernel, externalTriggerEvent: FishSandwichProcess.ProcessEvents.PrepareFishSandwich);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
#endregion
|
||||
protected async Task UsePrepareSpecificProductAsync(ProcessBuilder processBuilder, string externalTriggerEvent)
|
||||
{
|
||||
// Arrange
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
|
||||
// Act
|
||||
KernelProcess kernelProcess = processBuilder.Build();
|
||||
|
||||
// Assert
|
||||
Console.WriteLine($"=== Start SK Process '{processBuilder.Name}' ===");
|
||||
await using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent()
|
||||
{
|
||||
Id = externalTriggerEvent, Data = new List<string>()
|
||||
});
|
||||
Console.WriteLine($"=== End SK Process '{processBuilder.Name}' ===");
|
||||
}
|
||||
|
||||
// Step03a Utils for saving and loading SK Processes from/to repository
|
||||
private readonly string _step03RelativePath = Path.Combine("Step03", "ProcessesStates");
|
||||
private readonly string _statefulFriedFishProcessFilename = "FriedFishProcessStateSuccess.json";
|
||||
private readonly string _statefulFriedFishLowStockProcessFilename = "FriedFishProcessStateSuccessLowStock.json";
|
||||
private readonly string _statefulFriedFishNoStockProcessFilename = "FriedFishProcessStateSuccessNoStock.json";
|
||||
private readonly string _statefulFishSandwichProcessFilename = "FishSandwichStateProcessSuccess.json";
|
||||
private readonly string _statefulFishSandwichLowStockProcessFilename = "FishSandwichStateProcessSuccessLowStock.json";
|
||||
|
||||
private void DumpProcessStateMetadataLocally(KernelProcessStateMetadata processStateInfo, string jsonFilename)
|
||||
{
|
||||
var sampleRelativePath = GetSampleStep03Filepath(jsonFilename);
|
||||
ProcessStateMetadataUtilities.DumpProcessStateMetadataLocally(processStateInfo, sampleRelativePath);
|
||||
}
|
||||
|
||||
private KernelProcessStateMetadata? LoadProcessStateMetadata(string jsonFilename)
|
||||
{
|
||||
var sampleRelativePath = GetSampleStep03Filepath(jsonFilename);
|
||||
return ProcessStateMetadataUtilities.LoadProcessStateMetadata(sampleRelativePath);
|
||||
}
|
||||
|
||||
private string GetSampleStep03Filepath(string jsonFilename)
|
||||
{
|
||||
return Path.Combine(this._step03RelativePath, jsonFilename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step03.Models;
|
||||
using Step03.Processes;
|
||||
|
||||
namespace Step03;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate creation of <see cref="KernelProcess"/> and
|
||||
/// eliciting different food related events.
|
||||
/// For visual reference of the processes used here check the diagram in: https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step03b_foodOrdering
|
||||
/// </summary>
|
||||
public class Step03b_FoodOrdering(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true)
|
||||
{
|
||||
// Target Open AI Services
|
||||
protected override bool ForceOpenAI => true;
|
||||
|
||||
[Fact]
|
||||
public async Task UseSingleOrderFriedFishAsync()
|
||||
{
|
||||
await UsePrepareFoodOrderProcessSingleItemAsync(FoodItem.FriedFish);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseSingleOrderPotatoFriesAsync()
|
||||
{
|
||||
await UsePrepareFoodOrderProcessSingleItemAsync(FoodItem.PotatoFries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseSingleOrderFishSandwichAsync()
|
||||
{
|
||||
await UsePrepareFoodOrderProcessSingleItemAsync(FoodItem.FishSandwich);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseSingleOrderFishAndChipsAsync()
|
||||
{
|
||||
await UsePrepareFoodOrderProcessSingleItemAsync(FoodItem.FishAndChips);
|
||||
}
|
||||
|
||||
protected async Task UsePrepareFoodOrderProcessSingleItemAsync(FoodItem foodItem)
|
||||
{
|
||||
Kernel kernel = CreateKernelWithChatCompletion();
|
||||
KernelProcess kernelProcess = SingleFoodItemProcess.CreateProcess().Build();
|
||||
|
||||
await using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent()
|
||||
{
|
||||
Id = SingleFoodItemProcess.ProcessEvents.SingleOrderReceived,
|
||||
Data = foodItem
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Process;
|
||||
|
||||
namespace Step03.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Step used in the Processes Samples:
|
||||
/// - Step_03_FoodPreparation.cs
|
||||
/// </summary>
|
||||
[KernelProcessStepMetadata("CutFoodStep.V1")]
|
||||
public class CutFoodStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string ChopFood = nameof(ChopFood);
|
||||
public const string SliceFood = nameof(SliceFood);
|
||||
}
|
||||
|
||||
public static class OutputEvents
|
||||
{
|
||||
public const string ChoppingReady = nameof(ChoppingReady);
|
||||
public const string SlicingReady = nameof(SlicingReady);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.ChopFood)]
|
||||
public async Task ChopFoodAsync(KernelProcessStepContext context, List<string> foodActions)
|
||||
{
|
||||
var foodToBeCut = foodActions.First();
|
||||
foodActions.Add(this.getActionString(foodToBeCut, "chopped"));
|
||||
Console.WriteLine($"CUTTING_STEP: Ingredient {foodToBeCut} has been chopped!");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.ChoppingReady, Data = foodActions });
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.SliceFood)]
|
||||
public async Task SliceFoodAsync(KernelProcessStepContext context, List<string> foodActions)
|
||||
{
|
||||
var foodToBeCut = foodActions.First();
|
||||
foodActions.Add(this.getActionString(foodToBeCut, "sliced"));
|
||||
Console.WriteLine($"CUTTING_STEP: Ingredient {foodToBeCut} has been sliced!");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.SlicingReady, Data = foodActions });
|
||||
}
|
||||
|
||||
private string getActionString(string food, string action)
|
||||
{
|
||||
return $"{food}_{action}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Process;
|
||||
|
||||
namespace Step03.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Step used in the Processes Samples:
|
||||
/// - Step_03_FoodPreparation.cs
|
||||
/// </summary>
|
||||
[KernelProcessStepMetadata("CutFoodStep.V2")]
|
||||
public class CutFoodWithSharpeningStep : KernelProcessStep<CutFoodWithSharpeningState>
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string ChopFood = nameof(ChopFood);
|
||||
public const string SliceFood = nameof(SliceFood);
|
||||
public const string SharpenKnife = nameof(SharpenKnife);
|
||||
}
|
||||
|
||||
public static class OutputEvents
|
||||
{
|
||||
public const string ChoppingReady = nameof(ChoppingReady);
|
||||
public const string SlicingReady = nameof(SlicingReady);
|
||||
public const string KnifeNeedsSharpening = nameof(KnifeNeedsSharpening);
|
||||
public const string KnifeSharpened = nameof(KnifeSharpened);
|
||||
}
|
||||
|
||||
internal CutFoodWithSharpeningState? _state;
|
||||
|
||||
public override ValueTask ActivateAsync(KernelProcessStepState<CutFoodWithSharpeningState> state)
|
||||
{
|
||||
_state = state.State;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.ChopFood)]
|
||||
public async Task ChopFoodAsync(KernelProcessStepContext context, List<string> foodActions)
|
||||
{
|
||||
var foodToBeCut = foodActions.First();
|
||||
if (this.KnifeNeedsSharpening())
|
||||
{
|
||||
Console.WriteLine($"CUTTING_STEP: Dull knife, cannot chop {foodToBeCut} - needs sharpening.");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.KnifeNeedsSharpening, Data = foodActions });
|
||||
return;
|
||||
}
|
||||
// Update knife sharpness
|
||||
this._state!.KnifeSharpness--;
|
||||
|
||||
// Chop food
|
||||
foodActions.Add(this.getActionString(foodToBeCut, "chopped"));
|
||||
Console.WriteLine($"CUTTING_STEP: Ingredient {foodToBeCut} has been chopped! - knife sharpness: {this._state.KnifeSharpness}");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.ChoppingReady, Data = foodActions });
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.SliceFood)]
|
||||
public async Task SliceFoodAsync(KernelProcessStepContext context, List<string> foodActions)
|
||||
{
|
||||
var foodToBeCut = foodActions.First();
|
||||
if (this.KnifeNeedsSharpening())
|
||||
{
|
||||
Console.WriteLine($"CUTTING_STEP: Dull knife, cannot slice {foodToBeCut} - needs sharpening.");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.KnifeNeedsSharpening, Data = foodActions });
|
||||
return;
|
||||
}
|
||||
// Update knife sharpness
|
||||
this._state!.KnifeSharpness--;
|
||||
|
||||
// Slice food
|
||||
foodActions.Add(this.getActionString(foodToBeCut, "sliced"));
|
||||
Console.WriteLine($"CUTTING_STEP: Ingredient {foodToBeCut} has been sliced! - knife sharpness: {this._state.KnifeSharpness}");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.SlicingReady, Data = foodActions });
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.SharpenKnife)]
|
||||
public async Task SharpenKnifeAsync(KernelProcessStepContext context, List<string> foodActions)
|
||||
{
|
||||
this._state!.KnifeSharpness += this._state._sharpeningBoost;
|
||||
Console.WriteLine($"KNIFE SHARPENED: Knife sharpness is now {this._state.KnifeSharpness}!");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.KnifeSharpened, Data = foodActions });
|
||||
}
|
||||
|
||||
private bool KnifeNeedsSharpening() => this._state?.KnifeSharpness == this._state?._needsSharpeningLimit;
|
||||
|
||||
private string getActionString(string food, string action)
|
||||
{
|
||||
return $"{food}_{action}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The state object for the <see cref="CutFoodWithSharpeningStep"/>.
|
||||
/// </summary>
|
||||
public sealed class CutFoodWithSharpeningState
|
||||
{
|
||||
public int KnifeSharpness { get; set; } = 5;
|
||||
|
||||
internal int _needsSharpeningLimit = 3;
|
||||
internal int _sharpeningBoost = 5;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace Step03.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Step used in the Processes Samples:
|
||||
/// - Step_03_FoodPreparation.cs
|
||||
/// </summary>
|
||||
public class ExternalStep(string externalEventName) : KernelProcessStep
|
||||
{
|
||||
private readonly string _externalEventName = externalEventName;
|
||||
|
||||
[KernelFunction]
|
||||
public async Task EmitExternalEventAsync(KernelProcessStepContext context, object data)
|
||||
{
|
||||
await context.EmitEventAsync(new() { Id = this._externalEventName, Data = data, Visibility = KernelProcessEventVisibility.Public });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Process;
|
||||
|
||||
namespace Step03.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Step used in the Processes Samples:
|
||||
/// - Step_03_FoodPreparation.cs
|
||||
/// </summary>
|
||||
[KernelProcessStepMetadata("FryFoodStep.V1")]
|
||||
public class FryFoodStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string FryFood = nameof(FryFood);
|
||||
}
|
||||
|
||||
public static class OutputEvents
|
||||
{
|
||||
public const string FoodRuined = nameof(FoodRuined);
|
||||
public const string FriedFoodReady = nameof(FriedFoodReady);
|
||||
}
|
||||
|
||||
private readonly Random _randomSeed = new();
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.FryFood)]
|
||||
public async Task FryFoodAsync(KernelProcessStepContext context, List<string> foodActions)
|
||||
{
|
||||
var foodToFry = foodActions.First();
|
||||
// This step may fail sometimes
|
||||
int fryerMalfunction = _randomSeed.Next(0, 10);
|
||||
|
||||
// foodToFry could potentially be used to set the frying temperature and cooking duration
|
||||
if (fryerMalfunction < 5)
|
||||
{
|
||||
// Oh no! Food got burnt :(
|
||||
foodActions.Add($"{foodToFry}_frying_failed");
|
||||
Console.WriteLine($"FRYING_STEP: Ingredient {foodToFry} got burnt while frying :(");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.FoodRuined, Data = foodActions });
|
||||
return;
|
||||
}
|
||||
|
||||
foodActions.Add($"{foodToFry}_frying_succeeded");
|
||||
Console.WriteLine($"FRYING_STEP: Ingredient {foodToFry} is ready!");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.FriedFoodReady, Data = foodActions, Visibility = KernelProcessEventVisibility.Public });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using Microsoft.SemanticKernel;
|
||||
using Step03.Models;
|
||||
namespace Step03.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Step used as base by many other cooking processes
|
||||
/// When used in other processes a new step is based on this one with custom GatherIngredientsAsync functionality
|
||||
/// </summary>
|
||||
public class GatherIngredientsStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string GatherIngredients = nameof(GatherIngredients);
|
||||
}
|
||||
|
||||
public static class OutputEvents
|
||||
{
|
||||
public const string IngredientsGathered = nameof(IngredientsGathered);
|
||||
}
|
||||
|
||||
private readonly FoodIngredients _ingredient;
|
||||
|
||||
public GatherIngredientsStep(FoodIngredients ingredient)
|
||||
{
|
||||
this._ingredient = ingredient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to be overridden by the user set custom ingredients to be gathered and events to be triggered
|
||||
/// </summary>
|
||||
/// <param name="context">The context for the current step and process. <see cref="KernelProcessStepContext"/></param>
|
||||
/// <param name="foodActions">list of actions taken to the food</param>
|
||||
/// <returns></returns>
|
||||
[KernelFunction(ProcessStepFunctions.GatherIngredients)]
|
||||
public virtual async Task GatherIngredientsAsync(KernelProcessStepContext context, List<string> foodActions)
|
||||
{
|
||||
var ingredient = this._ingredient.ToFriendlyString();
|
||||
var updatedFoodActions = new List<string>();
|
||||
updatedFoodActions.AddRange(foodActions);
|
||||
if (updatedFoodActions.Count == 0)
|
||||
{
|
||||
updatedFoodActions.Add(ingredient);
|
||||
}
|
||||
updatedFoodActions.Add($"{ingredient}_gathered");
|
||||
|
||||
Console.WriteLine($"GATHER_INGREDIENT: Gathered ingredient {ingredient}");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.IngredientsGathered, Data = updatedFoodActions });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stateful Step used as base by many other cooking processes
|
||||
/// When used in other processes a new step is based on this one with custom GatherIngredientsAsync functionality
|
||||
/// </summary>
|
||||
public class GatherIngredientsWithStockStep : KernelProcessStep<GatherIngredientsState>
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string GatherIngredients = nameof(GatherIngredients);
|
||||
}
|
||||
|
||||
public static class OutputEvents
|
||||
{
|
||||
public const string IngredientsGathered = nameof(IngredientsGathered);
|
||||
public const string IngredientsOutOfStock = nameof(IngredientsOutOfStock);
|
||||
}
|
||||
|
||||
private readonly FoodIngredients _ingredient;
|
||||
|
||||
public GatherIngredientsWithStockStep(FoodIngredients ingredient)
|
||||
{
|
||||
this._ingredient = ingredient;
|
||||
}
|
||||
|
||||
internal GatherIngredientsState? _state;
|
||||
|
||||
public override ValueTask ActivateAsync(KernelProcessStepState<GatherIngredientsState> state)
|
||||
{
|
||||
_state = state.State;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to be overridden by the user set custom ingredients to be gathered and events to be triggered
|
||||
/// </summary>
|
||||
/// <param name="context">The context for the current step and process. <see cref="KernelProcessStepContext"/></param>
|
||||
/// <param name="foodActions">list of actions taken to the food</param>
|
||||
/// <returns></returns>
|
||||
[KernelFunction(ProcessStepFunctions.GatherIngredients)]
|
||||
public virtual async Task GatherIngredientsAsync(KernelProcessStepContext context, List<string> foodActions)
|
||||
{
|
||||
var ingredient = this._ingredient.ToFriendlyString(); ;
|
||||
var updatedFoodActions = new List<string>();
|
||||
updatedFoodActions.AddRange(foodActions);
|
||||
|
||||
if (this._state!.IngredientsStock == 0)
|
||||
{
|
||||
Console.WriteLine($"GATHER_INGREDIENT: Could not gather {ingredient} - OUT OF STOCK!");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.IngredientsOutOfStock, Data = updatedFoodActions });
|
||||
return;
|
||||
}
|
||||
|
||||
if (updatedFoodActions.Count == 0)
|
||||
{
|
||||
updatedFoodActions.Add(ingredient);
|
||||
}
|
||||
updatedFoodActions.Add($"{ingredient}_gathered");
|
||||
|
||||
// Updating stock of ingredients
|
||||
this._state.IngredientsStock--;
|
||||
|
||||
Console.WriteLine($"GATHER_INGREDIENT: Gathered ingredient {ingredient} - remaining: {this._state.IngredientsStock}");
|
||||
await context.EmitEventAsync(new() { Id = OutputEvents.IngredientsGathered, Data = updatedFoodActions });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The state object for the <see cref="GatherIngredientsWithStockStep"/>.
|
||||
/// </summary>
|
||||
public sealed class GatherIngredientsState
|
||||
{
|
||||
public int IngredientsStock { get; set; } = 5;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
namespace Step04;
|
||||
|
||||
/// <summary>
|
||||
/// Processes events used in <see cref="Step04_AgentOrchestration"/> samples
|
||||
/// </summary>
|
||||
public static class AgentOrchestrationEvents
|
||||
{
|
||||
public static readonly string StartProcess = nameof(StartProcess);
|
||||
|
||||
public static readonly string AgentResponse = nameof(AgentResponse);
|
||||
public static readonly string AgentResponded = nameof(AgentResponded);
|
||||
public static readonly string AgentWorking = nameof(AgentWorking);
|
||||
public static readonly string GroupInput = nameof(GroupInput);
|
||||
public static readonly string GroupMessage = nameof(GroupMessage);
|
||||
public static readonly string GroupCompleted = nameof(GroupCompleted);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
|
||||
namespace Step04;
|
||||
|
||||
/// <summary>
|
||||
/// Provider based access to the chat history.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// While the in-memory implementation is trivial, this abstraction demonstrates how one might
|
||||
/// allow for the ability to access chat history from a remote store for a distributed service.
|
||||
/// <code>
|
||||
/// class CosmosDbChatHistoryProvider(CosmosClient client, string sessionId) : IChatHistoryProvider { }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
internal interface IChatHistoryProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to the chat history.
|
||||
/// </summary>
|
||||
Task<ChatHistory> GetHistoryAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Commits any updates to the chat history.
|
||||
/// </summary>
|
||||
Task CommitAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In memory based specialization of <see cref="IChatHistoryProvider"/>.
|
||||
/// </summary>
|
||||
internal sealed class ChatHistoryProvider(ChatHistory history) : IChatHistoryProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public Task<ChatHistory> GetHistoryAsync() => Task.FromResult(history);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task CommitAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Agents;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
|
||||
namespace Step04;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience extensions for agent based process patterns.
|
||||
/// </summary>
|
||||
internal static class KernelExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Return chat history from a singleton <see cref="IChatHistoryProvider"/>.
|
||||
/// </summary>
|
||||
public static IChatHistoryProvider GetHistory(this Kernel kernel) =>
|
||||
kernel.Services.GetRequiredService<IChatHistoryProvider>();
|
||||
|
||||
/// <summary>
|
||||
/// Access an agent as a keyed service.
|
||||
/// </summary>
|
||||
public static TAgent GetAgent<TAgent>(this Kernel kernel, string key) where TAgent : Agent =>
|
||||
kernel.Services.GetRequiredKeyedService<TAgent>(key);
|
||||
|
||||
/// <summary>
|
||||
/// Summarize chat history using reducer accessed as a keyed service.
|
||||
/// </summary>
|
||||
public static async Task<string> SummarizeHistoryAsync(this Kernel kernel, string key, IReadOnlyList<ChatMessageContent> history)
|
||||
{
|
||||
ChatHistorySummarizationReducer reducer = kernel.Services.GetRequiredKeyedService<ChatHistorySummarizationReducer>(key);
|
||||
IEnumerable<ChatMessageContent>? reducedResponse = await reducer.ReduceAsync(history);
|
||||
ChatMessageContent summary = reducedResponse?.First() ?? throw new InvalidDataException("No summary available");
|
||||
return summary.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace Step04.Plugins;
|
||||
|
||||
internal sealed record CalendarEvent(
|
||||
string Title,
|
||||
string Start,
|
||||
string? End,
|
||||
string? Description = null)
|
||||
{
|
||||
[JsonIgnore]
|
||||
public DateTime StartDate { get; } = DateTime.Parse(Start);
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime? EndDate { get; } = End != null ? DateTime.Parse(End) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock plug-in to provide calendar information for the current and following month.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Calendar information is simplified in the sense that any event covers the entire
|
||||
/// day. Also, no special treatment for weekend.
|
||||
/// </remarks>
|
||||
internal sealed class CalendarPlugin
|
||||
{
|
||||
private static readonly DateTime s_now = DateTime.Now;
|
||||
private static readonly DateTime s_nextMonth = new(s_now.Year, DateTime.Now.AddMonths(1).Month, 1);
|
||||
|
||||
private readonly List<CalendarEvent> _events = [];
|
||||
|
||||
public CalendarPlugin()
|
||||
{
|
||||
CalendarGenerator generator = new();
|
||||
this._events =
|
||||
[
|
||||
.. generator.GenerateEvents(s_now.Month, s_now.Year),
|
||||
.. generator.GenerateEvents(s_nextMonth.Month, s_nextMonth.Year),
|
||||
];
|
||||
}
|
||||
|
||||
// Exposed for validation / no impact to plugin functionality
|
||||
public IReadOnlyList<CalendarEvent> Events => this._events;
|
||||
|
||||
[KernelFunction]
|
||||
public string GetCurrentDate() => DateTime.Now.Date.ToString("dd-MMM-yyyy");
|
||||
|
||||
[KernelFunction]
|
||||
[Description("Get the scheduled events that begin within the specified date range.")]
|
||||
public IReadOnlyList<CalendarEvent> GetEvents(
|
||||
[Description("The first date in the range")]
|
||||
string start,
|
||||
[Description("The final date in the range")]
|
||||
string end)
|
||||
{
|
||||
DateTime startDate = DateTime.Parse(start, CultureInfo.CurrentCulture);
|
||||
DateTime endDate = DateTime.Parse(end, CultureInfo.CurrentCulture);
|
||||
|
||||
return this._events.Where(e => e.StartDate.Date >= startDate.Date && e.StartDate.Date < endDate.Date.AddDays(1)).ToArray();
|
||||
}
|
||||
|
||||
[KernelFunction]
|
||||
[Description("Create a new scheduled event.")]
|
||||
public void NewEvent(string title, string startDate, string? endDate = null, string? description = null)
|
||||
{
|
||||
_events.Add(new CalendarEvent(title, startDate, endDate, description));
|
||||
}
|
||||
|
||||
private sealed class CalendarGenerator
|
||||
{
|
||||
public int MaximumMultiDayEventCount => this._multiDayEvents.Count;
|
||||
|
||||
public int MinimumEventGapInDays => 3; // Personal calendar less dense
|
||||
|
||||
public IEnumerable<CalendarEvent> GenerateEvents(int month, int year)
|
||||
{
|
||||
int targetDayOfMonth = 1;
|
||||
do
|
||||
{
|
||||
bool isMultiDay = Random.Shared.Next(5) == 0 && this.MaximumMultiDayEventCount > 0;
|
||||
int daySpan = Random.Shared.Next(2, 8);
|
||||
int gapDays = Random.Shared.Next(this.MinimumEventGapInDays, 5);
|
||||
|
||||
(string title, string description) = this.Pick(!isMultiDay);
|
||||
|
||||
yield return new CalendarEvent(
|
||||
title,
|
||||
FormatDate(targetDayOfMonth),
|
||||
isMultiDay ? FormatDate(targetDayOfMonth, daySpan) : null,
|
||||
description);
|
||||
|
||||
targetDayOfMonth += gapDays + 1 + (isMultiDay ? daySpan : 0);
|
||||
}
|
||||
while (targetDayOfMonth <= DateTime.DaysInMonth(year, month));
|
||||
|
||||
string FormatDate(int day, int span = 0)
|
||||
{
|
||||
DateOnly date = new(year, month, day);
|
||||
date = date.AddDays(span);
|
||||
return date.ToString("dd-MMM-yyyy");
|
||||
}
|
||||
}
|
||||
|
||||
private (string title, string description) Pick(bool isSingleDay)
|
||||
{
|
||||
return Pick(isSingleDay ? _singleDayEvents : _multiDayEvents);
|
||||
}
|
||||
|
||||
private static (string title, string description) Pick(List<(string title, string description)> eventList)
|
||||
{
|
||||
int index = Random.Shared.Next(eventList.Count);
|
||||
try
|
||||
{
|
||||
return eventList[index];
|
||||
}
|
||||
finally
|
||||
{
|
||||
eventList.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly List<(string title, string description)> _singleDayEvents =
|
||||
[
|
||||
("Doctor's Appointment", "Annual physical check-up."),
|
||||
("Grocery Shopping", "Weekly stock-up on essentials."),
|
||||
("Yoga Class", "1-hour morning yoga session."),
|
||||
("Car Maintenance", "Oil change and tire rotation."),
|
||||
("Dinner with Friends", "Casual dinner at local restaurant."),
|
||||
("Team Meeting", "Project update and discussion with the team."),
|
||||
("Haircut Appointment", "Haircut and style at the salon."),
|
||||
("Parent-Teacher Conference", "Discuss child's progress in school."),
|
||||
("Dentist Appointment", "Teeth cleaning and routine check-up."),
|
||||
("Workout Session", "Strength training at the gym."),
|
||||
("Birthday Party", "Attending a friend's birthday celebration."),
|
||||
("Movie Night", "Watch new release at the theater."),
|
||||
("Volunteer Work", "Community cleanup event participation."),
|
||||
("Job Interview", "Interview for potential new role."),
|
||||
("Library Visit", "Return books and browse for new reads.")
|
||||
];
|
||||
|
||||
public readonly List<(string title, string description)> _multiDayEvents =
|
||||
[
|
||||
("Vacation", "Relaxing trip with family."),
|
||||
("Home Renovation Project", "Kitchen remodeling."),
|
||||
("Annual Family Reunion", "Traveling to grandparents."),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using System.ComponentModel;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace Step04.Plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Mock plug-in to provide location information.
|
||||
/// </summary>
|
||||
internal sealed class LocationPlugin
|
||||
{
|
||||
[KernelFunction]
|
||||
[Description("Provide the user's current location by city, region, and country.")]
|
||||
public string GetCurrentLocation() => "Bellevue, WA, USA";
|
||||
|
||||
[KernelFunction]
|
||||
[Description("Provide the user's home location by city, region, and country.")]
|
||||
public string GetHomeLocation() => "Seattle, WA, USA";
|
||||
|
||||
[KernelFunction]
|
||||
[Description("Provide the user's work office location by city, region, and country.")]
|
||||
public string GetOfficeLocation() => "Redmond, WA, USA";
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using System.ComponentModel;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace Step04.Plugins;
|
||||
|
||||
internal sealed record WeatherForecast(
|
||||
string Date,
|
||||
string Location,
|
||||
string HighTemperature,
|
||||
string LowTemperature,
|
||||
string Precipition);
|
||||
|
||||
/// <summary>
|
||||
/// Mock plug-in to provide weather information.
|
||||
/// </summary>
|
||||
internal sealed class WeatherPlugin
|
||||
{
|
||||
private readonly Dictionary<string, WeatherForecast> _forecasts = [];
|
||||
|
||||
[KernelFunction]
|
||||
public string GetCurrentDate() => DateTime.Now.Date.ToString("dd-MMM-yyyy");
|
||||
|
||||
[KernelFunction]
|
||||
[Description("Provide the weather forecast for the given date and location. Dates farther than 15 days out will use historical data.")]
|
||||
public WeatherForecast GetForecast(
|
||||
string date,
|
||||
string location)
|
||||
{
|
||||
string key = $"{date}-{location}";
|
||||
|
||||
if (!this._forecasts.TryGetValue(key, out WeatherForecast? forecast))
|
||||
{
|
||||
forecast = GenerateForecast(date, location);
|
||||
this._forecasts[key] = forecast;
|
||||
}
|
||||
|
||||
return forecast;
|
||||
}
|
||||
|
||||
private static WeatherForecast GenerateForecast(string date, string location)
|
||||
{
|
||||
int highTemp = Random.Shared.Next(49, 96);
|
||||
int lowTemp = highTemp - Random.Shared.Next(12, 20);
|
||||
int precip = Random.Shared.Next(0, 80);
|
||||
|
||||
return
|
||||
new WeatherForecast(
|
||||
date,
|
||||
location,
|
||||
$"{highTemp} F",
|
||||
$"{lowTemp} F",
|
||||
$"{precip} %");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace Step04;
|
||||
|
||||
internal static class JsonSchemaGenerator
|
||||
{
|
||||
private static readonly AIJsonSchemaCreateOptions s_config = new()
|
||||
{
|
||||
TransformOptions = new()
|
||||
{
|
||||
DisallowAdditionalProperties = true,
|
||||
RequireAllProperties = true,
|
||||
MoveDefaultKeywordToDescription = true,
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for generating a JSON schema as string from a .NET type.
|
||||
/// </summary>
|
||||
public static string FromType<TSchemaType>()
|
||||
{
|
||||
return KernelJsonSchemaBuilder.Build(typeof(TSchemaType), "Intent Result", s_config).AsJson();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ClientModel;
|
||||
using Azure.Identity;
|
||||
using Events;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Agents;
|
||||
using Microsoft.SemanticKernel.Agents.Chat;
|
||||
using Microsoft.SemanticKernel.Agents.OpenAI;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
using OpenAI;
|
||||
using SharedSteps;
|
||||
using Step04.Plugins;
|
||||
using Step04.Steps;
|
||||
|
||||
namespace Step04;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate creation of a <see cref="KernelProcess"/> that orchestrates an <see cref="Agent"/> conversation.
|
||||
/// For visual reference of the process check <see href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithProcesses/README.md#step04_agentorchestration" >diagram</see>.
|
||||
/// </summary>
|
||||
public class Step04_AgentOrchestration : BaseTest
|
||||
{
|
||||
public Step04_AgentOrchestration(ITestOutputHelper output) : base(output, redirectSystemConsoleOutput: true)
|
||||
{
|
||||
this.Client =
|
||||
this.UseOpenAIConfig ?
|
||||
OpenAIAssistantAgent.CreateOpenAIClient(new ApiKeyCredential(this.ApiKey ?? throw new ConfigurationNotFoundException("OpenAI:ApiKey"))) :
|
||||
!string.IsNullOrWhiteSpace(this.ApiKey) ?
|
||||
OpenAIAssistantAgent.CreateAzureOpenAIClient(new ApiKeyCredential(this.ApiKey), new Uri(this.Endpoint!)) :
|
||||
OpenAIAssistantAgent.CreateAzureOpenAIClient(new AzureCliCredential(), new Uri(this.Endpoint!));
|
||||
}
|
||||
|
||||
protected OpenAIClient Client { get; init; }
|
||||
|
||||
// Target Open AI Services
|
||||
protected override bool ForceOpenAI => true;
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates a single agent gathering user input and then delegating to a group of agents.
|
||||
/// The group of agents provide a response back to the single agent who continues to
|
||||
/// interact with the user.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DelegatedGroupChatAsync()
|
||||
{
|
||||
// Define process
|
||||
KernelProcess process = SetupAgentProcess<BasicAgentChatUserInput>(nameof(DelegatedGroupChatAsync));
|
||||
|
||||
// Execute process
|
||||
await RunProcessAsync(process);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SingleTeacherStudentAgentCallAsync()
|
||||
{
|
||||
// Define process
|
||||
KernelProcess process = SetupSingleAgentProcess<BasicTeacherAgentInput>(nameof(SingleTeacherStudentAgentCallAsync));
|
||||
|
||||
// Setup kernel with OpenAI Client
|
||||
Kernel kernel = SetupKernel();
|
||||
|
||||
// Execute process
|
||||
await using LocalKernelProcessContext localProcess =
|
||||
await process.StartAsync(
|
||||
kernel,
|
||||
new KernelProcessEvent()
|
||||
{
|
||||
Id = AgentOrchestrationEvents.StartProcess
|
||||
});
|
||||
|
||||
// Cleaning up created agents
|
||||
var processState = await localProcess.GetStateAsync();
|
||||
var agentState = (KernelProcessStepState<KernelProcessAgentExecutorState>)processState.Steps.Where(step => step.State.Id == "Student").FirstOrDefault()!.State;
|
||||
var agentId = agentState?.State?.AgentId;
|
||||
if (agentId != null)
|
||||
{
|
||||
await this.Client.GetAssistantClient().DeleteAssistantAsync(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BasicAgentChatUserInput : ScriptedUserInputStep
|
||||
{
|
||||
public BasicAgentChatUserInput()
|
||||
{
|
||||
this.SuppressOutput = true;
|
||||
}
|
||||
|
||||
public override void PopulateUserInputs(UserInputState state)
|
||||
{
|
||||
state.UserInputs.Add("Hi");
|
||||
state.UserInputs.Add("List the upcoming events on my calendar for the next week");
|
||||
state.UserInputs.Add("Correct");
|
||||
state.UserInputs.Add("When is an open time to go camping near home for 4 days after the end of this week?");
|
||||
state.UserInputs.Add("Yes, and I'd prefer nice weather.");
|
||||
state.UserInputs.Add("Sounds good, add the soonest option without conflicts to my calendar");
|
||||
state.UserInputs.Add("Correct");
|
||||
state.UserInputs.Add("That's all, thank you");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BasicTeacherAgentInput : ScriptedUserInputStep
|
||||
{
|
||||
public BasicTeacherAgentInput()
|
||||
{
|
||||
this.SuppressOutput = true;
|
||||
}
|
||||
|
||||
public override void PopulateUserInputs(UserInputState state)
|
||||
{
|
||||
state.UserInputs.Add("What is 2+2");
|
||||
state.UserInputs.Add("What is 2+2");
|
||||
state.UserInputs.Add("What is the name of a shape with 3 consecutive sides");
|
||||
state.UserInputs.Add("What is the internal angle of a square");
|
||||
state.UserInputs.Add("What is a parallellogram");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunProcessAsync(KernelProcess process)
|
||||
{
|
||||
// Initialize services
|
||||
ChatHistory history = [];
|
||||
Kernel kernel = SetupKernel(history);
|
||||
|
||||
// Execute process
|
||||
await using LocalKernelProcessContext localProcess =
|
||||
await process.StartAsync(
|
||||
kernel,
|
||||
new KernelProcessEvent()
|
||||
{
|
||||
Id = AgentOrchestrationEvents.StartProcess
|
||||
});
|
||||
|
||||
// Demonstrate history is maintained independent of process state
|
||||
this.WriteHorizontalRule();
|
||||
foreach (ChatMessageContent message in history)
|
||||
{
|
||||
RenderMessageStep.Render(message);
|
||||
}
|
||||
}
|
||||
|
||||
private KernelProcess SetupSingleAgentProcess<TUserInputStep>(string processName) where TUserInputStep : ScriptedUserInputStep
|
||||
{
|
||||
ProcessBuilder process = new(processName);
|
||||
|
||||
var userInputStep = process.AddStepFromType<TUserInputStep>();
|
||||
var renderMessageStep = process.AddStepFromType<RenderMessageStep>();
|
||||
var agentStep = process.AddStepFromAgent(new()
|
||||
{
|
||||
Name = "Student",
|
||||
// On purpose not assigning AgentId, if not provided a new agent is created
|
||||
Description = "Solves problem given",
|
||||
Instructions = "Solve the problem given, if the question is repeated mention something like I already answered but here is the answer with a bit of humor",
|
||||
Model = new()
|
||||
{
|
||||
Id = "gpt-4o",
|
||||
},
|
||||
Type = OpenAIAssistantAgentFactory.OpenAIAssistantAgentType,
|
||||
});
|
||||
|
||||
// Entry point
|
||||
process.OnInputEvent(AgentOrchestrationEvents.StartProcess)
|
||||
.SendEventTo(new(userInputStep));
|
||||
|
||||
// Pass user input to primary agent
|
||||
userInputStep
|
||||
.OnEvent(CommonEvents.UserInputReceived)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(agentStep, parameterName: "message"))
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderUserText));
|
||||
|
||||
agentStep
|
||||
.OnFunctionResult()
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep))
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderMessage));
|
||||
|
||||
agentStep
|
||||
.OnFunctionError()
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderError, "error"))
|
||||
.StopProcess();
|
||||
|
||||
return process.Build();
|
||||
}
|
||||
|
||||
private KernelProcess SetupAgentProcess<TUserInputStep>(string processName) where TUserInputStep : ScriptedUserInputStep
|
||||
{
|
||||
ProcessBuilder process = new(processName);
|
||||
|
||||
var userInputStep = process.AddStepFromType<TUserInputStep>();
|
||||
var renderMessageStep = process.AddStepFromType<RenderMessageStep>();
|
||||
var managerAgentStep = process.AddStepFromType<ManagerAgentStep>();
|
||||
var agentGroupStep = process.AddStepFromType<AgentGroupChatStep>();
|
||||
|
||||
AttachErrorStep(
|
||||
userInputStep,
|
||||
ScriptedUserInputStep.ProcessStepFunctions.GetUserInput);
|
||||
|
||||
AttachErrorStep(
|
||||
managerAgentStep,
|
||||
ManagerAgentStep.ProcessStepFunctions.InvokeAgent,
|
||||
ManagerAgentStep.ProcessStepFunctions.InvokeGroup,
|
||||
ManagerAgentStep.ProcessStepFunctions.ReceiveResponse);
|
||||
|
||||
AttachErrorStep(
|
||||
agentGroupStep,
|
||||
AgentGroupChatStep.ProcessStepFunctions.InvokeAgentGroup);
|
||||
|
||||
// Entry point
|
||||
process.OnInputEvent(AgentOrchestrationEvents.StartProcess)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep));
|
||||
|
||||
// Pass user input to primary agent
|
||||
userInputStep
|
||||
.OnEvent(CommonEvents.UserInputReceived)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.ProcessStepFunctions.InvokeAgent))
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderUserText, parameterName: "message"));
|
||||
|
||||
// Process completed
|
||||
userInputStep
|
||||
.OnEvent(CommonEvents.UserInputComplete)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderDone))
|
||||
.StopProcess();
|
||||
|
||||
// Render response from primary agent
|
||||
managerAgentStep
|
||||
.OnEvent(AgentOrchestrationEvents.AgentResponse)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderMessage, parameterName: "message"));
|
||||
|
||||
// Request is complete
|
||||
managerAgentStep
|
||||
.OnEvent(CommonEvents.UserInputComplete)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderDone))
|
||||
.StopProcess();
|
||||
|
||||
// Request more user input
|
||||
managerAgentStep
|
||||
.OnEvent(AgentOrchestrationEvents.AgentResponded)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(userInputStep));
|
||||
|
||||
// Delegate to inner agents
|
||||
managerAgentStep
|
||||
.OnEvent(AgentOrchestrationEvents.AgentWorking)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.ProcessStepFunctions.InvokeGroup));
|
||||
|
||||
// Provide input to inner agents
|
||||
managerAgentStep
|
||||
.OnEvent(AgentOrchestrationEvents.GroupInput)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(agentGroupStep, parameterName: "input"));
|
||||
|
||||
// Render response from inner chat (for visibility)
|
||||
agentGroupStep
|
||||
.OnEvent(AgentOrchestrationEvents.GroupMessage)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderInnerMessage, parameterName: "message"));
|
||||
|
||||
// Provide inner response to primary agent
|
||||
agentGroupStep
|
||||
.OnEvent(AgentOrchestrationEvents.GroupCompleted)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(managerAgentStep, ManagerAgentStep.ProcessStepFunctions.ReceiveResponse, parameterName: "response"));
|
||||
|
||||
KernelProcess kernelProcess = process.Build();
|
||||
|
||||
return kernelProcess;
|
||||
|
||||
void AttachErrorStep(ProcessStepBuilder step, params string[] functionNames)
|
||||
{
|
||||
foreach (string functionName in functionNames)
|
||||
{
|
||||
step
|
||||
.OnFunctionError(functionName)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(renderMessageStep, RenderMessageStep.ProcessStepFunctions.RenderError, "error"))
|
||||
.StopProcess();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Kernel SetupKernel()
|
||||
{
|
||||
IKernelBuilder builder = Kernel.CreateBuilder();
|
||||
// Add Chat Completion to Kernel
|
||||
this.AddChatCompletionToKernel(builder);
|
||||
builder.Services.AddSingleton<OpenAIClient>(this.Client);
|
||||
|
||||
// NOTE: Uncomment to see process logging
|
||||
//builder.Services.AddSingleton<ILoggerFactory>(this.LoggerFactory);
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private Kernel SetupKernel(ChatHistory history)
|
||||
{
|
||||
IKernelBuilder builder = Kernel.CreateBuilder();
|
||||
|
||||
// Add Chat Completion to Kernel
|
||||
this.AddChatCompletionToKernel(builder);
|
||||
|
||||
// Inject agents into service collection
|
||||
SetupAgents(builder, builder.Build());
|
||||
// Inject history provider into service collection
|
||||
builder.Services.AddSingleton<IChatHistoryProvider>(new ChatHistoryProvider(history));
|
||||
|
||||
// NOTE: Uncomment to see process logging
|
||||
//builder.Services.AddSingleton<ILoggerFactory>(this.LoggerFactory);
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private const string ManagerInstructions =
|
||||
"""
|
||||
Capture information provided by the user for their scheduling request.
|
||||
Request confirmation without suggesting additional details.
|
||||
Once confirmed inform them you're working on the request.
|
||||
Never provide a direct answer to the user's request.
|
||||
""";
|
||||
|
||||
private const string CalendarInstructions =
|
||||
"""
|
||||
Evaluate the scheduled calendar events in response to the current direction.
|
||||
In the absence of specific dates, prioritize the earliest opportunity but do not restrict evaluation the current date.
|
||||
Never consider or propose scheduling that conflicts with existing events.
|
||||
""";
|
||||
|
||||
private const string WeatherInstructions =
|
||||
"""
|
||||
Provide weather information in response to the current direction.
|
||||
""";
|
||||
|
||||
private const string ManagerSummaryInstructions =
|
||||
"""
|
||||
Summarize the most recent user request in first person command form.
|
||||
""";
|
||||
|
||||
private const string SuggestionSummaryInstructions =
|
||||
"""
|
||||
Address the user directly with a summary of the response.
|
||||
""";
|
||||
|
||||
private static void SetupAgents(IKernelBuilder builder, Kernel kernel)
|
||||
{
|
||||
// Create and inject primary agent into service collection
|
||||
ChatCompletionAgent managerAgent = CreateAgent("Manager", ManagerInstructions, kernel.Clone());
|
||||
builder.Services.AddKeyedSingleton(ManagerAgentStep.AgentServiceKey, managerAgent);
|
||||
|
||||
// Create and inject group chat into service collection
|
||||
SetupGroupChat(builder, kernel);
|
||||
|
||||
// Create and inject reducers into service collection
|
||||
builder.Services.AddKeyedSingleton(ManagerAgentStep.ReducerServiceKey, SetupReducer(kernel, ManagerSummaryInstructions));
|
||||
builder.Services.AddKeyedSingleton(AgentGroupChatStep.ReducerServiceKey, SetupReducer(kernel, SuggestionSummaryInstructions));
|
||||
}
|
||||
|
||||
private static ChatHistorySummarizationReducer SetupReducer(Kernel kernel, string instructions) =>
|
||||
new(kernel.GetRequiredService<IChatCompletionService>(), 1)
|
||||
{
|
||||
SummarizationInstructions = instructions
|
||||
};
|
||||
|
||||
private static void SetupGroupChat(IKernelBuilder builder, Kernel kernel)
|
||||
{
|
||||
const string CalendarAgentName = "CalendarAgent";
|
||||
ChatCompletionAgent calendarAgent = CreateAgent(CalendarAgentName, CalendarInstructions, kernel.Clone());
|
||||
calendarAgent.Kernel.Plugins.AddFromType<CalendarPlugin>();
|
||||
|
||||
const string WeatherAgentName = "WeatherAgent";
|
||||
ChatCompletionAgent weatherAgent = CreateAgent(WeatherAgentName, WeatherInstructions, kernel.Clone());
|
||||
weatherAgent.Kernel.Plugins.AddFromType<WeatherPlugin>();
|
||||
weatherAgent.Kernel.Plugins.AddFromType<LocationPlugin>();
|
||||
|
||||
KernelFunction selectionFunction =
|
||||
AgentGroupChat.CreatePromptFunctionForStrategy(
|
||||
$$$"""
|
||||
Determine which participant takes the next turn in a conversation based on the the most recent participant.
|
||||
State only the name of the participant to take the next turn.
|
||||
No participant should take more than one turn in a row.
|
||||
|
||||
Choose only from these participants:
|
||||
- {{{CalendarAgentName}}}
|
||||
- {{{WeatherAgentName}}}
|
||||
|
||||
Always follow these rules when selecting the next participant:
|
||||
- After user input, it is {{{CalendarAgentName}}}'s turn.
|
||||
- After {{{CalendarAgentName}}}, it is {{{WeatherAgentName}}}'s turn.
|
||||
- After {{{WeatherAgentName}}}, it is {{{CalendarAgentName}}}'s turn.
|
||||
|
||||
History:
|
||||
{{$history}}
|
||||
""",
|
||||
safeParameterNames: "history");
|
||||
|
||||
KernelFunction terminationFunction =
|
||||
AgentGroupChat.CreatePromptFunctionForStrategy(
|
||||
$$$"""
|
||||
Evaluate if the a user's most recent calendar request has received a final response.
|
||||
If weather conditions are requested, {{{WeatherAgentName}}} is required to provide input.
|
||||
|
||||
If all of these conditions are met, respond with a single word: yes
|
||||
|
||||
History:
|
||||
{{$history}}
|
||||
""",
|
||||
safeParameterNames: "history");
|
||||
|
||||
AgentGroupChat chat =
|
||||
new(calendarAgent, weatherAgent)
|
||||
{
|
||||
// NOTE: Replace logger when using outside of sample.
|
||||
// Use `this.LoggerFactory` to observe logging output as part of sample.
|
||||
LoggerFactory = NullLoggerFactory.Instance,
|
||||
ExecutionSettings = new()
|
||||
{
|
||||
SelectionStrategy =
|
||||
new KernelFunctionSelectionStrategy(selectionFunction, kernel)
|
||||
{
|
||||
HistoryVariableName = "history",
|
||||
HistoryReducer = new ChatHistoryTruncationReducer(1),
|
||||
ResultParser = (result) => result.GetValue<string>() ?? calendarAgent.Name!,
|
||||
},
|
||||
TerminationStrategy =
|
||||
new KernelFunctionTerminationStrategy(terminationFunction, kernel)
|
||||
{
|
||||
HistoryVariableName = "history",
|
||||
MaximumIterations = 12,
|
||||
//HistoryReducer = new ChatHistoryTruncationReducer(2),
|
||||
ResultParser = (result) => result.GetValue<string>()?.Contains("yes", StringComparison.OrdinalIgnoreCase) ?? false,
|
||||
}
|
||||
}
|
||||
};
|
||||
builder.Services.AddSingleton(chat);
|
||||
}
|
||||
|
||||
private static ChatCompletionAgent CreateAgent(string name, string instructions, Kernel kernel) =>
|
||||
new()
|
||||
{
|
||||
Name = name,
|
||||
Instructions = instructions,
|
||||
Kernel = kernel.Clone(),
|
||||
Arguments =
|
||||
new KernelArguments(
|
||||
new OpenAIPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
|
||||
Temperature = 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Agents;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
|
||||
namespace Step04.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// This steps defines actions for the group chat in which to agents collaborate in
|
||||
/// response to input from the primary agent.
|
||||
/// </summary>
|
||||
public class AgentGroupChatStep : KernelProcessStep
|
||||
{
|
||||
public const string ChatServiceKey = $"{nameof(AgentGroupChatStep)}:{nameof(ChatServiceKey)}";
|
||||
public const string ReducerServiceKey = $"{nameof(AgentGroupChatStep)}:{nameof(ReducerServiceKey)}";
|
||||
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string InvokeAgentGroup = nameof(InvokeAgentGroup);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.InvokeAgentGroup)]
|
||||
public async Task InvokeAgentGroupAsync(KernelProcessStepContext context, Kernel kernel, string input)
|
||||
{
|
||||
AgentGroupChat chat = kernel.GetRequiredService<AgentGroupChat>();
|
||||
|
||||
// Reset chat state from previous invocation
|
||||
//await chat.ResetAsync();
|
||||
chat.IsComplete = false;
|
||||
|
||||
ChatMessageContent message = new(AuthorRole.User, input);
|
||||
chat.AddChatMessage(message);
|
||||
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.GroupMessage, Data = message });
|
||||
|
||||
await foreach (ChatMessageContent response in chat.InvokeAsync())
|
||||
{
|
||||
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.GroupMessage, Data = response });
|
||||
}
|
||||
|
||||
ChatMessageContent[] history = await chat.GetChatMessagesAsync().Reverse().ToArrayAsync();
|
||||
|
||||
// Summarize the group chat as a response to the primary agent
|
||||
string summary = await kernel.SummarizeHistoryAsync(ReducerServiceKey, history);
|
||||
|
||||
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.GroupCompleted, Data = summary });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Agents;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace Step04.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// This steps defines actions for the primary agent. This agent is responsible forinteracting with
|
||||
/// the user as well as as delegating to a group of agents.
|
||||
/// </summary>
|
||||
public class ManagerAgentStep : KernelProcessStep
|
||||
{
|
||||
public const string AgentServiceKey = $"{nameof(ManagerAgentStep)}:{nameof(AgentServiceKey)}";
|
||||
public const string ReducerServiceKey = $"{nameof(ManagerAgentStep)}:{nameof(ReducerServiceKey)}";
|
||||
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string InvokeAgent = nameof(InvokeAgent);
|
||||
public const string InvokeGroup = nameof(InvokeGroup);
|
||||
public const string ReceiveResponse = nameof(ReceiveResponse);
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.InvokeAgent)]
|
||||
public async Task InvokeAgentAsync(KernelProcessStepContext context, Kernel kernel, string userInput, ILogger logger)
|
||||
{
|
||||
// Get the chat history
|
||||
IChatHistoryProvider historyProvider = kernel.GetHistory();
|
||||
ChatHistory history = await historyProvider.GetHistoryAsync();
|
||||
ChatHistoryAgentThread agentThread = new(history);
|
||||
|
||||
// Obtain the agent response
|
||||
ChatCompletionAgent agent = kernel.GetAgent<ChatCompletionAgent>(AgentServiceKey);
|
||||
await foreach (ChatMessageContent message in agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, userInput), agentThread))
|
||||
{
|
||||
// Both the input message and response message will automatically be added to the thread, which will update the internal chat history.
|
||||
|
||||
// Emit event for each agent response
|
||||
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.AgentResponse, Data = message });
|
||||
}
|
||||
|
||||
// Commit any changes to the chat history
|
||||
await historyProvider.CommitAsync();
|
||||
|
||||
// Evaluate current intent
|
||||
IntentResult intent = await IsRequestingUserInputAsync(kernel, history, logger);
|
||||
|
||||
string intentEventId =
|
||||
intent.IsRequestingUserInput ?
|
||||
AgentOrchestrationEvents.AgentResponded :
|
||||
intent.IsWorking ?
|
||||
AgentOrchestrationEvents.AgentWorking :
|
||||
CommonEvents.UserInputComplete;
|
||||
|
||||
await context.EmitEventAsync(new() { Id = intentEventId });
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.InvokeGroup)]
|
||||
public async Task InvokeGroupAsync(KernelProcessStepContext context, Kernel kernel)
|
||||
{
|
||||
// Get the chat history
|
||||
IChatHistoryProvider historyProvider = kernel.GetHistory();
|
||||
ChatHistory history = await historyProvider.GetHistoryAsync();
|
||||
|
||||
// Summarize the conversation with the user to use as input to the agent group
|
||||
string summary = await kernel.SummarizeHistoryAsync(ReducerServiceKey, history);
|
||||
|
||||
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.GroupInput, Data = summary });
|
||||
}
|
||||
|
||||
[KernelFunction(ProcessStepFunctions.ReceiveResponse)]
|
||||
public async Task ReceiveResponseAsync(KernelProcessStepContext context, Kernel kernel, string response)
|
||||
{
|
||||
// Get the chat history
|
||||
IChatHistoryProvider historyProvider = kernel.GetHistory();
|
||||
ChatHistory history = await historyProvider.GetHistoryAsync();
|
||||
|
||||
// Proxy the inner response
|
||||
ChatCompletionAgent agent = kernel.GetAgent<ChatCompletionAgent>(AgentServiceKey);
|
||||
ChatMessageContent message = new(AuthorRole.Assistant, response) { AuthorName = agent.Name };
|
||||
history.Add(message);
|
||||
|
||||
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.AgentResponse, Data = message });
|
||||
|
||||
await context.EmitEventAsync(new() { Id = AgentOrchestrationEvents.AgentResponded });
|
||||
}
|
||||
|
||||
private static async Task<IntentResult> IsRequestingUserInputAsync(Kernel kernel, ChatHistory history, ILogger logger)
|
||||
{
|
||||
ChatHistory localHistory =
|
||||
[
|
||||
new ChatMessageContent(AuthorRole.System, "Analyze the conversation and determine if user input is being solicited."),
|
||||
.. history.TakeLast(1)
|
||||
];
|
||||
|
||||
IChatCompletionService service = kernel.GetRequiredService<IChatCompletionService>();
|
||||
|
||||
ChatMessageContent response = await service.GetChatMessageContentAsync(localHistory, new OpenAIPromptExecutionSettings { ResponseFormat = typeof(IntentResult) });
|
||||
IntentResult intent = JsonSerializer.Deserialize<IntentResult>(response.ToString())!;
|
||||
|
||||
logger.LogTrace("{StepName} Response Intent - {IsRequestingUserInput}: {Rationale}", nameof(ManagerAgentStep), intent.IsRequestingUserInput, intent.Rationale);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
[DisplayName("IntentResult")]
|
||||
[Description("this is the result description")]
|
||||
public sealed record IntentResult(
|
||||
[property:Description("True if user input is requested or solicited. Addressing the user with no specific request is False. Asking a question to the user is True.")]
|
||||
bool IsRequestingUserInput,
|
||||
[property:Description("True if the user request is being worked on.")]
|
||||
bool IsWorking,
|
||||
[property:Description("Rationale for the value assigned to IsRequestingUserInput")]
|
||||
string Rationale);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
|
||||
namespace Step04.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Displays output to the user. While in this case it is just writing to the console,
|
||||
/// in a real-world scenario this would be a more sophisticated rendering system. Isolating this
|
||||
/// rendering logic from the internal logic of other process steps simplifies responsibility contract
|
||||
/// and simplifies testing and state management.
|
||||
/// </summary>
|
||||
public class RenderMessageStep : KernelProcessStep
|
||||
{
|
||||
public static class ProcessStepFunctions
|
||||
{
|
||||
public const string RenderDone = nameof(RenderMessageStep.RenderDone);
|
||||
public const string RenderError = nameof(RenderMessageStep.RenderError);
|
||||
public const string RenderInnerMessage = nameof(RenderMessageStep.RenderInnerMessage);
|
||||
public const string RenderMessage = nameof(RenderMessageStep.RenderMessage);
|
||||
public const string RenderUserText = nameof(RenderMessageStep.RenderUserText);
|
||||
}
|
||||
|
||||
private static readonly Stopwatch s_timer = Stopwatch.StartNew();
|
||||
|
||||
/// <summary>
|
||||
/// Render an explicit message to indicate the process has completed in the expected state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If this message isn't rendered, the process is considered to have failed.
|
||||
/// </remarks>
|
||||
[KernelFunction]
|
||||
public void RenderDone()
|
||||
{
|
||||
Render("DONE!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render exception
|
||||
/// </summary>
|
||||
[KernelFunction]
|
||||
public void RenderError(KernelProcessError error, ILogger logger)
|
||||
{
|
||||
string message = string.IsNullOrWhiteSpace(error.Message) ? "Unexpected failure" : error.Message;
|
||||
Render($"ERROR: {message} [{error.GetType().Name}]{Environment.NewLine}{error.StackTrace}");
|
||||
logger.LogError("Unexpected failure: {ErrorMessage} [{ErrorType}]", error.Message, error.Type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render user input
|
||||
/// </summary>
|
||||
[KernelFunction]
|
||||
public void RenderUserText(string message)
|
||||
{
|
||||
Render($"{AuthorRole.User.Label.ToUpperInvariant()}: {message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render an assistant message from the primary chat
|
||||
/// </summary>
|
||||
[KernelFunction]
|
||||
public void RenderMessage(ChatMessageContent? message)
|
||||
{
|
||||
if (message is null)
|
||||
{
|
||||
// if the message is empty, we don't want to render it
|
||||
return;
|
||||
}
|
||||
|
||||
Render(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render an assistant message from the inner chat
|
||||
/// </summary>
|
||||
[KernelFunction]
|
||||
public void RenderInnerMessage(ChatMessageContent message)
|
||||
{
|
||||
Render(message, indent: true);
|
||||
}
|
||||
|
||||
public static void Render(ChatMessageContent message, bool indent = false)
|
||||
{
|
||||
string displayName = !string.IsNullOrWhiteSpace(message.AuthorName) ? $" - {message.AuthorName}" : string.Empty;
|
||||
Render($"{(indent ? "\t" : string.Empty)}{message.Role.Label.ToUpperInvariant()}{displayName}: {message.Content}");
|
||||
}
|
||||
|
||||
public static void Render(string message)
|
||||
{
|
||||
Console.WriteLine($"[{s_timer.Elapsed:mm\\:ss}] {message}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using System.Text;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Resources;
|
||||
|
||||
namespace Step05;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrate usage of <see cref="KernelProcessMap"/> for a map-reduce operation.
|
||||
/// </summary>
|
||||
public class Step05_MapReduce : BaseTest
|
||||
{
|
||||
// Target Open AI Services
|
||||
protected override bool ForceOpenAI => true;
|
||||
|
||||
/// <summary>
|
||||
/// Factor to increase the scale of the content processed.
|
||||
/// </summary>
|
||||
private const int ScaleFactor = 100;
|
||||
|
||||
private readonly string _sourceContent;
|
||||
|
||||
public Step05_MapReduce(ITestOutputHelper output)
|
||||
: base(output, redirectSystemConsoleOutput: true)
|
||||
{
|
||||
// Initialize the test content
|
||||
StringBuilder content = new();
|
||||
|
||||
for (int count = 0; count < ScaleFactor; ++count)
|
||||
{
|
||||
content.AppendLine(EmbeddedResource.Read("Grimms-The-King-of-the-Golden-Mountain.txt"));
|
||||
content.AppendLine(EmbeddedResource.Read("Grimms-The-Water-of-Life.txt"));
|
||||
content.AppendLine(EmbeddedResource.Read("Grimms-The-White-Snake.txt"));
|
||||
}
|
||||
|
||||
this._sourceContent = content.ToString().ToUpperInvariant();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunMapReduceAsync()
|
||||
{
|
||||
// Define the process
|
||||
KernelProcess process = SetupMapReduceProcess(nameof(RunMapReduceAsync), "Start");
|
||||
|
||||
// Execute the process
|
||||
Kernel kernel = new();
|
||||
await using LocalKernelProcessContext localProcess =
|
||||
await process.StartAsync(
|
||||
kernel,
|
||||
new KernelProcessEvent
|
||||
{
|
||||
Id = "Start",
|
||||
Data = this._sourceContent,
|
||||
});
|
||||
|
||||
// Display the results
|
||||
Dictionary<string, int> results = (Dictionary<string, int>?)kernel.Data[ResultStep.ResultKey] ?? [];
|
||||
foreach (var result in results)
|
||||
{
|
||||
Console.WriteLine($"{result.Key}: {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
private KernelProcess SetupMapReduceProcess(string processName, string inputEventId)
|
||||
{
|
||||
ProcessBuilder process = new(processName);
|
||||
|
||||
ProcessStepBuilder chunkStep = process.AddStepFromType<ChunkStep>();
|
||||
process
|
||||
.OnInputEvent(inputEventId)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(chunkStep));
|
||||
|
||||
ProcessMapBuilder mapStep = process.AddMapStepFromType<CountStep>();
|
||||
chunkStep
|
||||
.OnEvent(ChunkStep.EventId)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(mapStep));
|
||||
|
||||
ProcessStepBuilder resultStep = process.AddStepFromType<ResultStep>();
|
||||
mapStep
|
||||
.OnEvent(CountStep.EventId)
|
||||
.SendEventTo(new ProcessFunctionTargetBuilder(resultStep));
|
||||
|
||||
return process.Build();
|
||||
}
|
||||
|
||||
// Step for breaking the content into chunks
|
||||
private sealed class ChunkStep : KernelProcessStep
|
||||
{
|
||||
public const string EventId = "ChunkComplete";
|
||||
|
||||
[KernelFunction]
|
||||
public async ValueTask ChunkAsync(KernelProcessStepContext context, string content)
|
||||
{
|
||||
int chunkSize = content.Length / Environment.ProcessorCount;
|
||||
string[] chunks = ChunkContent(content, chunkSize).ToArray();
|
||||
|
||||
await context.EmitEventAsync(new() { Id = EventId, Data = chunks });
|
||||
}
|
||||
|
||||
private IEnumerable<string> ChunkContent(string content, int chunkSize)
|
||||
{
|
||||
for (int index = 0; index < content.Length; index += chunkSize)
|
||||
{
|
||||
yield return content.Substring(index, Math.Min(chunkSize, content.Length - index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step for counting the words in a chunk
|
||||
private sealed class CountStep : KernelProcessStep
|
||||
{
|
||||
public const string EventId = "CountComplete";
|
||||
|
||||
[KernelFunction]
|
||||
public async ValueTask ComputeAsync(KernelProcessStepContext context, string chunk)
|
||||
{
|
||||
Dictionary<string, int> counts = [];
|
||||
|
||||
string[] words = chunk.Split([" ", "\n", "\r", ".", ",", "’"], StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (string word in words)
|
||||
{
|
||||
if (s_notInteresting.Contains(word))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
counts.TryGetValue(word.Trim(), out int count);
|
||||
counts[word] = ++count;
|
||||
}
|
||||
|
||||
await context.EmitEventAsync(new() { Id = EventId, Data = counts });
|
||||
}
|
||||
}
|
||||
|
||||
// Step for combining the results
|
||||
private sealed class ResultStep : KernelProcessStep
|
||||
{
|
||||
public const string ResultKey = "WordCount";
|
||||
|
||||
[KernelFunction]
|
||||
public async ValueTask ComputeAsync(KernelProcessStepContext context, IList<Dictionary<string, int>> results, Kernel kernel)
|
||||
{
|
||||
Dictionary<string, int> totals = [];
|
||||
|
||||
foreach (Dictionary<string, int> result in results)
|
||||
{
|
||||
foreach (KeyValuePair<string, int> pair in result)
|
||||
{
|
||||
totals.TryGetValue(pair.Key, out int count);
|
||||
totals[pair.Key] = count + pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
var sorted =
|
||||
from kvp in totals
|
||||
orderby kvp.Value descending
|
||||
select kvp;
|
||||
|
||||
kernel.Data[ResultKey] = sorted.Take(10).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Uninteresting words to remove from content
|
||||
private static readonly HashSet<string> s_notInteresting =
|
||||
[
|
||||
"A",
|
||||
"ALL",
|
||||
"AN",
|
||||
"AND",
|
||||
"AS",
|
||||
"AT",
|
||||
"BE",
|
||||
"BEFORE",
|
||||
"BUT",
|
||||
"BY",
|
||||
"CAME",
|
||||
"COULD",
|
||||
"FOR",
|
||||
"GO",
|
||||
"HAD",
|
||||
"HAVE",
|
||||
"HE",
|
||||
"HER",
|
||||
"HIM",
|
||||
"HIMSELF",
|
||||
"HIS",
|
||||
"HOW",
|
||||
"I",
|
||||
"IF",
|
||||
"IN",
|
||||
"INTO",
|
||||
"IS",
|
||||
"IT",
|
||||
"ME",
|
||||
"MUST",
|
||||
"MY",
|
||||
"NO",
|
||||
"NOT",
|
||||
"NOW",
|
||||
"OF",
|
||||
"ON",
|
||||
"ONCE",
|
||||
"ONE",
|
||||
"ONLY",
|
||||
"OUT",
|
||||
"S",
|
||||
"SAID",
|
||||
"SAW",
|
||||
"SET",
|
||||
"SHE",
|
||||
"SHOULD",
|
||||
"SO",
|
||||
"THAT",
|
||||
"THE",
|
||||
"THEM",
|
||||
"THEN",
|
||||
"THEIR",
|
||||
"THERE",
|
||||
"THEY",
|
||||
"THIS",
|
||||
"TO",
|
||||
"VERY",
|
||||
"WAS",
|
||||
"WENT",
|
||||
"WERE",
|
||||
"WHAT",
|
||||
"WHEN",
|
||||
"WHO",
|
||||
"WILL",
|
||||
"WITH",
|
||||
"WOULD",
|
||||
"UP",
|
||||
"UPON",
|
||||
"YOU",
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Reflection;
|
||||
using PuppeteerSharp;
|
||||
|
||||
namespace Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Renders Mermaid diagrams to images using Puppeteer-Sharp.
|
||||
/// </summary>
|
||||
public static class MermaidRenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a Mermaid diagram image from the provided Mermaid code.
|
||||
/// </summary>
|
||||
/// <param name="mermaidCode"></param>
|
||||
/// <param name="filenameOrPath"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static async Task<string> GenerateMermaidImageAsync(string mermaidCode, string filenameOrPath)
|
||||
{
|
||||
// Ensure the filename has the correct .png extension
|
||||
if (!filenameOrPath.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("The filename must have a .png extension.", nameof(filenameOrPath));
|
||||
}
|
||||
|
||||
string outputFilePath;
|
||||
|
||||
// Check if the user provided an absolute path
|
||||
if (Path.IsPathRooted(filenameOrPath))
|
||||
{
|
||||
// Use the provided absolute path
|
||||
outputFilePath = filenameOrPath;
|
||||
|
||||
// Ensure the directory exists
|
||||
string directoryPath = Path.GetDirectoryName(outputFilePath)
|
||||
?? throw new InvalidOperationException("Could not determine the directory path.");
|
||||
if (!Directory.Exists(directoryPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"The directory '{directoryPath}' does not exist.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use the assembly's directory for relative paths
|
||||
string? assemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
if (assemblyPath == null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not determine the assembly path.");
|
||||
}
|
||||
|
||||
string outputPath = Path.Combine(assemblyPath, "output");
|
||||
Directory.CreateDirectory(outputPath); // Ensure output directory exists
|
||||
outputFilePath = Path.Combine(outputPath, filenameOrPath);
|
||||
}
|
||||
|
||||
// Download Chromium if it hasn't been installed yet
|
||||
BrowserFetcher browserFetcher = new();
|
||||
browserFetcher.Browser = SupportedBrowser.Chrome;
|
||||
await browserFetcher.DownloadAsync();
|
||||
|
||||
// Define the HTML template with Mermaid.js CDN
|
||||
string htmlContent = $@"
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
}}
|
||||
</style>
|
||||
<script type=""module"">
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
||||
mermaid.initialize({{ startOnLoad: true }});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class=""mermaid"">
|
||||
{mermaidCode}
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
// Create a temporary HTML file with the Mermaid code
|
||||
string tempHtmlFile = Path.Combine(Path.GetTempPath(), "mermaid_temp.html");
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempHtmlFile, htmlContent);
|
||||
|
||||
// Launch Puppeteer-Sharp with a headless browser to render the Mermaid diagram
|
||||
using (var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true }))
|
||||
using (var page = await browser.NewPageAsync())
|
||||
{
|
||||
await page.GoToAsync($"file://{tempHtmlFile}");
|
||||
await page.WaitForSelectorAsync(".mermaid"); // Wait for Mermaid to render
|
||||
await page.ScreenshotAsync(outputFilePath, new ScreenshotOptions { FullPage = true });
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
throw new IOException("An error occurred while accessing the file.", ex);
|
||||
}
|
||||
catch (Exception ex) // Catch any other exceptions that might occur
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"An unexpected error occurred during the Mermaid diagram rendering.", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up the temporary HTML file
|
||||
if (File.Exists(tempHtmlFile))
|
||||
{
|
||||
File.Delete(tempHtmlFile);
|
||||
}
|
||||
}
|
||||
|
||||
return outputFilePath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Process.Models;
|
||||
|
||||
namespace Utilities;
|
||||
|
||||
public static class ProcessStateMetadataUtilities
|
||||
{
|
||||
// Path used for storing json processes samples in repository
|
||||
private static readonly string s_currentSourceDir = Path.Combine(
|
||||
Directory.GetCurrentDirectory(), "..", "..", "..");
|
||||
|
||||
private static readonly JsonSerializerOptions s_jsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
public static void DumpProcessStateMetadataLocally(KernelProcessStateMetadata processStateInfo, string jsonFilename)
|
||||
{
|
||||
var filepath = GetRepositoryProcessStateFilepath(jsonFilename);
|
||||
StoreProcessStateLocally(processStateInfo, filepath);
|
||||
}
|
||||
|
||||
public static KernelProcessStateMetadata? LoadProcessStateMetadata(string jsonRelativePath)
|
||||
{
|
||||
var filepath = GetRepositoryProcessStateFilepath(jsonRelativePath, checkFilepathExists: true);
|
||||
|
||||
Console.WriteLine($"Loading ProcessStateMetadata from:\n'{Path.GetFullPath(filepath)}'");
|
||||
|
||||
using StreamReader reader = new(filepath);
|
||||
var content = reader.ReadToEnd();
|
||||
return JsonSerializer.Deserialize<KernelProcessStateMetadata>(content, s_jsonOptions);
|
||||
}
|
||||
|
||||
private static string GetRepositoryProcessStateFilepath(string jsonRelativePath, bool checkFilepathExists = false)
|
||||
{
|
||||
string filepath = Path.Combine(s_currentSourceDir, jsonRelativePath);
|
||||
if (checkFilepathExists && !File.Exists(filepath))
|
||||
{
|
||||
throw new KernelException($"Filepath {filepath} does not exist");
|
||||
}
|
||||
|
||||
return filepath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Function that stores the definition of the SK Process State`.<br/>
|
||||
/// </summary>
|
||||
/// <param name="processStateInfo">Process State to be stored</param>
|
||||
/// <param name="fullFilepath">Filepath to store definition of process in json format</param>
|
||||
private static void StoreProcessStateLocally(KernelProcessStateMetadata processStateInfo, string fullFilepath)
|
||||
{
|
||||
if (!(Path.GetDirectoryName(fullFilepath) is string directory && Directory.Exists(directory)))
|
||||
{
|
||||
throw new KernelException($"Directory for path '{fullFilepath}' does not exist, could not save process {processStateInfo.Name}");
|
||||
}
|
||||
|
||||
if (!(Path.GetExtension(fullFilepath) is string extension && !string.IsNullOrEmpty(extension) && extension == ".json"))
|
||||
{
|
||||
throw new KernelException($"Filepath for process {processStateInfo.Name} does not have .json extension");
|
||||
}
|
||||
|
||||
string content = JsonSerializer.Serialize(processStateInfo, s_jsonOptions);
|
||||
Console.WriteLine($"Process State: \n{content}");
|
||||
Console.WriteLine($"Saving Process State Locally: \n{Path.GetFullPath(fullFilepath)}");
|
||||
File.WriteAllText(fullFilepath, content);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user