chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -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);
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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 });
}
}
@@ -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 });
}
}
@@ -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?");
}
}
@@ -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?");
}
}
@@ -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) { }
}
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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);
}
}