diff --git a/python/samples/concepts/processes/cycles_with_fan_in.py b/python/samples/concepts/processes/cycles_with_fan_in.py
index 3b0591891d78..005e2c1685da 100644
--- a/python/samples/concepts/processes/cycles_with_fan_in.py
+++ b/python/samples/concepts/processes/cycles_with_fan_in.py
@@ -42,8 +42,8 @@ class KickOffStep(KernelProcessStep):
@kernel_function(name=KICK_OFF_FUNCTION)
async def print_welcome_message(self, context: KernelProcessStepContext):
- context.emit_event(KernelProcessEvent(id=CommonEvents.StartARequested.value, data="Get Going A"))
- context.emit_event(KernelProcessEvent(id=CommonEvents.StartBRequested.value, data="Get Going B"))
+ await context.emit_event(process_event=CommonEvents.StartARequested.value, data="Get Going A")
+ await context.emit_event(process_event=CommonEvents.StartBRequested.value, data="Get Going B")
# Define a sample `AStep` step that will emit an event after 1 second.
@@ -52,7 +52,7 @@ class AStep(KernelProcessStep):
@kernel_function()
async def do_it(self, context: KernelProcessStepContext):
await asyncio.sleep(1)
- context.emit_event(KernelProcessEvent(id=CommonEvents.AStepDone.value, data="I did A"))
+ await context.emit_event(process_event=CommonEvents.AStepDone.value, data="I did A")
# Define a sample `BStep` step that will emit an event after 2 seconds.
@@ -61,7 +61,7 @@ class BStep(KernelProcessStep):
@kernel_function()
async def do_it(self, context: KernelProcessStepContext):
await asyncio.sleep(2)
- context.emit_event(KernelProcessEvent(id=CommonEvents.BStepDone.value, data="I did B"))
+ await context.emit_event(process_event=CommonEvents.BStepDone.value, data="I did B")
# Define a sample `CStepState` that will keep track of the current cycle.
@@ -84,9 +84,9 @@ async def do_it(self, context: KernelProcessStepContext, astepdata: str, bstepda
print(f"CStep Current Cycle: {self.state.current_cycle}")
if self.state.current_cycle == 3:
print("CStep Exit Requested")
- context.emit_event(process_event=KernelProcessEvent(id=CommonEvents.ExitRequested.value))
+ await context.emit_event(process_event=CommonEvents.ExitRequested.value)
return
- context.emit_event(process_event=KernelProcessEvent(id=CommonEvents.CStepDone.value))
+ await context.emit_event(process_event=CommonEvents.CStepDone.value)
kernel = Kernel()
diff --git a/python/samples/concepts/processes/nested_process.py b/python/samples/concepts/processes/nested_process.py
index 14dbd61efb2c..501c26e0b9e7 100644
--- a/python/samples/concepts/processes/nested_process.py
+++ b/python/samples/concepts/processes/nested_process.py
@@ -15,7 +15,6 @@
from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
-from semantic_kernel.processes.local_runtime.local_event import KernelProcessEvent
from semantic_kernel.processes.local_runtime.local_kernel_process import start
from semantic_kernel.processes.process_builder import ProcessBuilder
from semantic_kernel.processes.process_types import TState
@@ -58,17 +57,15 @@ async def repeat(self, message: str, context: KernelProcessStepContext, count: i
self.state.last_message = output
print(f"[REPEAT] {output}")
- context.emit_event(
- process_event=KernelProcessEvent(
- id=ProcessEvents.OutputReadyPublic.value, data=output, visibility=KernelProcessEventVisibility.Public
- )
+ await context.emit_event(
+ process_event=ProcessEvents.OutputReadyPublic.value,
+ data=output,
+ visibility=KernelProcessEventVisibility.Public,
)
- context.emit_event(
- process_event=KernelProcessEvent(
- id=ProcessEvents.OutputReadyInternal.value,
- data=output,
- visibility=KernelProcessEventVisibility.Internal,
- )
+ await context.emit_event(
+ process_event=ProcessEvents.OutputReadyInternal.value,
+ data=output,
+ visibility=KernelProcessEventVisibility.Internal,
)
diff --git a/python/samples/getting_started_with_processes/README.md b/python/samples/getting_started_with_processes/README.md
index 82fca8997113..b4f5b938c87e 100644
--- a/python/samples/getting_started_with_processes/README.md
+++ b/python/samples/getting_started_with_processes/README.md
@@ -20,7 +20,9 @@ The getting started with agents examples include:
Example|Description
---|---
-[step01_processes](../getting_started_with_processes/step01_processes.py)|How to create a simple process with a loop and a conditional exit
+[step01_processes](../getting_started_with_processes/step01/step01_processes.py)|How to create a simple process with a loop and a conditional exit|
+[step03a_food_preparation](../getting_started_with_processes/step03/step03a_food_preparation.py)|Showcasing reuse of steps, creation of processes, spawning of multiple events, use of stateful steps with food preparation samples.
+[step03b_food_ordering](../getting_started_with_processes/step03/step03b_food_ordering.py)|Showcasing use of subprocesses as steps, spawning of multiple events conditionally reusing the food preparation samples.
### step01_processes
@@ -32,6 +34,187 @@ flowchart LR
AssistantResponse--> UserInput
```
+### step03a_food_preparation
+
+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
Fries Event])
+ PotatoFriesReadyEvent([Potato Fries
Ready Event])
+
+ GatherIngredientsStep[Gather Ingredients
Step]
+ CutStep[Cut Food
Step]
+ FryStep[Fry Food
Step]
+
+ PreparePotatoFriesEvent --> GatherIngredientsStep -->| Slice Potatoes
_Ingredients Gathered_ | CutStep --> |**Potato Sliced Ready**
_Food Sliced Ready_ | FryStep --> |_Fried Food Ready_|PotatoFriesReadyEvent
+ FryStep -->|Fried Potato Ruined
_Fried Food Ruined_| GatherIngredientsStep
+```
+
+###### Fried Fish Preparation Process
+
+``` mermaid
+flowchart LR
+ PrepareFriedFishEvent([Prepare Fried
Fish Event])
+ FriedFishReadyEvent([Fried Fish
Ready Event])
+
+ GatherIngredientsStep[Gather Ingredients
Step]
+ CutStep[Cut Food
Step]
+ FryStep[Fry Food
Step]
+
+ PrepareFriedFishEvent --> GatherIngredientsStep -->| Chop Fish
_Ingredients Gathered_ | CutStep --> |**Fish Chopped Ready**
_Food Chopped Ready_| FryStep --> |_Fried Food Ready_ | FriedFishReadyEvent
+ FryStep -->|**Fried Fish Ruined**
_Fried Food Ruined_| GatherIngredientsStep
+```
+
+###### Fish Sandwich Preparation Process
+
+``` mermaid
+flowchart LR
+ PrepareFishSandwichEvent([Prepare Fish
Sandwich Event])
+ FishSandwichReadyEvent([Fish Sandwich
Ready Event])
+
+ FriedFishStep[[Fried Fish
Process Step]]
+ AddBunsStep[Add Buns
Step]
+ AddSpecialSauceStep[Add Special
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
Fish And Chips
Event])
+ FishAndChipsReadyEvent([Fish And Chips
Ready Event])
+
+ FriedFishStep[[Fried Fish
Process Step]]
+ PotatoFriesStep[[Potato Fries
Process Step]]
+ AddCondiments[Add Condiments
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
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_food_ordering
+
+#### 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
Event])
+ SingleOrderReadyEvent([Single Order
Ready Event])
+ OrderPackedEvent([Order Packed
Event])
+
+ DispatchOrderStep{{Dispatch
Order Step}}
+ FriedFishStep[[Fried Fish
Process Step]]
+ PotatoFriesStep[[Potato Fries
Process Step]]
+ FishSandwichStep[[Fish Sandwich
Process Step]]
+ FishAndChipsStep[[Fish & Chips
Process Step]]
+
+ PackFoodStep[Pack Food
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
+```
+
## Configuring the Kernel
Similar to the Semantic Kernel Python concept samples, it is necessary to configure the secrets
diff --git a/python/samples/getting_started_with_processes/step01_processes.py b/python/samples/getting_started_with_processes/step01/step01_processes.py
similarity index 95%
rename from python/samples/getting_started_with_processes/step01_processes.py
rename to python/samples/getting_started_with_processes/step01/step01_processes.py
index b2b2be516e3e..80dde1447776 100644
--- a/python/samples/getting_started_with_processes/step01_processes.py
+++ b/python/samples/getting_started_with_processes/step01/step01_processes.py
@@ -70,13 +70,13 @@ async def get_user_input(self, context: KernelProcessStepContext):
print(f"USER: {user_message}")
if "exit" in user_message:
- context.emit_event(KernelProcessEvent(id=ChatBotEvents.Exit.value, data=None))
+ await context.emit_event(process_event=ChatBotEvents.Exit.value, data=None)
return
self.state.current_input_index += 1
# Emit the user input event
- context.emit_event({"id": CommonEvents.UserInputReceived.value, "data": user_message})
+ await context.emit_event(process_event=CommonEvents.UserInputReceived.value, data=user_message)
class ChatUserInputStep(ScriptedUserInputStep):
@@ -140,9 +140,7 @@ async def get_chat_response(self, context: "KernelProcessStepContext", user_mess
self.state.chat_messages.append(answer)
# Emit an event: assistantResponse
- context.emit_event(
- process_event=KernelProcessEvent(id=ChatBotEvents.AssistantResponseGenerated.value, data=answer)
- )
+ await context.emit_event(process_event=ChatBotEvents.AssistantResponseGenerated.value, data=answer)
kernel = Kernel()
diff --git a/python/samples/getting_started_with_processes/step03/models/__init__.py b/python/samples/getting_started_with_processes/step03/models/__init__.py
new file mode 100644
index 000000000000..373edab489f9
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/models/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from samples.getting_started_with_processes.step03.models.food_ingredients import FoodIngredients
+from samples.getting_started_with_processes.step03.models.food_order_item import FoodItem
+
+__all__ = ["FoodIngredients", "FoodItem"]
diff --git a/python/samples/getting_started_with_processes/step03/models/food_ingredients.py b/python/samples/getting_started_with_processes/step03/models/food_ingredients.py
new file mode 100644
index 000000000000..7b3b6a4831e6
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/models/food_ingredients.py
@@ -0,0 +1,15 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from enum import Enum
+
+
+class FoodIngredients(str, Enum):
+ POTATOES = "Potatoes"
+ FISH = "Fish"
+ BUNS = "Buns"
+ SAUCE = "Sauce"
+ CONDIMENTS = "Condiments"
+ NONE = "None"
+
+ def to_friendly_string(self) -> str:
+ return self.value
diff --git a/python/samples/getting_started_with_processes/step03/models/food_order_item.py b/python/samples/getting_started_with_processes/step03/models/food_order_item.py
new file mode 100644
index 000000000000..71cfe80af724
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/models/food_order_item.py
@@ -0,0 +1,13 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from enum import Enum
+
+
+class FoodItem(str, Enum):
+ POTATO_FRIES = "Potato Fries"
+ FRIED_FISH = "Fried Fish"
+ FISH_SANDWICH = "Fish Sandwich"
+ FISH_AND_CHIPS = "Fish & Chips"
+
+ def to_friendly_string(self) -> str:
+ return self.value
diff --git a/python/samples/getting_started_with_processes/step03/processes/__init__.py b/python/samples/getting_started_with_processes/step03/processes/__init__.py
new file mode 100644
index 000000000000..d8bdb2f082ed
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/processes/__init__.py
@@ -0,0 +1,7 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from samples.getting_started_with_processes.step03.processes.fish_and_chips_process import FishAndChipsProcess
+from samples.getting_started_with_processes.step03.processes.fish_sandwich_process import FishSandwichProcess
+from samples.getting_started_with_processes.step03.processes.fried_fish_process import FriedFishProcess
+
+__all__ = ["FishAndChipsProcess", "FriedFishProcess", "FishSandwichProcess"]
diff --git a/python/samples/getting_started_with_processes/step03/processes/fish_and_chips_process.py b/python/samples/getting_started_with_processes/step03/processes/fish_and_chips_process.py
new file mode 100644
index 000000000000..9fe17941ced0
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/processes/fish_and_chips_process.py
@@ -0,0 +1,72 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import json
+from enum import Enum
+
+from samples.getting_started_with_processes.step03.processes.fried_fish_process import FriedFishProcess
+from samples.getting_started_with_processes.step03.processes.potato_fries_process import PotatoFriesProcess
+from samples.getting_started_with_processes.step03.steps.external_step import ExternalStep
+from semantic_kernel.functions.kernel_function_decorator import kernel_function
+from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
+from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
+from semantic_kernel.processes.process_builder import ProcessBuilder
+from semantic_kernel.processes.process_function_target_builder import ProcessFunctionTargetBuilder
+
+
+class AddFishAndChipsCondimentsStep(KernelProcessStep):
+ class Functions(Enum):
+ AddCondiments = "AddCondiments"
+
+ class OutputEvents(Enum):
+ CondimentsAdded = "CondimentsAdded"
+
+ @kernel_function(name=Functions.AddCondiments.value)
+ async def add_condiments(
+ self, context: KernelProcessStepContext, fish_actions: list[str], potato_actions: list[str]
+ ):
+ print(
+ f"ADD_CONDIMENTS: Added condiments to Fish & Chips - Fish: {json.dumps(fish_actions)}, Potatoes: {json.dumps(potato_actions)}" # noqa: E501
+ )
+ fish_actions.extend(potato_actions)
+ fish_actions.append("Condiments")
+ await context.emit_event(
+ process_event=AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded.value, data=fish_actions
+ )
+
+
+class FishAndChipsProcess:
+ class ProcessEvents(Enum):
+ PrepareFishAndChips = "PrepareFishAndChips"
+ FishAndChipsReady = "FishAndChipsReady"
+
+ class ExternalFishAndChipsStep(ExternalStep):
+ def __init__(self):
+ super().__init__(FishAndChipsProcess.ProcessEvents.FishAndChipsReady.value)
+
+ @staticmethod
+ def create_process(process_name: str = "FishAndChipsProcess"):
+ process_builder = ProcessBuilder(process_name)
+ make_fried_fish_step = process_builder.add_step_from_process(FriedFishProcess.create_process())
+ make_potato_fries_step = process_builder.add_step_from_process(PotatoFriesProcess.create_process())
+ add_condiments_step = process_builder.add_step(AddFishAndChipsCondimentsStep)
+ external_step = process_builder.add_step(FishAndChipsProcess.ExternalFishAndChipsStep)
+
+ process_builder.on_input_event(FishAndChipsProcess.ProcessEvents.PrepareFishAndChips.value).send_event_to(
+ make_fried_fish_step.where_input_event_is(FriedFishProcess.ProcessEvents.PrepareFriedFish.value)
+ ).send_event_to(
+ make_potato_fries_step.where_input_event_is(PotatoFriesProcess.ProcessEvents.PreparePotatoFries.value)
+ )
+
+ make_fried_fish_step.on_event(FriedFishProcess.ProcessEvents.FriedFishReady.value).send_event_to(
+ ProcessFunctionTargetBuilder(add_condiments_step, parameter_name="fishActions")
+ )
+
+ make_potato_fries_step.on_event(PotatoFriesProcess.ProcessEvents.PotatoFriesReady.value).send_event_to(
+ ProcessFunctionTargetBuilder(add_condiments_step, parameter_name="potatoActions")
+ )
+
+ add_condiments_step.on_event(AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded.value).send_event_to(
+ ProcessFunctionTargetBuilder(external_step, parameter_name="fishActions")
+ )
+
+ return process_builder
diff --git a/python/samples/getting_started_with_processes/step03/processes/fish_sandwich_process.py b/python/samples/getting_started_with_processes/step03/processes/fish_sandwich_process.py
new file mode 100644
index 000000000000..db476aa721dc
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/processes/fish_sandwich_process.py
@@ -0,0 +1,76 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from enum import Enum
+
+from samples.getting_started_with_processes.step03.processes.fried_fish_process import FriedFishProcess
+from samples.getting_started_with_processes.step03.steps.external_step import ExternalStep
+from semantic_kernel.functions.kernel_function_decorator import kernel_function
+from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
+from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
+from semantic_kernel.processes.process_builder import ProcessBuilder
+from semantic_kernel.processes.process_function_target_builder import ProcessFunctionTargetBuilder
+
+
+class AddBunStep(KernelProcessStep):
+ class Functions(Enum):
+ AddBuns = "AddBuns"
+
+ class OutputEvents(Enum):
+ BunsAdded = "BunsAdded"
+
+ @kernel_function(name=Functions.AddBuns.value)
+ async def slice_food(self, context: KernelProcessStepContext, food_actions: list[str]):
+ print(f"BUNS_ADDED_STEP: Buns added to ingredient {food_actions[0]}")
+ food_actions.append("Buns")
+ await context.emit_event(process_event=self.OutputEvents.BunsAdded.value, data=food_actions)
+
+
+class AddSpecialSauceStep(KernelProcessStep):
+ class Functions(Enum):
+ AddSpecialSauce = "AddSpecialSauce"
+
+ class OutputEvents(Enum):
+ SpecialSauceAdded = "SpecialSauceAdded"
+
+ @kernel_function(name=Functions.AddSpecialSauce.value)
+ async def slice_food(self, context: KernelProcessStepContext, food_actions: list[str]):
+ print(f"SPECIAL_SAUCE_ADDED: Special sauce added to ingredient {food_actions[0]}")
+ food_actions.append("Sauce")
+ await context.emit_event(process_event=self.OutputEvents.SpecialSauceAdded.value, data=food_actions)
+
+
+class ExternalFriedFishStep(ExternalStep):
+ def __init__(self):
+ super().__init__(FishSandwichProcess.ProcessEvents.FishSandwichReady.value)
+
+
+class FishSandwichProcess:
+ class ProcessEvents(Enum):
+ PrepareFishSandwich = "PrepareFishSandwich"
+ FishSandwichReady = "FishSandwichReady"
+
+ @staticmethod
+ def create_process(process_name: str = "FishSandwichProcess"):
+ process_builder = ProcessBuilder(process_name)
+ make_fried_fish_step = process_builder.add_step_from_process(FriedFishProcess.create_process())
+ add_buns_step = process_builder.add_step(AddBunStep)
+ add_special_sauce_step = process_builder.add_step(AddSpecialSauceStep)
+ external_step = process_builder.add_step(ExternalFriedFishStep)
+
+ process_builder.on_input_event(FishSandwichProcess.ProcessEvents.PrepareFishSandwich.value).send_event_to(
+ make_fried_fish_step.where_input_event_is(FriedFishProcess.ProcessEvents.PrepareFriedFish.value)
+ )
+
+ make_fried_fish_step.on_event(FriedFishProcess.ProcessEvents.FriedFishReady.value).send_event_to(
+ ProcessFunctionTargetBuilder(add_buns_step)
+ )
+
+ add_buns_step.on_event(AddBunStep.OutputEvents.BunsAdded.value).send_event_to(
+ ProcessFunctionTargetBuilder(add_special_sauce_step)
+ )
+
+ add_special_sauce_step.on_event(AddSpecialSauceStep.OutputEvents.SpecialSauceAdded.value).send_event_to(
+ ProcessFunctionTargetBuilder(external_step)
+ )
+
+ return process_builder
diff --git a/python/samples/getting_started_with_processes/step03/processes/fried_fish_process.py b/python/samples/getting_started_with_processes/step03/processes/fried_fish_process.py
new file mode 100644
index 000000000000..9128cd102b89
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/processes/fried_fish_process.py
@@ -0,0 +1,93 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from enum import Enum
+
+from samples.getting_started_with_processes.step03.models import FoodIngredients
+from samples.getting_started_with_processes.step03.steps import CutFoodStep, FryFoodStep, GatherIngredientsStep
+from samples.getting_started_with_processes.step03.steps.cut_food_with_sharpening_step import CutFoodWithSharpeningStep
+from samples.getting_started_with_processes.step03.steps.gather_ingredients_step import GatherIngredientsWithStockStep
+from semantic_kernel.processes.process_builder import ProcessBuilder
+from semantic_kernel.processes.process_function_target_builder import ProcessFunctionTargetBuilder
+
+
+class GatherFriedFishIngredientsStep(GatherIngredientsStep):
+ def __init__(self):
+ super().__init__(FoodIngredients.FISH)
+
+
+class GatherFriedFishIngredientsWithStockStep(GatherIngredientsWithStockStep):
+ def __init__(self):
+ super().__init__(FoodIngredients.FISH)
+
+
+class FriedFishProcess:
+ class ProcessEvents(Enum):
+ PrepareFriedFish = "PrepareFriedFish"
+ FriedFishReady = FryFoodStep.OutputEvents.FriedFoodReady.value
+
+ @staticmethod
+ def create_process(process_name: str = "FriedFishProcess") -> ProcessBuilder:
+ process_builder = ProcessBuilder(process_name)
+ gatherIngredientsStep = process_builder.add_step(GatherFriedFishIngredientsStep)
+ chopStep = process_builder.add_step(CutFoodStep, name="chopStep")
+ fryStep = process_builder.add_step(FryFoodStep)
+
+ process_builder.on_input_event(FriedFishProcess.ProcessEvents.PrepareFriedFish.value).send_event_to(
+ gatherIngredientsStep
+ )
+
+ gatherIngredientsStep.on_event(
+ GatherFriedFishIngredientsStep.OutputEvents.IngredientsGathered.value
+ ).send_event_to(ProcessFunctionTargetBuilder(chopStep, function_name=CutFoodStep.Functions.ChopFood.value))
+
+ chopStep.on_event(CutFoodStep.OutputEvents.ChoppingReady.value).send_event_to(
+ ProcessFunctionTargetBuilder(fryStep)
+ )
+
+ fryStep.on_event(FryFoodStep.OutputEvents.FoodRuined.value).send_event_to(
+ ProcessFunctionTargetBuilder(gatherIngredientsStep)
+ )
+
+ return process_builder
+
+ @staticmethod
+ def create_process_with_stateful_steps(process_name: str = "FriedFishWithStatefulStepsProcess") -> ProcessBuilder:
+ process_builder = ProcessBuilder(process_name)
+
+ gather_ingredients_step = process_builder.add_step(GatherFriedFishIngredientsWithStockStep)
+ chop_step = process_builder.add_step(CutFoodWithSharpeningStep, name="chopStep")
+ fry_step = process_builder.add_step(FryFoodStep)
+
+ process_builder.on_input_event(FriedFishProcess.ProcessEvents.PrepareFriedFish.value).send_event_to(
+ gather_ingredients_step
+ )
+
+ gather_ingredients_step.on_event(
+ GatherFriedFishIngredientsWithStockStep.OutputEvents.IngredientsGathered.value
+ ).send_event_to(
+ ProcessFunctionTargetBuilder(chop_step, function_name=CutFoodWithSharpeningStep.Functions.ChopFood.value)
+ )
+
+ gather_ingredients_step.on_event(
+ GatherFriedFishIngredientsWithStockStep.OutputEvents.IngredientsOutOfStock.value
+ ).stop_process()
+
+ chop_step.on_event(CutFoodWithSharpeningStep.OutputEvents.ChoppingReady.value).send_event_to(
+ ProcessFunctionTargetBuilder(fry_step)
+ )
+
+ chop_step.on_event(CutFoodWithSharpeningStep.OutputEvents.KnifeNeedsSharpening.value).send_event_to(
+ ProcessFunctionTargetBuilder(
+ chop_step, function_name=CutFoodWithSharpeningStep.Functions.SharpenKnife.value
+ )
+ )
+
+ chop_step.on_event(CutFoodWithSharpeningStep.OutputEvents.KnifeSharpened.value).send_event_to(
+ ProcessFunctionTargetBuilder(chop_step, function_name=CutFoodWithSharpeningStep.Functions.ChopFood.value)
+ )
+
+ fry_step.on_event(FryFoodStep.OutputEvents.FoodRuined.value).send_event_to(
+ ProcessFunctionTargetBuilder(gather_ingredients_step)
+ )
+
+ return process_builder
diff --git a/python/samples/getting_started_with_processes/step03/processes/potato_fries_process.py b/python/samples/getting_started_with_processes/step03/processes/potato_fries_process.py
new file mode 100644
index 000000000000..f34bd275ac1c
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/processes/potato_fries_process.py
@@ -0,0 +1,96 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from enum import Enum
+
+from samples.getting_started_with_processes.step03.models.food_ingredients import FoodIngredients
+from samples.getting_started_with_processes.step03.steps.cut_food_step import CutFoodStep
+from samples.getting_started_with_processes.step03.steps.cut_food_with_sharpening_step import CutFoodWithSharpeningStep
+from samples.getting_started_with_processes.step03.steps.fry_food_step import FryFoodStep
+from samples.getting_started_with_processes.step03.steps.gather_ingredients_step import (
+ GatherIngredientsStep,
+ GatherIngredientsWithStockStep,
+)
+from semantic_kernel.processes.process_builder import ProcessBuilder
+from semantic_kernel.processes.process_function_target_builder import ProcessFunctionTargetBuilder
+
+
+class GatherPotatoFriesIngredientsStep(GatherIngredientsStep):
+ def __init__(self):
+ super().__init__(FoodIngredients.POTATOES)
+
+
+class GatherPotatoFriesIngredientsWithStockStep(GatherIngredientsWithStockStep):
+ def __init__(self):
+ super().__init__(FoodIngredients.POTATOES)
+
+
+class PotatoFriesProcess:
+ class ProcessEvents(Enum):
+ PreparePotatoFries = "PreparePotatoFries"
+ PotatoFriesReady = FryFoodStep.OutputEvents.FriedFoodReady.value
+
+ @staticmethod
+ def create_process(process_name: str = "PotatoFriesProcess"):
+ process_builder = ProcessBuilder(process_name)
+ gather_ingredients_step = process_builder.add_step(GatherPotatoFriesIngredientsStep)
+ slice_step = process_builder.add_step(CutFoodStep, name="sliceStep")
+ fry_step = process_builder.add_step(FryFoodStep)
+
+ process_builder.on_input_event(PotatoFriesProcess.ProcessEvents.PreparePotatoFries.value).send_event_to(
+ gather_ingredients_step
+ )
+
+ gather_ingredients_step.on_event(
+ GatherPotatoFriesIngredientsStep.OutputEvents.IngredientsGathered.value
+ ).send_event_to(ProcessFunctionTargetBuilder(slice_step, function_name=CutFoodStep.Functions.SliceFood.value))
+
+ slice_step.on_event(CutFoodStep.OutputEvents.SlicingReady.value).send_event_to(
+ ProcessFunctionTargetBuilder(fry_step)
+ )
+
+ fry_step.on_event(FryFoodStep.OutputEvents.FoodRuined.value).send_event_to(
+ ProcessFunctionTargetBuilder(gather_ingredients_step)
+ )
+
+ return process_builder
+
+ @staticmethod
+ def create_process_with_stateful_steps(process_name: str = "PotatoFriesWithStatefulStepsProcess"):
+ process_builder = ProcessBuilder(process_name)
+ gather_ingredients_step = process_builder.add_step(GatherPotatoFriesIngredientsWithStockStep)
+ slice_step = process_builder.add_step(CutFoodWithSharpeningStep, name="sliceStep")
+ fry_step = process_builder.add_step(FryFoodStep)
+
+ process_builder.on_input_event(PotatoFriesProcess.ProcessEvents.PreparePotatoFries.value).send_event_to(
+ gather_ingredients_step
+ )
+
+ gather_ingredients_step.on_event(
+ GatherPotatoFriesIngredientsWithStockStep.OutputEvents.IngredientsGathered.value
+ ).send_event_to(
+ ProcessFunctionTargetBuilder(slice_step, function_name=CutFoodWithSharpeningStep.Functions.SliceFood.value)
+ )
+
+ gather_ingredients_step.on_event(
+ GatherPotatoFriesIngredientsWithStockStep.OutputEvents.IngredientsOutOfStock.value
+ ).stop_process()
+
+ slice_step.on_event(CutFoodWithSharpeningStep.OutputEvents.SlicingReady.value).send_event_to(
+ ProcessFunctionTargetBuilder(fry_step)
+ )
+
+ slice_step.on_event(CutFoodWithSharpeningStep.OutputEvents.KnifeNeedsSharpening.value).send_event_to(
+ ProcessFunctionTargetBuilder(
+ slice_step, function_name=CutFoodWithSharpeningStep.Functions.SharpenKnife.value
+ )
+ )
+
+ slice_step.on_event(CutFoodWithSharpeningStep.OutputEvents.KnifeSharpened.value).send_event_to(
+ ProcessFunctionTargetBuilder(slice_step, function_name=CutFoodWithSharpeningStep.Functions.SliceFood.value)
+ )
+
+ fry_step.on_event(FryFoodStep.OutputEvents.FoodRuined.value).send_event_to(
+ ProcessFunctionTargetBuilder(gather_ingredients_step)
+ )
+
+ return process_builder
diff --git a/python/samples/getting_started_with_processes/step03/processes/single_food_item_process.py b/python/samples/getting_started_with_processes/step03/processes/single_food_item_process.py
new file mode 100644
index 000000000000..686fd1a10a13
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/processes/single_food_item_process.py
@@ -0,0 +1,130 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from enum import Enum
+
+from samples.getting_started_with_processes.step03.models.food_order_item import FoodItem
+from samples.getting_started_with_processes.step03.processes.fish_and_chips_process import FishAndChipsProcess
+from samples.getting_started_with_processes.step03.processes.fish_sandwich_process import FishSandwichProcess
+from samples.getting_started_with_processes.step03.processes.fried_fish_process import FriedFishProcess
+from samples.getting_started_with_processes.step03.processes.potato_fries_process import PotatoFriesProcess
+from samples.getting_started_with_processes.step03.steps.external_step import ExternalStep
+from semantic_kernel.functions.kernel_function_decorator import kernel_function
+from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
+from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
+from semantic_kernel.processes.process_builder import ProcessBuilder
+from semantic_kernel.processes.process_function_target_builder import ProcessFunctionTargetBuilder
+
+
+class PackOrderStep(KernelProcessStep):
+ class Functions(Enum):
+ PackFood = "PackFood"
+
+ class OutputEvents(Enum):
+ FoodPacked = "FoodPacked"
+
+ @kernel_function(name=Functions.PackFood.value)
+ async def pack_food(self, context: KernelProcessStepContext, food_actions: list[str]):
+ print(f"PACKING_FOOD: Food {food_actions[0]} Packed! - {food_actions}")
+ await context.emit_event(process_event=PackOrderStep.OutputEvents.FoodPacked.value)
+
+
+class ExternalSingleOrderStep(ExternalStep):
+ def __init__(self):
+ super().__init__(SingleFoodItemProcess.ProcessEvents.SingleOrderReady.value)
+
+
+class DispatchSingleOrderStep(KernelProcessStep):
+ class Functions(Enum):
+ PrepareSingleOrder = "PrepareSingleOrder"
+
+ class OutputEvents(Enum):
+ PrepareFries = "PrepareFries"
+ PrepareFriedFish = "PrepareFriedFish"
+ PrepareFishSandwich = "PrepareFishSandwich"
+ PrepareFishAndChips = "PrepareFishAndChips"
+
+ @kernel_function(name=Functions.PrepareSingleOrder.value)
+ async def dispatch_single_order(self, context: KernelProcessStepContext, food_item: FoodItem):
+ food_name = food_item.to_friendly_string()
+ print(f"DISPATCH_SINGLE_ORDER: Dispatching '{food_name}'!")
+ food_actions = []
+
+ if food_item == FoodItem.POTATO_FRIES:
+ await context.emit_event(
+ process_event=DispatchSingleOrderStep.OutputEvents.PrepareFries.value, data=food_actions
+ )
+
+ elif food_item == FoodItem.FRIED_FISH:
+ await context.emit_event(
+ process_event=DispatchSingleOrderStep.OutputEvents.PrepareFriedFish.value, data=food_actions
+ )
+
+ elif food_item == FoodItem.FISH_SANDWICH:
+ await context.emit_event(
+ process_event=DispatchSingleOrderStep.OutputEvents.PrepareFishSandwich.value, data=food_actions
+ )
+
+ elif food_item == FoodItem.FISH_AND_CHIPS:
+ await context.emit_event(
+ process_event=DispatchSingleOrderStep.OutputEvents.PrepareFishAndChips.value, data=food_actions
+ )
+
+
+class SingleFoodItemProcess:
+ class ProcessEvents(Enum):
+ SingleOrderReceived = "SingleOrderReceived"
+ SingleOrderReady = "SingleOrderReady"
+
+ @staticmethod
+ def create_process(process_name: str = "SingleFoodItemProcess"):
+ process_builder = ProcessBuilder(process_name)
+
+ dispatch_order_step = process_builder.add_step(DispatchSingleOrderStep)
+ make_fried_fish_step = process_builder.add_step_from_process(FriedFishProcess.create_process())
+ make_potato_fries_step = process_builder.add_step_from_process(PotatoFriesProcess.create_process())
+ make_fish_sandwich_step = process_builder.add_step_from_process(FishSandwichProcess.create_process())
+ make_fish_and_chips_step = process_builder.add_step_from_process(FishAndChipsProcess.create_process())
+ pack_order_step = process_builder.add_step(PackOrderStep)
+ external_step = process_builder.add_step(ExternalSingleOrderStep)
+
+ process_builder.on_input_event(SingleFoodItemProcess.ProcessEvents.SingleOrderReceived).send_event_to(
+ ProcessFunctionTargetBuilder(dispatch_order_step)
+ )
+
+ dispatch_order_step.on_event(DispatchSingleOrderStep.OutputEvents.PrepareFriedFish.value).send_event_to(
+ make_fried_fish_step.where_input_event_is(FriedFishProcess.ProcessEvents.PrepareFriedFish.value)
+ )
+
+ dispatch_order_step.on_event(DispatchSingleOrderStep.OutputEvents.PrepareFries.value).send_event_to(
+ make_potato_fries_step.where_input_event_is(PotatoFriesProcess.ProcessEvents.PreparePotatoFries.value)
+ )
+
+ dispatch_order_step.on_event(DispatchSingleOrderStep.OutputEvents.PrepareFishSandwich.value).send_event_to(
+ make_fish_sandwich_step.where_input_event_is(FishSandwichProcess.ProcessEvents.PrepareFishSandwich.value)
+ )
+
+ dispatch_order_step.on_event(DispatchSingleOrderStep.OutputEvents.PrepareFishAndChips.value).send_event_to(
+ make_fish_and_chips_step.where_input_event_is(FishAndChipsProcess.ProcessEvents.PrepareFishAndChips.value)
+ )
+
+ make_fried_fish_step.on_event(FriedFishProcess.ProcessEvents.FriedFishReady.value).send_event_to(
+ ProcessFunctionTargetBuilder(pack_order_step)
+ )
+
+ make_potato_fries_step.on_event(PotatoFriesProcess.ProcessEvents.PotatoFriesReady.value).send_event_to(
+ ProcessFunctionTargetBuilder(pack_order_step)
+ )
+
+ make_fish_sandwich_step.on_event(FishSandwichProcess.ProcessEvents.FishSandwichReady.value).send_event_to(
+ ProcessFunctionTargetBuilder(pack_order_step)
+ )
+
+ make_fish_and_chips_step.on_event(FishAndChipsProcess.ProcessEvents.FishAndChipsReady.value).send_event_to(
+ ProcessFunctionTargetBuilder(pack_order_step)
+ )
+
+ pack_order_step.on_event(PackOrderStep.OutputEvents.FoodPacked.value).send_event_to(
+ ProcessFunctionTargetBuilder(external_step)
+ )
+
+ return process_builder
diff --git a/python/samples/getting_started_with_processes/step03/step03a_food_preparation.py b/python/samples/getting_started_with_processes/step03/step03a_food_preparation.py
new file mode 100644
index 000000000000..8be3efa5b893
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/step03a_food_preparation.py
@@ -0,0 +1,126 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import asyncio
+
+from samples.getting_started_with_processes.step03.processes.fish_and_chips_process import FishAndChipsProcess
+from samples.getting_started_with_processes.step03.processes.fish_sandwich_process import FishSandwichProcess
+from samples.getting_started_with_processes.step03.processes.fried_fish_process import FriedFishProcess
+from samples.getting_started_with_processes.step03.processes.potato_fries_process import PotatoFriesProcess
+from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
+from semantic_kernel.kernel import Kernel
+from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
+from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
+from semantic_kernel.processes.local_runtime.local_kernel_process import start
+from semantic_kernel.processes.process_builder import ProcessBuilder
+
+################################################################################################
+# Demonstrate the creation of 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/python/samples/ #
+# getting_started_with_processes#step03a_food_preparation #
+################################################################################################
+
+# region Helper Methods
+
+
+def _create_kernel_with_chat_completion(service_id: str) -> Kernel:
+ kernel = Kernel()
+ kernel.add_service(OpenAIChatCompletion(service_id=service_id), overwrite=True)
+ return kernel
+
+
+async def use_prepare_specific_product(process: ProcessBuilder, external_trigger_event):
+ kernel = _create_kernel_with_chat_completion("sample")
+ kernel_process = process.build()
+ print(f"=== Start SK Process '{process.name}' ===")
+ _ = await start(
+ process=kernel_process, kernel=kernel, initial_event=KernelProcessEvent(id=external_trigger_event, data=[])
+ )
+ print(f"=== End SK Process '{process.name}' ===")
+
+
+async def execute_process_with_state(process, kernel, external_trigger_event, order_label) -> KernelProcess:
+ print(f"=== {order_label} ===")
+ async with await start(process, kernel, KernelProcessEvent(id=external_trigger_event, data=[])) as running_process:
+ return await running_process.get_state()
+
+
+# endregion
+
+# region Stateless Processes
+
+
+async def use_prepare_fried_fish_process():
+ process = FriedFishProcess.create_process()
+ await use_prepare_specific_product(process, FriedFishProcess.ProcessEvents.PrepareFriedFish)
+
+
+async def use_prepare_potato_fries_process():
+ process = PotatoFriesProcess.create_process()
+ await use_prepare_specific_product(process, PotatoFriesProcess.ProcessEvents.PreparePotatoFries)
+
+
+async def use_prepare_fish_sandwich_process():
+ process = FishSandwichProcess.create_process()
+ await use_prepare_specific_product(process, FishSandwichProcess.ProcessEvents.PrepareFishSandwich)
+
+
+async def use_prepare_fish_and_chips_process():
+ process = FishAndChipsProcess.create_process()
+ await use_prepare_specific_product(process, FishAndChipsProcess.ProcessEvents.PrepareFishAndChips)
+
+
+# endregion
+
+# region Stateful Processes
+
+
+async def use_prepare_stateful_fried_fish_process_no_shared_state():
+ process_builder = FriedFishProcess.create_process_with_stateful_steps()
+ external_trigger_event = FriedFishProcess.ProcessEvents.PrepareFriedFish
+
+ kernel = _create_kernel_with_chat_completion("sample")
+
+ print(f"=== Start SK Process '{process_builder.name}' ===")
+ await execute_process_with_state(process_builder.build(), kernel, external_trigger_event, "Order 1")
+ await execute_process_with_state(process_builder.build(), kernel, external_trigger_event, "Order 2")
+ print(f"=== End SK Process '{process_builder.name}' ===")
+
+
+async def use_prepare_stateful_fried_fish_process_shared_state():
+ process_builder = FriedFishProcess.create_process_with_stateful_steps()
+ external_trigger_event = FriedFishProcess.ProcessEvents.PrepareFriedFish
+
+ kernel = _create_kernel_with_chat_completion("sample")
+
+ print(f"=== Start SK Process '{process_builder.name}' ===")
+ await execute_process_with_state(process_builder.build(), kernel, external_trigger_event, "Order 1")
+ await execute_process_with_state(process_builder.build(), kernel, external_trigger_event, "Order 2")
+ await execute_process_with_state(process_builder.build(), kernel, external_trigger_event, "Order 3")
+ print(f"=== End SK Process '{process_builder.name}' ===")
+
+
+async def use_prepare_stateful_potato_fries_process_shared_state():
+ process_builder = PotatoFriesProcess.create_process_with_stateful_steps()
+ external_trigger_event = PotatoFriesProcess.ProcessEvents.PreparePotatoFries
+
+ kernel = _create_kernel_with_chat_completion("sample")
+
+ print(f"=== Start SK Process '{process_builder.name}' ===")
+ await execute_process_with_state(process_builder.build(), kernel, external_trigger_event, "Order 1")
+ await execute_process_with_state(process_builder.build(), kernel, external_trigger_event, "Order 2")
+ await execute_process_with_state(process_builder.build(), kernel, external_trigger_event, "Order 3")
+ print(f"=== End SK Process '{process_builder.name}' ===")
+
+
+# endregion
+
+
+if __name__ == "__main__":
+ asyncio.run(use_prepare_fried_fish_process())
+ asyncio.run(use_prepare_potato_fries_process())
+ asyncio.run(use_prepare_fish_sandwich_process())
+ asyncio.run(use_prepare_fish_and_chips_process())
+ asyncio.run(use_prepare_stateful_fried_fish_process_no_shared_state())
+ asyncio.run(use_prepare_stateful_fried_fish_process_shared_state())
+ asyncio.run(use_prepare_stateful_potato_fries_process_shared_state())
diff --git a/python/samples/getting_started_with_processes/step03/step03b_food_ordering.py b/python/samples/getting_started_with_processes/step03/step03b_food_ordering.py
new file mode 100644
index 000000000000..297d18081927
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/step03b_food_ordering.py
@@ -0,0 +1,69 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+
+import asyncio
+
+from samples.getting_started_with_processes.step03.models.food_order_item import FoodItem
+from samples.getting_started_with_processes.step03.processes.single_food_item_process import SingleFoodItemProcess
+from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
+from semantic_kernel.kernel import Kernel
+from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
+from semantic_kernel.processes.local_runtime.local_kernel_process import start
+
+################################################################################################
+# Demonstrate the creation of 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/python/samples/ #
+# getting_started_with_processes#step03b_food_ordering #
+################################################################################################
+
+
+def _create_kernel_with_chat_completion(service_id: str) -> Kernel:
+ kernel = Kernel()
+ kernel.add_service(OpenAIChatCompletion(service_id=service_id), overwrite=True)
+ return kernel
+
+
+async def use_prepare_food_order_process_single_item(food_item: FoodItem):
+ kernel = _create_kernel_with_chat_completion("sample")
+ kernel_process = SingleFoodItemProcess.create_process().build()
+ async with await start(
+ kernel_process,
+ kernel,
+ KernelProcessEvent(id=SingleFoodItemProcess.ProcessEvents.SingleOrderReceived, data=food_item),
+ ) as running_process:
+ return running_process
+
+
+async def use_single_order_fried_fish():
+ await use_prepare_food_order_process_single_item(FoodItem.FRIED_FISH)
+
+
+async def use_single_order_potato_fries():
+ await use_prepare_food_order_process_single_item(FoodItem.POTATO_FRIES)
+
+
+async def use_single_order_fish_sandwich():
+ await use_prepare_food_order_process_single_item(FoodItem.FISH_SANDWICH)
+
+
+async def use_single_order_fish_and_chips():
+ await use_prepare_food_order_process_single_item(FoodItem.FISH_AND_CHIPS)
+
+
+async def main():
+ order_methods = [
+ (use_single_order_fried_fish, "use_single_order_fried_fish"),
+ (use_single_order_potato_fries, "use_single_order_potato_fries"),
+ (use_single_order_fish_sandwich, "use_single_order_fish_sandwich"),
+ (use_single_order_fish_and_chips, "use_single_order_fish_and_chips"),
+ ]
+
+ for method, name in order_methods:
+ print(f"=== Start '{name}' ===")
+ await method()
+ print(f"=== End '{name}' ===\n")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/python/samples/getting_started_with_processes/step03/steps/__init__.py b/python/samples/getting_started_with_processes/step03/steps/__init__.py
new file mode 100644
index 000000000000..db7968333f46
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/steps/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from samples.getting_started_with_processes.step03.steps.cut_food_step import CutFoodStep
+from samples.getting_started_with_processes.step03.steps.cut_food_with_sharpening_step import CutFoodWithSharpeningStep
+from samples.getting_started_with_processes.step03.steps.external_step import ExternalStep
+from samples.getting_started_with_processes.step03.steps.fry_food_step import FryFoodStep
+from samples.getting_started_with_processes.step03.steps.gather_ingredients_step import (
+ GatherIngredientsStep,
+ GatherIngredientsWithStockStep,
+)
+
+__all__ = [
+ "ExternalStep",
+ "CutFoodStep",
+ "GatherIngredientsStep",
+ "GatherIngredientsWithStockStep",
+ "CutFoodWithSharpeningStep",
+ "FryFoodStep",
+]
diff --git a/python/samples/getting_started_with_processes/step03/steps/cut_food_step.py b/python/samples/getting_started_with_processes/step03/steps/cut_food_step.py
new file mode 100644
index 000000000000..054f26c5e49a
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/steps/cut_food_step.py
@@ -0,0 +1,34 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from enum import Enum
+
+from semantic_kernel.functions.kernel_function_decorator import kernel_function
+from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
+from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
+
+
+class CutFoodStep(KernelProcessStep):
+ class Functions(Enum):
+ ChopFood = "ChopFood"
+ SliceFood = "SliceFood"
+
+ class OutputEvents(Enum):
+ ChoppingReady = "ChoppingReady"
+ SlicingReady = "SlicingReady"
+
+ def get_action_string(self, food: str, action: str) -> str:
+ return f"{food}_{action}"
+
+ @kernel_function(name=Functions.ChopFood.value)
+ async def chop_food(self, context: KernelProcessStepContext, food_actions: list[str]):
+ food_to_be_cut = food_actions[0]
+ food_actions.append(self.get_action_string(food_to_be_cut, "chopped"))
+ print(f"CUTTING_STEP: Ingredient {food_to_be_cut} has been chopped!")
+ await context.emit_event(process_event=CutFoodStep.OutputEvents.ChoppingReady.value, data=food_actions)
+
+ @kernel_function(name=Functions.SliceFood.value)
+ async def slice_food(self, context: KernelProcessStepContext, food_actions: list[str]):
+ food_to_be_cut = food_actions[0]
+ food_actions.append(self.get_action_string(food_to_be_cut, "sliced"))
+ print(f"CUTTING_STEP: Ingredient {food_to_be_cut} has been sliced!")
+ await context.emit_event(process_event=CutFoodStep.OutputEvents.SlicingReady.value, data=food_actions)
diff --git a/python/samples/getting_started_with_processes/step03/steps/cut_food_with_sharpening_step.py b/python/samples/getting_started_with_processes/step03/steps/cut_food_with_sharpening_step.py
new file mode 100644
index 000000000000..f7ffd3367ccf
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/steps/cut_food_with_sharpening_step.py
@@ -0,0 +1,89 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from enum import Enum
+
+from semantic_kernel.functions.kernel_function_decorator import kernel_function
+from semantic_kernel.kernel_pydantic import KernelBaseModel
+from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
+from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
+from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
+
+
+class CutFoodWithSharpeningState(KernelBaseModel):
+ knife_sharpness: int = 5
+ needs_sharpening_limit: int = 3
+ sharpening_boost: int = 5
+
+
+class CutFoodWithSharpeningStep(KernelProcessStep[CutFoodWithSharpeningState]):
+ class Functions(Enum):
+ ChopFood = "ChopFood"
+ SliceFood = "SliceFood"
+ SharpenKnife = "SharpenKnife"
+
+ class OutputEvents(Enum):
+ ChoppingReady = "ChoppingReady"
+ SlicingReady = "SlicingReady"
+ KnifeNeedsSharpening = "KnifeNeedsSharpening"
+ KnifeSharpened = "KnifeSharpened"
+
+ state: CutFoodWithSharpeningState | None = None
+
+ async def activate(self, state: KernelProcessStepState[CutFoodWithSharpeningState]) -> None:
+ self.state = state.state
+
+ def knife_needs_sharpening(self) -> bool:
+ return self.state.knife_sharpness == self.state.needs_sharpening_limit
+
+ def get_action_string(self, food: str, action: str) -> str:
+ return f"{food}_{action}"
+
+ @kernel_function(name=Functions.ChopFood.value)
+ async def chop_food(self, context: KernelProcessStepContext, food_actions: list[str]):
+ food_to_be_cut = food_actions[0]
+ if self.knife_needs_sharpening():
+ print(f"CUTTING_STEP: Dull knife, cannot chop {food_to_be_cut} - needs sharpening.")
+ await context.emit_event(
+ process_event=CutFoodWithSharpeningStep.OutputEvents.KnifeNeedsSharpening.value, data=food_actions
+ )
+
+ return
+ # Update knife sharpness
+ self.state.knife_sharpness -= 1
+
+ food_actions.append(self.get_action_string(food_to_be_cut, "chopped"))
+ print(
+ f"CUTTING_STEP: Ingredient {food_to_be_cut} has been chopped! - knife sharpness: {self.state.knife_sharpness}" # noqa: E501
+ )
+ await context.emit_event(
+ process_event=CutFoodWithSharpeningStep.OutputEvents.ChoppingReady.value, data=food_actions
+ )
+
+ @kernel_function(name=Functions.SliceFood.value)
+ async def slice_food(self, context: KernelProcessStepContext, food_actions: list[str]):
+ food_to_be_cut = food_actions[0]
+ if self.knife_needs_sharpening():
+ print(f"CUTTING_STEP: Dull knife, cannot slice {food_to_be_cut} - needs sharpening.")
+ await context.emit_event(
+ process_event=CutFoodWithSharpeningStep.OutputEvents.KnifeNeedsSharpening.value, data=food_actions
+ )
+
+ return
+ # Update knife sharpness
+ self.state.knife_sharpness -= 1
+
+ food_actions.append(self.get_action_string(food_to_be_cut, "sliced"))
+ print(
+ f"CUTTING_STEP: Ingredient {food_to_be_cut} has been sliced! - knife sharpness: {self.state.knife_sharpness}" # noqa: E501
+ )
+ await context.emit_event(
+ process_event=CutFoodWithSharpeningStep.OutputEvents.SlicingReady.value, data=food_actions
+ )
+
+ @kernel_function(name=Functions.SharpenKnife.value)
+ async def sharpen_knife(self, context: KernelProcessStepContext, food_actions: list[str]):
+ self.state.knife_sharpness += self.state.sharpening_boost
+ print(f"KNIFE SHARPENED: Knife sharpness is now {self.state.knife_sharpness}!")
+ await context.emit_event(
+ process_event=CutFoodWithSharpeningStep.OutputEvents.KnifeSharpened.value, data=food_actions
+ )
diff --git a/python/samples/getting_started_with_processes/step03/steps/external_step.py b/python/samples/getting_started_with_processes/step03/steps/external_step.py
new file mode 100644
index 000000000000..13a434221701
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/steps/external_step.py
@@ -0,0 +1,21 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from typing import Any
+
+from semantic_kernel.functions.kernel_function_decorator import kernel_function
+from semantic_kernel.processes.kernel_process.kernel_process_event import (
+ KernelProcessEventVisibility,
+)
+from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
+from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
+
+
+class ExternalStep(KernelProcessStep):
+ def __init__(self, external_event_name: str):
+ super().__init__(external_event_name=external_event_name)
+
+ @kernel_function()
+ async def emit_external_event(self, context: KernelProcessStepContext, data: Any):
+ await context.emit_event(
+ process_event=self.external_event_name, data=data, visibility=KernelProcessEventVisibility.Public
+ )
diff --git a/python/samples/getting_started_with_processes/step03/steps/fry_food_step.py b/python/samples/getting_started_with_processes/step03/steps/fry_food_step.py
new file mode 100644
index 000000000000..9752c4ee163f
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/steps/fry_food_step.py
@@ -0,0 +1,44 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from enum import Enum
+from random import Random
+
+from pydantic import Field
+
+from semantic_kernel.functions.kernel_function_decorator import kernel_function
+from semantic_kernel.processes.kernel_process.kernel_process_event import (
+ KernelProcessEventVisibility,
+)
+from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
+from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
+
+
+class FryFoodStep(KernelProcessStep):
+ class Functions(Enum):
+ FryFood = "FryFood"
+
+ class OutputEvents(Enum):
+ FoodRuined = "FoodRuined"
+ FriedFoodReady = "FriedFoodReady"
+
+ random_seed: int = Field(default_factory=Random)
+
+ @kernel_function(name=Functions.FryFood.value)
+ async def fry_food(self, context: KernelProcessStepContext, food_actions: list[str]):
+ food_to_fry = food_actions[0]
+ fryer_malfunction = self.random_seed.randint(0, 10)
+
+ # Oh no! Food got burnt :(
+ if fryer_malfunction < 5:
+ food_actions.append(f"{food_to_fry}_frying_failed")
+ print(f"FRYING_STEP: Ingredient {food_to_fry} got burnt while frying :(")
+ await context.emit_event(process_event=FryFoodStep.OutputEvents.FoodRuined.value, data=food_actions)
+ return
+
+ food_actions.append(f"{food_to_fry}_frying_succeeded")
+ print(f"FRYING_STEP: Ingredient {food_to_fry} is ready!")
+ await context.emit_event(
+ process_event=FryFoodStep.OutputEvents.FriedFoodReady.value,
+ data=food_actions,
+ visibility=KernelProcessEventVisibility.Public,
+ )
diff --git a/python/samples/getting_started_with_processes/step03/steps/gather_ingredients_step.py b/python/samples/getting_started_with_processes/step03/steps/gather_ingredients_step.py
new file mode 100644
index 000000000000..7b2f5807b0bd
--- /dev/null
+++ b/python/samples/getting_started_with_processes/step03/steps/gather_ingredients_step.py
@@ -0,0 +1,85 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from enum import Enum
+
+from samples.getting_started_with_processes.step03.models.food_ingredients import FoodIngredients
+from semantic_kernel.functions.kernel_function_decorator import kernel_function
+from semantic_kernel.kernel_pydantic import KernelBaseModel
+from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
+from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
+from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
+
+
+class GatherIngredientsStep(KernelProcessStep):
+ class Functions(Enum):
+ GatherIngredients = "GatherIngredients"
+
+ class OutputEvents(Enum):
+ IngredientsGathered = "IngredientsGathered"
+ IngredientsOutOfStock = "IngredientsOutOfStock"
+
+ ingredient: FoodIngredients
+
+ def __init__(self, ingredient: FoodIngredients):
+ super().__init__(ingredient=ingredient)
+
+ @kernel_function(name=Functions.GatherIngredients.value)
+ async def gather_ingredients(self, context: KernelProcessStepContext, food_actions: list[str]):
+ ingredient = self.ingredient.to_friendly_string()
+ updated_food_actions = []
+ updated_food_actions.extend(food_actions)
+ if len(updated_food_actions) == 0:
+ updated_food_actions.append(ingredient)
+ updated_food_actions.append(f"{ingredient}_gathered")
+
+ print(f"GATHER_INGREDIENT: Gathered ingredient {ingredient}")
+ await context.emit_event(
+ process_event=GatherIngredientsStep.OutputEvents.IngredientsGathered.value, data=updated_food_actions
+ )
+
+
+class GatherIngredientsState(KernelBaseModel):
+ ingredients_stock: int = 5
+
+
+class GatherIngredientsWithStockStep(KernelProcessStep[GatherIngredientsState]):
+ class Functions(Enum):
+ GatherIngredients = "GatherIngredients"
+
+ class OutputEvents(Enum):
+ IngredientsGathered = "IngredientsGathered"
+ IngredientsOutOfStock = "IngredientsOutOfStock"
+
+ ingredient: FoodIngredients
+ state: GatherIngredientsState | None = None
+
+ def __init__(self, ingredient: FoodIngredients):
+ super().__init__(ingredient=ingredient)
+
+ async def activate(self, state: KernelProcessStepState[GatherIngredientsState]) -> None:
+ self.state = state.state
+
+ @kernel_function(name=Functions.GatherIngredients.value)
+ async def gather_ingredients(self, context: KernelProcessStepContext, food_actions: list[str]):
+ ingredient = self.ingredient.to_friendly_string()
+ updated_food_actions = []
+ updated_food_actions.extend(food_actions)
+
+ if self.state.ingredients_stock == 0:
+ print(f"GATHER_INGREDIENT: Could not gather {ingredient} - OUT OF STOCK!")
+ await context.emit_event(
+ process_event=GatherIngredientsWithStockStep.OutputEvents.IngredientsOutOfStock.value,
+ data=updated_food_actions,
+ )
+ return
+
+ if len(updated_food_actions) == 0:
+ updated_food_actions.append(ingredient)
+ updated_food_actions.append(f"{ingredient}_gathered")
+
+ self.state.ingredients_stock -= 1
+ print(f"GATHER_INGREDIENT: Gathered ingredient {ingredient} - remaining: {self.state.ingredients_stock}")
+ await context.emit_event(
+ process_event=GatherIngredientsWithStockStep.OutputEvents.IngredientsGathered.value,
+ data=updated_food_actions,
+ )
diff --git a/python/semantic_kernel/processes/kernel_process/kernel_process_message_channel.py b/python/semantic_kernel/processes/kernel_process/kernel_process_message_channel.py
index 1fe436e077c2..566a787878ad 100644
--- a/python/semantic_kernel/processes/kernel_process/kernel_process_message_channel.py
+++ b/python/semantic_kernel/processes/kernel_process/kernel_process_message_channel.py
@@ -11,6 +11,6 @@ class KernelProcessMessageChannel(ABC):
"""Abstract base class for emitting events from a step."""
@abstractmethod
- def emit_event(self, process_event: "KernelProcessEvent") -> None:
+ async def emit_event(self, process_event: "KernelProcessEvent") -> None:
"""Emits the specified event from the step."""
pass
diff --git a/python/semantic_kernel/processes/kernel_process/kernel_process_step_context.py b/python/semantic_kernel/processes/kernel_process/kernel_process_step_context.py
index e36feaf495c0..4a9bb4c4208d 100644
--- a/python/semantic_kernel/processes/kernel_process/kernel_process_step_context.py
+++ b/python/semantic_kernel/processes/kernel_process/kernel_process_step_context.py
@@ -18,8 +18,22 @@ def __init__(self, channel: KernelProcessMessageChannel):
"""Initialize the step context."""
super().__init__(step_message_channel=channel)
- def emit_event(self, process_event: "KernelProcessEvent") -> None:
- """Emit an event from the current step."""
+ async def emit_event(self, process_event: "KernelProcessEvent | str", **kwargs) -> None:
+ """Emit an event from the current step.
+
+ It is possible to either specify a `KernelProcessEvent` object or the ID of the event
+ along with the `data` and optional `visibility` keyword arguments.
+
+ Args:
+ process_event (KernelProcessEvent | str): The event to emit.
+ **kwargs: Additional keyword arguments to pass to the event.
+ """
+ from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
+
if process_event is None:
raise ProcessEventUndefinedException("Process event cannot be None")
- self.step_message_channel.emit_event(process_event)
+
+ if not isinstance(process_event, KernelProcessEvent):
+ process_event = KernelProcessEvent(id=process_event, **kwargs)
+
+ await self.step_message_channel.emit_event(process_event)
diff --git a/python/semantic_kernel/processes/local_runtime/local_process.py b/python/semantic_kernel/processes/local_runtime/local_process.py
index a826dae613eb..b3077c9eb7d1 100644
--- a/python/semantic_kernel/processes/local_runtime/local_process.py
+++ b/python/semantic_kernel/processes/local_runtime/local_process.py
@@ -218,9 +218,9 @@ async def enqueue_step_messages(self, step: LocalStep, message_channel: Queue[Lo
for step_event in all_step_events:
if step_event.visibility == KernelProcessEventVisibility.Public:
if isinstance(step_event, KernelProcessEvent):
- self.emit_event(step_event) # type: ignore
+ await self.emit_event(step_event) # type: ignore
elif isinstance(step_event, LocalEvent):
- self.emit_local_event(step_event) # type: ignore
+ await self.emit_local_event(step_event) # type: ignore
for edge in step.get_edge_for_event(step_event.id):
message = LocalMessageFactory.create_from_edge(edge, step_event.data)
diff --git a/python/semantic_kernel/processes/local_runtime/local_step.py b/python/semantic_kernel/processes/local_runtime/local_step.py
index dbea7cef0896..c5af81f324e0 100644
--- a/python/semantic_kernel/processes/local_runtime/local_step.py
+++ b/python/semantic_kernel/processes/local_runtime/local_step.py
@@ -163,7 +163,7 @@ async def handle_message(self, message: LocalMessage):
event_name = f"{target_function}.OnError"
event_value = str(ex)
finally:
- self.emit_event(KernelProcessEvent(id=event_name, data=event_value))
+ await self.emit_event(KernelProcessEvent(id=event_name, data=event_value))
# Reset the inputs for the function that was just executed
self.inputs[target_function] = self.initial_inputs.get(target_function, {}).copy()
@@ -172,11 +172,11 @@ async def invoke_function(self, function: "KernelFunction", kernel: "Kernel", ar
"""Invokes the function."""
return await kernel.invoke(function, **arguments)
- def emit_event(self, process_event: KernelProcessEvent):
+ async def emit_event(self, process_event: KernelProcessEvent):
"""Emits an event from the step."""
- self.emit_local_event(LocalEvent.from_kernel_process_event(process_event, self.event_namespace))
+ await self.emit_local_event(LocalEvent.from_kernel_process_event(process_event, self.event_namespace))
- def emit_local_event(self, local_event: "LocalEvent"):
+ async def emit_local_event(self, local_event: "LocalEvent"):
"""Emits an event from the step."""
scoped_event = self.scoped_event(local_event)
self.outgoing_event_queue.put(scoped_event)
diff --git a/python/tests/unit/processes/kernel_process/test_kernel_process_step_context.py b/python/tests/unit/processes/kernel_process/test_kernel_process_step_context.py
index e20a3feb0ce9..a6dd09b7ef88 100644
--- a/python/tests/unit/processes/kernel_process/test_kernel_process_step_context.py
+++ b/python/tests/unit/processes/kernel_process/test_kernel_process_step_context.py
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
-from unittest.mock import AsyncMock, MagicMock
+from unittest.mock import AsyncMock
import pytest
@@ -29,15 +29,15 @@ async def test_initialization():
@pytest.mark.asyncio
-def test_emit_event():
+async def test_emit_event():
# Arrange
channel = MockKernelProcessMessageChannel()
- channel.emit_event = MagicMock()
+ channel.emit_event = AsyncMock()
context = KernelProcessStepContext(channel=channel)
event = KernelProcessEvent(id="event_001", data={"key": "value"})
# Act
- context.emit_event(event)
+ await context.emit_event(event)
# Assert
channel.emit_event.assert_called_once_with(event)