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; +}