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)