diff --git a/dotnet/samples/GettingStartedWithProcesses/README.md b/dotnet/samples/GettingStartedWithProcesses/README.md
index e4c269a8af..be9b8142da 100644
--- a/dotnet/samples/GettingStartedWithProcesses/README.md
+++ b/dotnet/samples/GettingStartedWithProcesses/README.md
@@ -23,7 +23,7 @@ Example|Description
---|---
[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
[Step02_AccountOpening](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/Step02/Step02_AccountOpening.cs)|Showcasing processes cycles, fan in, fan out for opening an account.
-[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 with food preparation samples.
+[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_.
@@ -77,7 +77,9 @@ The following recipes for preparation of Order Items are defined as SK Processes
#### Product Preparation Processes
-##### Potato Fries Preparation Process
+##### Stateless Product Preparation Processes
+
+###### Potato Fries Preparation Process
``` mermaid
flowchart LR
@@ -92,7 +94,7 @@ flowchart LR
FryStep -->|Fried Potato Ruined
_Fried Food Ruined_| GatherIngredientsStep
```
-##### Fried Fish Preparation Process
+###### Fried Fish Preparation Process
``` mermaid
flowchart LR
@@ -107,7 +109,7 @@ flowchart LR
FryStep -->|**Fried Fish Ruined**
_Fried Food Ruined_| GatherIngredientsStep
```
-##### Fish Sandwich Preparation Process
+###### Fish Sandwich Preparation Process
``` mermaid
flowchart LR
@@ -121,7 +123,7 @@ flowchart LR
PrepareFishSandwichEvent -->|Prepare Fried Fish| FriedFishStep -->|Fried Fish Ready| AddBunsStep --> |Buns Added | AddSpecialSauceStep --> |Special Sauce Added | FishSandwichReadyEvent
```
-##### Fish And Chips Preparation Process
+###### Fish And Chips Preparation Process
``` mermaid
flowchart LR
@@ -137,6 +139,88 @@ flowchart LR
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
Fries Event])
+ PotatoFriesReadyEvent([Potato Fries
Ready Event])
+ OutOfStock([Ingredients
Out of Stock
Event])
+
+ FryStep[Fry Food
Step]
+
+ subgraph GatherIngredientsStep[Gather Ingredients Step]
+ GatherIngredientsFunction[Gather Potato
Function]
+ IngredientsState[(Ingredients
Stock
State)]
+ end
+ subgraph CutStep ["Cut Food Step"]
+ direction LR
+ SliceFoodFunction[Slice Food
Function]
+ SharpenKnifeFunction[Sharpen Knife
Function]
+ CutState[(Knife
Sharpness
State)]
+ end
+
+ CutStep --> |**Potato Sliced Ready**
_Food Sliced Ready_ | FryStep --> |_Fried Food Ready_|PotatoFriesReadyEvent
+ FryStep -->|Fried Potato Ruined
_Fried Food Ruined_| GatherIngredientsStep
+ GatherIngredientsStep --> OutOfStock
+
+ SliceFoodFunction --> |Knife Needs Sharpening| SharpenKnifeFunction
+ SharpenKnifeFunction --> |Knife Sharpened| SliceFoodFunction
+
+ GatherIngredientsStep -->| Slice Potatoes
_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
Fish Event])
+ FriedFishReadyEvent([Fried Fish
Ready Event])
+ OutOfStock([Ingredients
Out of Stock
Event])
+
+ FryStep[Fry Food
Step]
+
+ subgraph GatherIngredientsStep[Gather Ingredients Step]
+ GatherIngredientsFunction[Gather Fish
Function]
+ IngredientsState[(Ingredients
Stock
State)]
+ end
+ subgraph CutStep ["Cut Food Step"]
+ direction LR
+ ChopFoodFunction[Chop Food
Function]
+ SharpenKnifeFunction[Sharpen Knife
Function]
+ CutState[(Knife
Sharpness
State)]
+ end
+
+ CutStep --> |**Fish Chopped Ready**
_Food Chopped Ready_| FryStep --> |_Fried Food Ready_|FriedFishReadyEvent
+ FryStep -->|**Fried Fish Ruined**
_Fried Food Ruined_| GatherIngredientsStep
+ GatherIngredientsStep --> OutOfStock
+
+ ChopFoodFunction --> |Knife Needs Sharpening| SharpenKnifeFunction
+ SharpenKnifeFunction --> |Knife Sharpened| ChopFoodFunction
+
+ GatherIngredientsStep -->| Chop Fish
_Ingredients Gathered_ | CutStep
+ PrepareFriedFishEvent --> GatherIngredientsStep
+```
+
### Step03b_FoodOrdering
#### Single Order Preparation Process
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs
index 31053f64c3..88d5dc752c 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/FriedFishProcess.cs
@@ -7,7 +7,6 @@ namespace Step03.Processes;
///
/// Sample process that showcases how to create a process with sequential steps and reuse of existing steps.
-/// For a visual reference of the FriedFishProcess check this diagram
///
public static class FriedFishProcess
{
@@ -20,6 +19,12 @@ public static class FriedFishProcess
public const string FriedFishReady = nameof(FryFoodStep.OutputEvents.FriedFoodReady);
}
+ ///
+ /// For a visual reference of the FriedFishProcess check this
+ /// diagram
+ ///
+ /// name of the process
+ ///
public static ProcessBuilder CreateProcess(string processName = "FriedFishProcess")
{
var processBuilder = new ProcessBuilder(processName);
@@ -47,21 +52,58 @@ public static class FriedFishProcess
return processBuilder;
}
+ ///
+ /// For a visual reference of the FriedFishProcess with stateful steps check this
+ /// diagram
+ ///
+ /// name of the process
+ ///
+ public static ProcessBuilder CreateProcessWithStatefulSteps(string processName = "FriedFishWithStatefulStepsProcess")
+ {
+ var processBuilder = new ProcessBuilder(processName);
+
+ var gatherIngredientsStep = processBuilder.AddStepFromType();
+ var chopStep = processBuilder.AddStepFromType("chopStep");
+ var fryStep = processBuilder.AddStepFromType();
+
+ processBuilder
+ .OnInputEvent(ProcessEvents.PrepareFriedFish)
+ .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
+
+ gatherIngredientsStep
+ .OnEvent(GatherFriedFishIngredientsWithStockStep.OutputEvents.IngredientsGathered)
+ .SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodWithSharpeningStep.Functions.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.Functions.SharpenKnife));
+
+ chopStep
+ .OnEvent(CutFoodWithSharpeningStep.OutputEvents.KnifeSharpened)
+ .SendEventTo(new ProcessFunctionTargetBuilder(chopStep, functionName: CutFoodWithSharpeningStep.Functions.ChopFood));
+
+ fryStep
+ .OnEvent(FryFoodStep.OutputEvents.FoodRuined)
+ .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
+
+ return processBuilder;
+ }
+
private sealed class GatherFriedFishIngredientsStep : GatherIngredientsStep
{
- public override async Task GatherIngredientsAsync(KernelProcessStepContext context, List foodActions)
- {
- var ingredient = FoodIngredients.Fish.ToFriendlyString();
- var updatedFoodActions = new List();
- updatedFoodActions.AddRange(foodActions);
- if (updatedFoodActions.Count == 0)
- {
- updatedFoodActions.Add(ingredient);
- }
- updatedFoodActions.Add($"{ingredient}_gathered");
+ public GatherFriedFishIngredientsStep() : base(FoodIngredients.Fish) { }
+ }
- Console.WriteLine($"GATHER_INGREDIENT: Gathered ingredient {ingredient}");
- await context.EmitEventAsync(new() { Id = OutputEvents.IngredientsGathered, Data = updatedFoodActions });
- }
+ private sealed class GatherFriedFishIngredientsWithStockStep : GatherIngredientsWithStockStep
+ {
+ public GatherFriedFishIngredientsWithStockStep() : base(FoodIngredients.Fish) { }
}
}
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/PotatoFriesProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/PotatoFriesProcess.cs
index 3794c05b86..bdf9c62e6c 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/PotatoFriesProcess.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Processes/PotatoFriesProcess.cs
@@ -8,7 +8,6 @@ namespace Step03.Processes;
///
/// Sample process that showcases how to create a process with sequential steps and reuse of existing steps.
-/// For a visual reference of the FriedFishProcess check this diagram
///
public static class PotatoFriesProcess
{
@@ -21,6 +20,12 @@ public static class PotatoFriesProcess
public const string PotatoFriesReady = nameof(FryFoodStep.OutputEvents.FriedFoodReady);
}
+ ///
+ /// For a visual reference of the PotatoFriesProcess check this
+ /// diagram
+ ///
+ /// name of the process
+ ///
public static ProcessBuilder CreateProcess(string processName = "PotatoFriesProcess")
{
var processBuilder = new ProcessBuilder(processName);
@@ -48,26 +53,58 @@ public static class PotatoFriesProcess
return processBuilder;
}
+ ///
+ /// For a visual reference of the PotatoFriesProcess with stateful steps check this
+ /// diagram
+ ///
+ /// name of the process
+ ///
+ public static ProcessBuilder CreateProcessWithStatefulSteps(string processName = "PotatoFriesWithStatefulStepsProcess")
+ {
+ var processBuilder = new ProcessBuilder(processName);
+
+ var gatherIngredientsStep = processBuilder.AddStepFromType();
+ var sliceStep = processBuilder.AddStepFromType("sliceStep");
+ var fryStep = processBuilder.AddStepFromType();
+
+ processBuilder
+ .OnInputEvent(ProcessEvents.PreparePotatoFries)
+ .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
+
+ gatherIngredientsStep
+ .OnEvent(GatherPotatoFriesIngredientsWithStockStep.OutputEvents.IngredientsGathered)
+ .SendEventTo(new ProcessFunctionTargetBuilder(sliceStep, functionName: CutFoodWithSharpeningStep.Functions.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.Functions.SharpenKnife));
+
+ sliceStep
+ .OnEvent(CutFoodWithSharpeningStep.OutputEvents.KnifeSharpened)
+ .SendEventTo(new ProcessFunctionTargetBuilder(sliceStep, functionName: CutFoodWithSharpeningStep.Functions.SliceFood));
+
+ fryStep
+ .OnEvent(FryFoodStep.OutputEvents.FoodRuined)
+ .SendEventTo(new ProcessFunctionTargetBuilder(gatherIngredientsStep));
+
+ return processBuilder;
+ }
+
private sealed class GatherPotatoFriesIngredientsStep : GatherIngredientsStep
{
- public override async Task GatherIngredientsAsync(KernelProcessStepContext context, List foodActions)
- {
- var ingredient = FoodIngredients.Pototoes.ToFriendlyString();
- var updatedFoodActions = new List();
- 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 });
- }
+ public GatherPotatoFriesIngredientsStep() : base(FoodIngredients.Pototoes) { }
}
- private sealed class ExternalPotatoFriesStep : ExternalStep
+ private sealed class GatherPotatoFriesIngredientsWithStockStep : GatherIngredientsWithStockStep
{
- public ExternalPotatoFriesStep() : base(ProcessEvents.PotatoFriesReady) { }
+ public GatherPotatoFriesIngredientsWithStockStep() : base(FoodIngredients.Pototoes) { }
}
}
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs
index 30532c89c7..578e7c1b90 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Step03a_FoodPreparation.cs
@@ -15,6 +15,7 @@ public class Step03a_FoodPreparation(ITestOutputHelper output) : BaseTest(output
// Target Open AI Services
protected override bool ForceOpenAI => true;
+ #region Stateless Processes
[Fact]
public async Task UsePrepareFriedFishProcessAsync()
{
@@ -42,7 +43,75 @@ public class Step03a_FoodPreparation(ITestOutputHelper output) : BaseTest(output
var process = FishAndChipsProcess.CreateProcess();
await UsePrepareSpecificProductAsync(process, FishAndChipsProcess.ProcessEvents.PrepareFishAndChips);
}
+ #endregion
+ #region Stateful Processes
+ ///
+ /// Test case that showcase when the same process is build multiple times, it will have different initial states
+ ///
+ ///
+ [Fact]
+ public async Task UsePrepareStatefulFriedFishProcessNoSharedStateAsync()
+ {
+ var processBuilder = FriedFishProcess.CreateProcessWithStatefulSteps();
+ 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}' ===");
+ }
+
+ ///
+ /// 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
+ ///
+ ///
+ [Fact]
+ public async Task UsePrepareStatefulFriedFishProcessSharedStateAsync()
+ {
+ var processBuilder = FriedFishProcess.CreateProcessWithStatefulSteps();
+ 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 ExecuteProcessWithStateAsync(KernelProcess process, Kernel kernel, string externalTriggerEvent, string orderLabel)
+ {
+ Console.WriteLine($"=== {orderLabel} ===");
+ var runningProcess = await process.StartAsync(kernel, new KernelProcessEvent()
+ {
+ Id = externalTriggerEvent,
+ Data = new List()
+ });
+ return await runningProcess.GetStateAsync();
+ }
+ #endregion
protected async Task UsePrepareSpecificProductAsync(ProcessBuilder processBuilder, string externalTriggerEvent)
{
// Arrange
@@ -52,9 +121,11 @@ public class Step03a_FoodPreparation(ITestOutputHelper output) : BaseTest(output
KernelProcess kernelProcess = processBuilder.Build();
// Assert
+ Console.WriteLine($"=== Start SK Process '{processBuilder.Name}' ===");
using var runningProcess = await kernelProcess.StartAsync(kernel, new KernelProcessEvent()
{
Id = externalTriggerEvent, Data = new List()
});
+ Console.WriteLine($"=== End SK Process '{processBuilder.Name}' ===");
}
}
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/CutFoodWithSharpeningStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/CutFoodWithSharpeningStep.cs
new file mode 100644
index 0000000000..550aacb3e8
--- /dev/null
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/CutFoodWithSharpeningStep.cs
@@ -0,0 +1,99 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.SemanticKernel;
+
+namespace Step03.Steps;
+
+///
+/// Step used in the Processes Samples:
+/// - Step_03_FoodPreparation.cs
+///
+public class CutFoodWithSharpeningStep : KernelProcessStep
+{
+ public static class Functions
+ {
+ 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 state)
+ {
+ _state = state.State;
+ return ValueTask.CompletedTask;
+ }
+
+ [KernelFunction(Functions.ChopFood)]
+ public async Task ChopFoodAsync(KernelProcessStepContext context, List 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(Functions.SliceFood)]
+ public async Task SliceFoodAsync(KernelProcessStepContext context, List 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(Functions.SharpenKnife)]
+ public async Task SharpenKnifeAsync(KernelProcessStepContext context, List 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}";
+ }
+}
+
+///
+/// The state object for the .
+///
+public sealed class CutFoodWithSharpeningState
+{
+ internal int KnifeSharpness { get; set; } = 5;
+
+ internal int _needsSharpeningLimit = 3;
+ internal int _sharpeningBoost = 5;
+}
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/FryFoodStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/FryFoodStep.cs
index 708ea0daed..532be358c7 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/FryFoodStep.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/FryFoodStep.cs
@@ -31,7 +31,7 @@ public class FryFoodStep : KernelProcessStep
int fryerMalfunction = _randomSeed.Next(0, 10);
// foodToFry could potentially be used to set the frying temperature and cooking duration
- if (fryerMalfunction > 6)
+ if (fryerMalfunction < 5)
{
// Oh no! Food got burnt :(
foodActions.Add($"{foodToFry}_frying_failed");
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/GatherIngredientsStep.cs b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/GatherIngredientsStep.cs
index 7f1d1dcec7..7423b9ba51 100644
--- a/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/GatherIngredientsStep.cs
+++ b/dotnet/samples/GettingStartedWithProcesses/Step03/Steps/GatherIngredientsStep.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
+using Step03.Models;
namespace Step03.Steps;
///
@@ -18,6 +19,13 @@ public class GatherIngredientsStep : KernelProcessStep
public const string IngredientsGathered = nameof(IngredientsGathered);
}
+ private readonly FoodIngredients _ingredient;
+
+ public GatherIngredientsStep(FoodIngredients ingredient)
+ {
+ this._ingredient = ingredient;
+ }
+
///
/// Method to be overridden by the user set custom ingredients to be gathered and events to be triggered
///
@@ -27,6 +35,90 @@ public class GatherIngredientsStep : KernelProcessStep
[KernelFunction(Functions.GatherIngredients)]
public virtual async Task GatherIngredientsAsync(KernelProcessStepContext context, List foodActions)
{
- await context.EmitEventAsync(new() { Id = OutputEvents.IngredientsGathered, Data = foodActions });
+ var ingredient = this._ingredient.ToFriendlyString();
+ var updatedFoodActions = new List();
+ 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 });
}
}
+
+///
+/// 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
+///
+public class GatherIngredientsWithStockStep : KernelProcessStep
+{
+ public static class Functions
+ {
+ 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 state)
+ {
+ _state = state.State;
+ return ValueTask.CompletedTask;
+ }
+
+ ///
+ /// Method to be overridden by the user set custom ingredients to be gathered and events to be triggered
+ ///
+ /// The context for the current step and process.
+ /// list of actions taken to the food
+ ///
+ [KernelFunction(Functions.GatherIngredients)]
+ public virtual async Task GatherIngredientsAsync(KernelProcessStepContext context, List foodActions)
+ {
+ var ingredient = this._ingredient.ToFriendlyString(); ;
+ var updatedFoodActions = new List();
+ 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 });
+ }
+}
+
+///
+/// The state object for the .
+///
+public sealed class GatherIngredientsState
+{
+ internal int IngredientsStock { get; set; } = 5;
+}