.Net: Processes - Stateful food preparation samples (#9300)

### Description

Adding Stateful Food Preparation Samples

- refactoring existing gatherIngredients step
- adding stateful gatherIngredients step
- adding stateful cutFoodStep with knifeSharpening to showcase state
persistance and use across functions in same step
- adding samples making use of different processes - state resets - and
the same process multiple times - last state gets reused

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄
This commit is contained in:
Estefanía Tenorio
2024-10-17 17:33:43 +00:00
committed by GitHub
parent 22757e17c4
commit db354ec387
7 changed files with 463 additions and 38 deletions
@@ -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 <br/> _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** <br/> _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 <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
@@ -7,7 +7,6 @@ namespace Step03.Processes;
/// <summary>
/// Sample process that showcases how to create a process with sequential steps and reuse of existing steps.<br/>
/// 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>
public static class FriedFishProcess
{
@@ -20,6 +19,12 @@ public static class FriedFishProcess
public const string FriedFishReady = nameof(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);
@@ -47,21 +52,58 @@ public static class FriedFishProcess
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 CreateProcessWithStatefulSteps(string processName = "FriedFishWithStatefulStepsProcess")
{
var processBuilder = new ProcessBuilder(processName);
var gatherIngredientsStep = processBuilder.AddStepFromType<GatherFriedFishIngredientsWithStockStep>();
var chopStep = processBuilder.AddStepFromType<CutFoodWithSharpeningStep>("chopStep");
var fryStep = processBuilder.AddStepFromType<FryFoodStep>();
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<string> foodActions)
{
var ingredient = FoodIngredients.Fish.ToFriendlyString();
var updatedFoodActions = new List<string>();
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) { }
}
}
@@ -8,7 +8,6 @@ namespace Step03.Processes;
/// <summary>
/// Sample process that showcases how to create a process with sequential steps and reuse of existing steps.<br/>
/// For a visual reference of the FriedFishProcess check this <see href="https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithProcesses/README.md#potato-fries-preparation-process" >diagram</see>
/// </summary>
public static class PotatoFriesProcess
{
@@ -21,6 +20,12 @@ public static class PotatoFriesProcess
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);
@@ -48,26 +53,58 @@ public static class PotatoFriesProcess
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.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<string> foodActions)
{
var ingredient = FoodIngredients.Pototoes.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 });
}
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) { }
}
}
@@ -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
/// <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.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}' ===");
}
/// <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.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<KernelProcess> 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<string>()
});
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<string>()
});
Console.WriteLine($"=== End SK Process '{processBuilder.Name}' ===");
}
}
@@ -0,0 +1,99 @@
// 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 CutFoodWithSharpeningStep : KernelProcessStep<CutFoodWithSharpeningState>
{
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<CutFoodWithSharpeningState> state)
{
_state = state.State;
return ValueTask.CompletedTask;
}
[KernelFunction(Functions.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(Functions.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(Functions.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
{
internal int KnifeSharpness { get; set; } = 5;
internal int _needsSharpeningLimit = 3;
internal int _sharpeningBoost = 5;
}
@@ -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");
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Step03.Models;
namespace Step03.Steps;
/// <summary>
@@ -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;
}
/// <summary>
/// Method to be overridden by the user set custom ingredients to be gathered and events to be triggered
/// </summary>
@@ -27,6 +35,90 @@ public class GatherIngredientsStep : KernelProcessStep
[KernelFunction(Functions.GatherIngredients)]
public virtual async Task GatherIngredientsAsync(KernelProcessStepContext context, List<string> foodActions)
{
await context.EmitEventAsync(new() { Id = OutputEvents.IngredientsGathered, Data = 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 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<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(Functions.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
{
internal int IngredientsStock { get; set; } = 5;
}