Python: Add the Dapr Runtime for Processes (#9642)

### Motivation and Context

In addition to the Python local process runtime, we're adding the
ability to congfigure the Dapr runtime for Python SK processes.

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

### Description

The PR introduces:
- The ability to configure the Dapr Runtime for Python processes
- Adds a sample FastAPI app showing how to configure the Dapr runtime
with a sample SK Python process (see the README for details on setup)
- Adds some unit test coverage for the Dapr runtime
- Closes #9355 

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

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

- [X] The code builds clean without any errors or warnings
- [X] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [X] All unit tests pass, and I have added new tests where possible
- [X] I didn't break anyone 😄
This commit is contained in:
Evan Mattson
2024-11-15 14:43:15 +00:00
committed by GitHub
parent 434db27496
commit 29e3e83c2d
66 changed files with 3449 additions and 206 deletions
+20
View File
@@ -10,6 +10,26 @@
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": true
},
{
"name": "Python FastAPI app with Dapr",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/samples/demos/process_with_dapr/fastapi_app.py",
"console": "integratedTerminal",
"preLaunchTask": "daprd-debug-python",
"postDebugTask": "daprd-down-python",
"justMyCode": false
},
{
"name": "Python Flask API app with Dapr",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/samples/demos/process_with_dapr/flask_app.py",
"console": "integratedTerminal",
"preLaunchTask": "daprd-debug-python",
"postDebugTask": "daprd-down-python",
"justMyCode": false
}
]
+14
View File
@@ -164,6 +164,20 @@
"panel": "shared"
},
"problemMatcher": []
},
{
"label": "daprd-debug-python",
"type": "daprd",
"appId": "dapr-processes",
"httpPort": 3500,
"appPort": 5001,
"grpcPort": 53317,
"metricsPort": 9091
},
{
"label": "daprd-down-python",
"type": "daprd-down",
"appId": "dapr-processes"
}
],
"inputs": [
+5
View File
@@ -117,6 +117,11 @@ pandas = [
aws = [
"boto3>=1.28.57",
]
dapr = [
"dapr>=1.14.0",
"dapr-ext-fastapi>=1.14.0",
"flask-dapr>=1.14.0"
]
[tool.uv]
prerelease = "if-necessary-or-explicit"
@@ -42,8 +42,8 @@ class KickOffStep(KernelProcessStep):
@kernel_function(name=KICK_OFF_FUNCTION)
async def print_welcome_message(self, context: KernelProcessStepContext):
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")
await context.emit_event(process_event=CommonEvents.StartARequested, data="Get Going A")
await context.emit_event(process_event=CommonEvents.StartBRequested, 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)
await context.emit_event(process_event=CommonEvents.AStepDone.value, data="I did A")
await context.emit_event(process_event=CommonEvents.AStepDone, 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)
await context.emit_event(process_event=CommonEvents.BStepDone.value, data="I did B")
await context.emit_event(process_event=CommonEvents.BStepDone, data="I did B")
# Define a sample `CStepState` that will keep track of the current cycle.
@@ -84,9 +84,9 @@ class CStep(KernelProcessStep[CStepState]):
print(f"CStep Current Cycle: {self.state.current_cycle}")
if self.state.current_cycle == 3:
print("CStep Exit Requested")
await context.emit_event(process_event=CommonEvents.ExitRequested.value)
await context.emit_event(process_event=CommonEvents.ExitRequested)
return
await context.emit_event(process_event=CommonEvents.CStepDone.value)
await context.emit_event(process_event=CommonEvents.CStepDone)
kernel = Kernel()
@@ -105,17 +105,17 @@ async def cycles_with_fan_in():
myCStep = process.add_step(step_type=CStep)
# Define the input event and where to send it to
process.on_input_event(event_id=CommonEvents.StartProcess.value).send_event_to(target=kickoff_step)
process.on_input_event(event_id=CommonEvents.StartProcess).send_event_to(target=kickoff_step)
# Define the process flow
kickoff_step.on_event(event_id=CommonEvents.StartARequested.value).send_event_to(target=myAStep)
kickoff_step.on_event(event_id=CommonEvents.StartBRequested.value).send_event_to(target=myBStep)
myAStep.on_event(event_id=CommonEvents.AStepDone.value).send_event_to(target=myCStep, parameter_name="astepdata")
kickoff_step.on_event(event_id=CommonEvents.StartARequested).send_event_to(target=myAStep)
kickoff_step.on_event(event_id=CommonEvents.StartBRequested).send_event_to(target=myBStep)
myAStep.on_event(event_id=CommonEvents.AStepDone).send_event_to(target=myCStep, parameter_name="astepdata")
# Define the fan in behavior once both AStep and BStep are done
myBStep.on_event(event_id=CommonEvents.BStepDone.value).send_event_to(target=myCStep, parameter_name="bstepdata")
myCStep.on_event(event_id=CommonEvents.CStepDone.value).send_event_to(target=kickoff_step)
myCStep.on_event(event_id=CommonEvents.ExitRequested.value).stop_process()
myBStep.on_event(event_id=CommonEvents.BStepDone).send_event_to(target=myCStep, parameter_name="bstepdata")
myCStep.on_event(event_id=CommonEvents.CStepDone).send_event_to(target=kickoff_step)
myCStep.on_event(event_id=CommonEvents.ExitRequested).stop_process()
# Build the process
kernel_process = process.build()
@@ -123,7 +123,7 @@ async def cycles_with_fan_in():
async with await start(
process=kernel_process,
kernel=kernel,
initial_event=KernelProcessEvent(id=CommonEvents.StartProcess.value, data="foo"),
initial_event=KernelProcessEvent(id=CommonEvents.StartProcess, data="foo"),
) as process_context:
process_state = await process_context.get_state()
c_step_state: KernelProcessStepState[CStepState] = next(
@@ -58,12 +58,12 @@ class RepeatStep(KernelProcessStep[StepState]):
print(f"[REPEAT] {output}")
await context.emit_event(
process_event=ProcessEvents.OutputReadyPublic.value,
process_event=ProcessEvents.OutputReadyPublic,
data=output,
visibility=KernelProcessEventVisibility.Public,
)
await context.emit_event(
process_event=ProcessEvents.OutputReadyInternal.value,
process_event=ProcessEvents.OutputReadyInternal,
data=output,
visibility=KernelProcessEventVisibility.Internal,
)
@@ -74,7 +74,7 @@ def create_linear_process(name: str):
echo_step = process_builder.add_step(step_type=EchoStep)
repeat_step = process_builder.add_step(step_type=RepeatStep)
process_builder.on_input_event(event_id=ProcessEvents.StartProcess.value).send_event_to(target=echo_step)
process_builder.on_input_event(event_id=ProcessEvents.StartProcess).send_event_to(target=echo_step)
echo_step.on_function_result(function_name=EchoStep.ECHO).send_event_to(
target=repeat_step, parameter_name="message"
@@ -93,8 +93,8 @@ async def nested_process():
nested_process_step = process_builder.add_step_from_process(create_linear_process("Inner"))
process_builder.steps[1].on_event(ProcessEvents.OutputReadyInternal.value).send_event_to(
nested_process_step.where_input_event_is(ProcessEvents.StartProcess.value)
process_builder.steps[1].on_event(ProcessEvents.OutputReadyInternal).send_event_to(
nested_process_step.where_input_event_is(ProcessEvents.StartProcess)
)
process = process_builder.build()
@@ -102,7 +102,7 @@ async def nested_process():
test_input = "Test"
process_handle = await start(
process=process, kernel=kernel, initial_event=ProcessEvents.StartProcess.value, data=test_input
process=process, kernel=kernel, initial_event=ProcessEvents.StartProcess, data=test_input
)
process_info = await process_handle.get_state()
@@ -0,0 +1,180 @@
# Semantic Kernel Processes in Dapr
This demo contains a FastAPI app that uses Dapr to run a Semantic Kernel Process. Dapr is a portable, event-driven runtime that can simplify the process of building resilient, stateful application that run in the cloud and/or edge. Dapr is a natural fit for hosting Semantic Kernel Processes and allows you to scale your processes in size and quantity without sacrificing performance, or reliability.
For more information about Semantic Kernel Processes and Dapr, see the following documentation:
#### Semantic Kernel Processes
- [Overview of the Process Framework (docs)](https://learn.microsoft.com/semantic-kernel/frameworks/process/process-framework)
- [Getting Started with Processes (samples)](../../getting_started_with_processes/)
- [Semantic Kernel Dapr Runtime](../../../semantic_kernel/processes/dapr_runtime/)
#### Dapr
- [Dapr documentation](https://docs.dapr.io/)
- [Dapr Actor documentation](https://v1-10.docs.dapr.io/developing-applications/building-blocks/actors/)
- [Dapr local development](https://docs.dapr.io/getting-started/install-dapr-selfhost/)
### Supported Dapr Extensions:
| Extension | Supported |
|--------------------|:----:|
| FastAPI | ✅ |
| Flask | ✅ |
| gRPC | ❌ |
| Dapr Workflow | ❌ |
## Running the Demo
Before running this Demo, make sure to configure Dapr for local development following the links above. The Dapr containers must be running for this demo application to run.
```mermaid
flowchart LR
Kickoff --> A
Kickoff --> B
A --> C
B --> C
C -->|Count < 3| Kickoff
C -->|Count >= 3| End
classDef kickoffClass fill:#f9f,stroke:#333,stroke-width:2px;
class Kickoff kickoffClass;
End((End))
```
1. Build and run the sample. Running the Dapr service locally can be done using the Dapr Cli or with the Dapr VS Code extension. The VS Code extension is the recommended approach if you want to debug the code as it runs.
- If using VSCode to debug, select either the `Python FastAPI App with Dapr` or the `Python Flask API App with Dapr` option from the Run and Debug dropdown list.
1. When the service is up and running, it will expose a single API in localhost port 5001.
#### Invoking the process:
1. Open a web browser and point it to [http://localhost:5001/processes/1234](http://localhost:5001/processes/1234) to invoke a new process with `Id = "1234"`
1. When the process is complete, you should see `{"processId":"1234"}` in the web browser.
1. You should also see console output from the running service with logs that match the following:
```text
##### Kickoff ran.
##### AStep ran.
##### BStep ran.
##### CStep activated with Cycle = '1'.
##### CStep run cycle 2.
##### Kickoff ran.
##### AStep ran.
##### BStep ran.
##### CStep run cycle 3 - exiting.
```
Now refresh the page in your browser to run the same processes instance again. Now the logs should look like this:
```text
##### Kickoff ran.
##### AStep ran.
##### BStep ran.
##### CStep run cycle 4 - exiting.
```
Notice that the logs from the two runs are not the same. In the first run, the processes has not been run before and so it's initial
state came from what we defined in the process:
**_First Run_**
- `CState` is initialized with `Cycle = 1` which is the initial state that we specified while building the process.
- `CState` is invoked a total of two times before the terminal condition of `Cycle >= 3` is reached.
In the second run however, the process has persisted state from the first run:
**_Second Run_**
- `CState` is initialized with `Cycle = 3` which is the final state from the first run of the process.
- `CState` is invoked only once and is already in the terminal condition of `Cycle >= 3`.
If you create a new instance of the process with `Id = "ABCD"` by pointing your browser to [http://localhost:5001/processes/ABCD](http://localhost:5001/processes/ABCD), you will see the it will start with the initial state as expected.
## Understanding the Code
Below are the key aspects of the code that show how Dapr and Semantic Kernel Processes can be integrated into a FastAPI app:
- Create a new Dapr FastAPI app.
- Add the required Semantic Kernel and Dapr packages to your project:
**_General Imports and Dapr Packages_**
**_FastAPI App_**
```python
import logging
from contextlib import asynccontextmanager
import uvicorn
from dapr.ext.fastapi import DaprActor
from fastapi import FastAPI
from fastapi.responses import JSONResponse
```
**_Flask API App_**
```python
import asyncio
import logging
from flask import Flask, jsonify
from flask_dapr.actor import DaprActor
```
**_Semantic Kernel Process Imports_**
```python
from samples.demos.process_with_dapr.process.process import get_process
from samples.demos.process_with_dapr.process.steps import CommonEvents
from semantic_kernel import Kernel
from semantic_kernel.processes.dapr_runtime import (
register_fastapi_dapr_actors,
start,
)
```
**_Define the FastAPI app, Dapr App, and the DaprActor_**
```python
# Define the kernel that is used throughout the process
kernel = Kernel()
# Define a lifespan method that registers the actors with the Dapr runtime
@asynccontextmanager
async def lifespan(app: FastAPI):
print("## actor startup ##")
await register_fastapi_dapr_actors(actor, kernel)
yield
# Define the FastAPI app along with the DaprActor
app = FastAPI(title="SKProcess", lifespan=lifespan)
actor = DaprActor(app)
```
If using Flask, you will define:
```python
kernel = Kernel()
app = Flask("SKProcess")
# Enable DaprActor Flask extension
actor = DaprActor(app)
# Synchronously register actors
print("## actor startup ##")
register_flask_dapr_actors(actor, kernel)
# Create the global event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
```
- Build and run a Process as you normally would. For this Demo we run a simple example process from with either a FastAPI or a Flask API method in response to a GET request.
- [See the FastAPI app here](./fastapi_app.py).
- [See the Flask API app here](./flask_app.py)
@@ -0,0 +1,73 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from contextlib import asynccontextmanager
import uvicorn
from dapr.ext.fastapi import DaprActor
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from samples.demos.process_with_dapr.process.process import get_process
from samples.demos.process_with_dapr.process.steps import CommonEvents
from semantic_kernel import Kernel
from semantic_kernel.processes.dapr_runtime import (
register_fastapi_dapr_actors,
start,
)
logging.basicConfig(level=logging.ERROR)
# Define the kernel that is used throughout the process
kernel = Kernel()
#########################################################################
# The following Process and Dapr runtime sample uses a FastAPI app #
# to start a process and run steps. The process is defined in the #
# process/process.py file and the steps are defined in the steps.py #
# file. The process is started by calling the /processes/{process_id} #
# endpoint. The actors are registered with the Dapr runtime using #
# the DaprActor class. The ProcessActor and the StepActor require a #
# kernel dependency to be injected during creation. This is done by #
# defining a factory function that takes the kernel as a parameter #
# and returns the actor instance with the kernel injected. #
#########################################################################
# Define a lifespan method that registers the actors with the Dapr runtime
@asynccontextmanager
async def lifespan(app: FastAPI):
print("## actor startup ##")
await register_fastapi_dapr_actors(actor, kernel)
yield
# Define the FastAPI app along with the DaprActor
app = FastAPI(title="SKProcess", lifespan=lifespan)
actor = DaprActor(app)
@app.get("/healthz")
async def healthcheck():
return "Healthy!"
@app.get("/processes/{process_id}")
async def start_process(process_id: str):
try:
process = get_process()
_ = await start(
process=process,
kernel=kernel,
initial_event=CommonEvents.StartProcess,
process_id=process_id,
)
return JSONResponse(content={"processId": process_id}, status_code=200)
except Exception as e:
return JSONResponse(content={"error": str(e)}, status_code=500)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=5001, log_level="error") # nosec
@@ -0,0 +1,65 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from flask import Flask, jsonify
from flask_dapr.actor import DaprActor
from samples.demos.process_with_dapr.process.process import get_process
from samples.demos.process_with_dapr.process.steps import CommonEvents
from semantic_kernel import Kernel
from semantic_kernel.processes.dapr_runtime import (
register_flask_dapr_actors,
start,
)
logging.basicConfig(level=logging.ERROR)
# Define the kernel that is used throughout the process
kernel = Kernel()
app = Flask("SKProcess")
# Enable DaprActor Flask extension
actor = DaprActor(app)
# Synchronously register actors
print("## actor startup ##")
register_flask_dapr_actors(actor, kernel)
# Create the global event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
@app.route("/healthz", methods=["GET"])
def healthcheck():
return "Healthy!", 200
@app.route("/processes/<process_id>", methods=["GET"])
def start_process(process_id):
try:
process = get_process()
# Run the start coroutine in a synchronous manner
asyncio.set_event_loop(loop)
_ = asyncio.run(
start(
process=process,
kernel=kernel,
initial_event=CommonEvents.StartProcess,
process_id=process_id,
)
)
return jsonify({"processId": process_id}), 200
except Exception as e:
logging.exception("Error starting process")
return jsonify({"error": str(e)}), 500
# Run application
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5001) # nosec
@@ -0,0 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
from samples.demos.process_with_dapr.process.process import get_process
from samples.demos.process_with_dapr.process.steps import AStep, BStep, CStep, KickOffStep
__all__ = ["AStep", "BStep", "KickOffStep", "CStep", "get_process"]
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import TYPE_CHECKING
from samples.demos.process_with_dapr.process.steps import AStep, BStep, CommonEvents, CStep, CStepState, KickOffStep
from semantic_kernel.processes import ProcessBuilder
if TYPE_CHECKING:
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
def get_process() -> "KernelProcess":
# Define the process builder
process = ProcessBuilder(name="ProcessWithDapr")
# Add the step types to the builder
kickoff_step = process.add_step(step_type=KickOffStep)
myAStep = process.add_step(step_type=AStep)
myBStep = process.add_step(step_type=BStep)
# Initialize the CStep with an initial state and the state's current cycle set to 1
myCStep = process.add_step(step_type=CStep, initial_state=CStepState(current_cycle=1))
# Define the input event and where to send it to
process.on_input_event(event_id=CommonEvents.StartProcess).send_event_to(target=kickoff_step)
# Define the process flow
kickoff_step.on_event(event_id=CommonEvents.StartARequested).send_event_to(target=myAStep)
kickoff_step.on_event(event_id=CommonEvents.StartBRequested).send_event_to(target=myBStep)
myAStep.on_event(event_id=CommonEvents.AStepDone).send_event_to(target=myCStep, parameter_name="astepdata")
# Define the fan in behavior once both AStep and BStep are done
myBStep.on_event(event_id=CommonEvents.BStepDone).send_event_to(target=myCStep, parameter_name="bstepdata")
myCStep.on_event(event_id=CommonEvents.CStepDone).send_event_to(target=kickoff_step)
myCStep.on_event(event_id=CommonEvents.ExitRequested).stop_process()
# Build the process
return process.build()
@@ -0,0 +1,89 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from enum import Enum
from typing import ClassVar
from pydantic import Field
from semantic_kernel.functions import kernel_function
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.processes.kernel_process import (
KernelProcessStep,
KernelProcessStepContext,
KernelProcessStepState,
)
class CommonEvents(Enum):
"""Common events for the sample process."""
UserInputReceived = "UserInputReceived"
CompletionResponseGenerated = "CompletionResponseGenerated"
WelcomeDone = "WelcomeDone"
AStepDone = "AStepDone"
BStepDone = "BStepDone"
CStepDone = "CStepDone"
StartARequested = "StartARequested"
StartBRequested = "StartBRequested"
ExitRequested = "ExitRequested"
StartProcess = "StartProcess"
# Define a sample step that once the `on_input_event` is received,
# it will emit two events to start the A and B steps.
class KickOffStep(KernelProcessStep):
KICK_OFF_FUNCTION: ClassVar[str] = "kick_off"
@kernel_function(name=KICK_OFF_FUNCTION)
async def print_welcome_message(self, context: KernelProcessStepContext):
print("##### Kickoff ran.")
await context.emit_event(process_event=CommonEvents.StartARequested, data="Get Going A")
await context.emit_event(process_event=CommonEvents.StartBRequested, data="Get Going B")
# Define a sample `AStep` step that will emit an event after 1 second.
# The event will be sent to the `CStep` step with the data `I did A`.
class AStep(KernelProcessStep):
@kernel_function()
async def do_it(self, context: KernelProcessStepContext):
print("##### AStep ran.")
await asyncio.sleep(1)
await context.emit_event(process_event=CommonEvents.AStepDone, data="I did A")
# Define a sample `BStep` step that will emit an event after 2 seconds.
# The event will be sent to the `CStep` step with the data `I did B`.
class BStep(KernelProcessStep):
@kernel_function()
async def do_it(self, context: KernelProcessStepContext):
print("##### BStep ran.")
await asyncio.sleep(2)
await context.emit_event(process_event=CommonEvents.BStepDone, data="I did B")
# Define a sample `CStepState` that will keep track of the current cycle.
class CStepState(KernelBaseModel):
current_cycle: int = 1
# Define a sample `CStep` step that will emit an `ExitRequested` event after 3 cycles.
class CStep(KernelProcessStep[CStepState]):
state: CStepState = Field(default_factory=CStepState)
# The activate method overrides the base class method to set the state in the step.
async def activate(self, state: KernelProcessStepState[CStepState]):
"""Activates the step and sets the state."""
step_state = CStepState.model_validate(state.state)
print(f"##### CStep activated with Cycle = '{step_state.current_cycle}'.")
self.state = step_state
@kernel_function()
async def do_it(self, context: KernelProcessStepContext, astepdata: str, bstepdata: str):
self.state.current_cycle += 1
if self.state.current_cycle >= 3:
print("##### CStep run cycle 3 - exiting.")
await context.emit_event(process_event=CommonEvents.ExitRequested)
return
print(f"##### CStep run cycle {self.state.current_cycle}")
await context.emit_event(process_event=CommonEvents.CStepDone)
@@ -70,13 +70,13 @@ class ScriptedUserInputStep(KernelProcessStep[UserInputState]):
print(f"USER: {user_message}")
if "exit" in user_message:
await context.emit_event(process_event=ChatBotEvents.Exit.value, data=None)
await context.emit_event(process_event=ChatBotEvents.Exit, data=None)
return
self.state.current_input_index += 1
# Emit the user input event
await context.emit_event(process_event=CommonEvents.UserInputReceived.value, data=user_message)
await context.emit_event(process_event=CommonEvents.UserInputReceived, data=user_message)
class ChatUserInputStep(ScriptedUserInputStep):
@@ -140,7 +140,7 @@ class ChatBotResponseStep(KernelProcessStep[ChatBotState]):
self.state.chat_messages.append(answer)
# Emit an event: assistantResponse
await context.emit_event(process_event=ChatBotEvents.AssistantResponseGenerated.value, data=answer)
await context.emit_event(process_event=ChatBotEvents.AssistantResponseGenerated, data=answer)
kernel = Kernel()
@@ -157,7 +157,7 @@ async def step01_processes():
response_step = process.add_step(ChatBotResponseStep)
# Define the input event that starts the process and where to send it
process.on_input_event(event_id=ChatBotEvents.StartProcess.value).send_event_to(target=intro_step)
process.on_input_event(event_id=ChatBotEvents.StartProcess).send_event_to(target=intro_step)
# Define the event that triggers the next step in the process
intro_step.on_function_result(function_name=IntroStep.print_intro_message.__name__).send_event_to(
@@ -165,16 +165,14 @@ async def step01_processes():
)
# Define the event that triggers the process to stop
user_input_step.on_event(event_id=ChatBotEvents.Exit.value).stop_process()
user_input_step.on_event(event_id=ChatBotEvents.Exit).stop_process()
# For the user step, send the user input to the response step
user_input_step.on_event(event_id=CommonEvents.UserInputReceived.value).send_event_to(
user_input_step.on_event(event_id=CommonEvents.UserInputReceived).send_event_to(
target=response_step, parameter_name="user_message"
)
# For the response step, send the response back to the user input step
response_step.on_event(event_id=ChatBotEvents.AssistantResponseGenerated.value).send_event_to(
target=user_input_step
)
response_step.on_event(event_id=ChatBotEvents.AssistantResponseGenerated).send_event_to(target=user_input_step)
# Build the kernel process
kernel_process = process.build()
@@ -183,7 +181,7 @@ async def step01_processes():
await start(
process=kernel_process,
kernel=kernel,
initial_event=KernelProcessEvent(id=ChatBotEvents.StartProcess.value, data=None),
initial_event=KernelProcessEvent(id=ChatBotEvents.StartProcess, data=None),
)
@@ -20,7 +20,7 @@ class AddFishAndChipsCondimentsStep(KernelProcessStep):
class OutputEvents(Enum):
CondimentsAdded = "CondimentsAdded"
@kernel_function(name=Functions.AddCondiments.value)
@kernel_function(name=Functions.AddCondiments)
async def add_condiments(
self, context: KernelProcessStepContext, fish_actions: list[str], potato_actions: list[str]
):
@@ -30,7 +30,7 @@ class AddFishAndChipsCondimentsStep(KernelProcessStep):
fish_actions.extend(potato_actions)
fish_actions.append("Condiments")
await context.emit_event(
process_event=AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded.value, data=fish_actions
process_event=AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded, data=fish_actions
)
@@ -41,7 +41,7 @@ class FishAndChipsProcess:
class ExternalFishAndChipsStep(ExternalStep):
def __init__(self):
super().__init__(FishAndChipsProcess.ProcessEvents.FishAndChipsReady.value)
super().__init__(FishAndChipsProcess.ProcessEvents.FishAndChipsReady)
@staticmethod
def create_process(process_name: str = "FishAndChipsProcess"):
@@ -51,21 +51,21 @@ class FishAndChipsProcess:
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)
process_builder.on_input_event(FishAndChipsProcess.ProcessEvents.PrepareFishAndChips).send_event_to(
make_fried_fish_step.where_input_event_is(FriedFishProcess.ProcessEvents.PrepareFriedFish)
).send_event_to(
make_potato_fries_step.where_input_event_is(PotatoFriesProcess.ProcessEvents.PreparePotatoFries.value)
make_potato_fries_step.where_input_event_is(PotatoFriesProcess.ProcessEvents.PreparePotatoFries)
)
make_fried_fish_step.on_event(FriedFishProcess.ProcessEvents.FriedFishReady.value).send_event_to(
make_fried_fish_step.on_event(FriedFishProcess.ProcessEvents.FriedFishReady).send_event_to(
ProcessFunctionTargetBuilder(add_condiments_step, parameter_name="fishActions")
)
make_potato_fries_step.on_event(PotatoFriesProcess.ProcessEvents.PotatoFriesReady.value).send_event_to(
make_potato_fries_step.on_event(PotatoFriesProcess.ProcessEvents.PotatoFriesReady).send_event_to(
ProcessFunctionTargetBuilder(add_condiments_step, parameter_name="potatoActions")
)
add_condiments_step.on_event(AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded.value).send_event_to(
add_condiments_step.on_event(AddFishAndChipsCondimentsStep.OutputEvents.CondimentsAdded).send_event_to(
ProcessFunctionTargetBuilder(external_step, parameter_name="fishActions")
)
@@ -18,11 +18,11 @@ class AddBunStep(KernelProcessStep):
class OutputEvents(Enum):
BunsAdded = "BunsAdded"
@kernel_function(name=Functions.AddBuns.value)
@kernel_function(name=Functions.AddBuns)
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)
await context.emit_event(process_event=self.OutputEvents.BunsAdded, data=food_actions)
class AddSpecialSauceStep(KernelProcessStep):
@@ -32,16 +32,16 @@ class AddSpecialSauceStep(KernelProcessStep):
class OutputEvents(Enum):
SpecialSauceAdded = "SpecialSauceAdded"
@kernel_function(name=Functions.AddSpecialSauce.value)
@kernel_function(name=Functions.AddSpecialSauce)
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)
await context.emit_event(process_event=self.OutputEvents.SpecialSauceAdded, data=food_actions)
class ExternalFriedFishStep(ExternalStep):
def __init__(self):
super().__init__(FishSandwichProcess.ProcessEvents.FishSandwichReady.value)
super().__init__(FishSandwichProcess.ProcessEvents.FishSandwichReady)
class FishSandwichProcess:
@@ -57,19 +57,19 @@ class FishSandwichProcess:
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)
process_builder.on_input_event(FishSandwichProcess.ProcessEvents.PrepareFishSandwich).send_event_to(
make_fried_fish_step.where_input_event_is(FriedFishProcess.ProcessEvents.PrepareFriedFish)
)
make_fried_fish_step.on_event(FriedFishProcess.ProcessEvents.FriedFishReady.value).send_event_to(
make_fried_fish_step.on_event(FriedFishProcess.ProcessEvents.FriedFishReady).send_event_to(
ProcessFunctionTargetBuilder(add_buns_step)
)
add_buns_step.on_event(AddBunStep.OutputEvents.BunsAdded.value).send_event_to(
add_buns_step.on_event(AddBunStep.OutputEvents.BunsAdded).send_event_to(
ProcessFunctionTargetBuilder(add_special_sauce_step)
)
add_special_sauce_step.on_event(AddSpecialSauceStep.OutputEvents.SpecialSauceAdded.value).send_event_to(
add_special_sauce_step.on_event(AddSpecialSauceStep.OutputEvents.SpecialSauceAdded).send_event_to(
ProcessFunctionTargetBuilder(external_step)
)
@@ -32,19 +32,17 @@ class FriedFishProcess:
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(
process_builder.on_input_event(FriedFishProcess.ProcessEvents.PrepareFriedFish).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)
gatherIngredientsStep.on_event(GatherFriedFishIngredientsStep.OutputEvents.IngredientsGathered).send_event_to(
ProcessFunctionTargetBuilder(chopStep, function_name=CutFoodStep.Functions.ChopFood)
)
fryStep.on_event(FryFoodStep.OutputEvents.FoodRuined.value).send_event_to(
chopStep.on_event(CutFoodStep.OutputEvents.ChoppingReady).send_event_to(ProcessFunctionTargetBuilder(fryStep))
fryStep.on_event(FryFoodStep.OutputEvents.FoodRuined).send_event_to(
ProcessFunctionTargetBuilder(gatherIngredientsStep)
)
@@ -58,35 +56,33 @@ class FriedFishProcess:
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(
process_builder.on_input_event(FriedFishProcess.ProcessEvents.PrepareFriedFish).send_event_to(
gather_ingredients_step
)
gather_ingredients_step.on_event(
GatherFriedFishIngredientsWithStockStep.OutputEvents.IngredientsGathered.value
GatherFriedFishIngredientsWithStockStep.OutputEvents.IngredientsGathered
).send_event_to(
ProcessFunctionTargetBuilder(chop_step, function_name=CutFoodWithSharpeningStep.Functions.ChopFood.value)
ProcessFunctionTargetBuilder(chop_step, function_name=CutFoodWithSharpeningStep.Functions.ChopFood)
)
gather_ingredients_step.on_event(
GatherFriedFishIngredientsWithStockStep.OutputEvents.IngredientsOutOfStock.value
GatherFriedFishIngredientsWithStockStep.OutputEvents.IngredientsOutOfStock
).stop_process()
chop_step.on_event(CutFoodWithSharpeningStep.OutputEvents.ChoppingReady.value).send_event_to(
chop_step.on_event(CutFoodWithSharpeningStep.OutputEvents.ChoppingReady).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.KnifeNeedsSharpening).send_event_to(
ProcessFunctionTargetBuilder(chop_step, function_name=CutFoodWithSharpeningStep.Functions.SharpenKnife)
)
chop_step.on_event(CutFoodWithSharpeningStep.OutputEvents.KnifeSharpened.value).send_event_to(
ProcessFunctionTargetBuilder(chop_step, function_name=CutFoodWithSharpeningStep.Functions.ChopFood.value)
chop_step.on_event(CutFoodWithSharpeningStep.OutputEvents.KnifeSharpened).send_event_to(
ProcessFunctionTargetBuilder(chop_step, function_name=CutFoodWithSharpeningStep.Functions.ChopFood)
)
fry_step.on_event(FryFoodStep.OutputEvents.FoodRuined.value).send_event_to(
fry_step.on_event(FryFoodStep.OutputEvents.FoodRuined).send_event_to(
ProcessFunctionTargetBuilder(gather_ingredients_step)
)
@@ -36,19 +36,17 @@ class PotatoFriesProcess:
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(
process_builder.on_input_event(PotatoFriesProcess.ProcessEvents.PreparePotatoFries).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))
GatherPotatoFriesIngredientsStep.OutputEvents.IngredientsGathered
).send_event_to(ProcessFunctionTargetBuilder(slice_step, function_name=CutFoodStep.Functions.SliceFood))
slice_step.on_event(CutFoodStep.OutputEvents.SlicingReady.value).send_event_to(
ProcessFunctionTargetBuilder(fry_step)
)
slice_step.on_event(CutFoodStep.OutputEvents.SlicingReady).send_event_to(ProcessFunctionTargetBuilder(fry_step))
fry_step.on_event(FryFoodStep.OutputEvents.FoodRuined.value).send_event_to(
fry_step.on_event(FryFoodStep.OutputEvents.FoodRuined).send_event_to(
ProcessFunctionTargetBuilder(gather_ingredients_step)
)
@@ -61,35 +59,33 @@ class PotatoFriesProcess:
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(
process_builder.on_input_event(PotatoFriesProcess.ProcessEvents.PreparePotatoFries).send_event_to(
gather_ingredients_step
)
gather_ingredients_step.on_event(
GatherPotatoFriesIngredientsWithStockStep.OutputEvents.IngredientsGathered.value
GatherPotatoFriesIngredientsWithStockStep.OutputEvents.IngredientsGathered
).send_event_to(
ProcessFunctionTargetBuilder(slice_step, function_name=CutFoodWithSharpeningStep.Functions.SliceFood.value)
ProcessFunctionTargetBuilder(slice_step, function_name=CutFoodWithSharpeningStep.Functions.SliceFood)
)
gather_ingredients_step.on_event(
GatherPotatoFriesIngredientsWithStockStep.OutputEvents.IngredientsOutOfStock.value
GatherPotatoFriesIngredientsWithStockStep.OutputEvents.IngredientsOutOfStock
).stop_process()
slice_step.on_event(CutFoodWithSharpeningStep.OutputEvents.SlicingReady.value).send_event_to(
slice_step.on_event(CutFoodWithSharpeningStep.OutputEvents.SlicingReady).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.KnifeNeedsSharpening).send_event_to(
ProcessFunctionTargetBuilder(slice_step, function_name=CutFoodWithSharpeningStep.Functions.SharpenKnife)
)
slice_step.on_event(CutFoodWithSharpeningStep.OutputEvents.KnifeSharpened.value).send_event_to(
ProcessFunctionTargetBuilder(slice_step, function_name=CutFoodWithSharpeningStep.Functions.SliceFood.value)
slice_step.on_event(CutFoodWithSharpeningStep.OutputEvents.KnifeSharpened).send_event_to(
ProcessFunctionTargetBuilder(slice_step, function_name=CutFoodWithSharpeningStep.Functions.SliceFood)
)
fry_step.on_event(FryFoodStep.OutputEvents.FoodRuined.value).send_event_to(
fry_step.on_event(FryFoodStep.OutputEvents.FoodRuined).send_event_to(
ProcessFunctionTargetBuilder(gather_ingredients_step)
)
@@ -22,15 +22,15 @@ class PackOrderStep(KernelProcessStep):
class OutputEvents(Enum):
FoodPacked = "FoodPacked"
@kernel_function(name=Functions.PackFood.value)
@kernel_function(name=Functions.PackFood)
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)
await context.emit_event(process_event=PackOrderStep.OutputEvents.FoodPacked)
class ExternalSingleOrderStep(ExternalStep):
def __init__(self):
super().__init__(SingleFoodItemProcess.ProcessEvents.SingleOrderReady.value)
super().__init__(SingleFoodItemProcess.ProcessEvents.SingleOrderReady)
class DispatchSingleOrderStep(KernelProcessStep):
@@ -43,30 +43,28 @@ class DispatchSingleOrderStep(KernelProcessStep):
PrepareFishSandwich = "PrepareFishSandwich"
PrepareFishAndChips = "PrepareFishAndChips"
@kernel_function(name=Functions.PrepareSingleOrder.value)
@kernel_function(name=Functions.PrepareSingleOrder)
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
)
await context.emit_event(process_event=DispatchSingleOrderStep.OutputEvents.PrepareFries, data=food_actions)
elif food_item == FoodItem.FRIED_FISH:
await context.emit_event(
process_event=DispatchSingleOrderStep.OutputEvents.PrepareFriedFish.value, data=food_actions
process_event=DispatchSingleOrderStep.OutputEvents.PrepareFriedFish, data=food_actions
)
elif food_item == FoodItem.FISH_SANDWICH:
await context.emit_event(
process_event=DispatchSingleOrderStep.OutputEvents.PrepareFishSandwich.value, data=food_actions
process_event=DispatchSingleOrderStep.OutputEvents.PrepareFishSandwich, data=food_actions
)
elif food_item == FoodItem.FISH_AND_CHIPS:
await context.emit_event(
process_event=DispatchSingleOrderStep.OutputEvents.PrepareFishAndChips.value, data=food_actions
process_event=DispatchSingleOrderStep.OutputEvents.PrepareFishAndChips, data=food_actions
)
@@ -91,39 +89,39 @@ class SingleFoodItemProcess:
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.PrepareFriedFish).send_event_to(
make_fried_fish_step.where_input_event_is(FriedFishProcess.ProcessEvents.PrepareFriedFish)
)
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.PrepareFries).send_event_to(
make_potato_fries_step.where_input_event_is(PotatoFriesProcess.ProcessEvents.PreparePotatoFries)
)
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.PrepareFishSandwich).send_event_to(
make_fish_sandwich_step.where_input_event_is(FishSandwichProcess.ProcessEvents.PrepareFishSandwich)
)
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)
dispatch_order_step.on_event(DispatchSingleOrderStep.OutputEvents.PrepareFishAndChips).send_event_to(
make_fish_and_chips_step.where_input_event_is(FishAndChipsProcess.ProcessEvents.PrepareFishAndChips)
)
make_fried_fish_step.on_event(FriedFishProcess.ProcessEvents.FriedFishReady.value).send_event_to(
make_fried_fish_step.on_event(FriedFishProcess.ProcessEvents.FriedFishReady).send_event_to(
ProcessFunctionTargetBuilder(pack_order_step)
)
make_potato_fries_step.on_event(PotatoFriesProcess.ProcessEvents.PotatoFriesReady.value).send_event_to(
make_potato_fries_step.on_event(PotatoFriesProcess.ProcessEvents.PotatoFriesReady).send_event_to(
ProcessFunctionTargetBuilder(pack_order_step)
)
make_fish_sandwich_step.on_event(FishSandwichProcess.ProcessEvents.FishSandwichReady.value).send_event_to(
make_fish_sandwich_step.on_event(FishSandwichProcess.ProcessEvents.FishSandwichReady).send_event_to(
ProcessFunctionTargetBuilder(pack_order_step)
)
make_fish_and_chips_step.on_event(FishAndChipsProcess.ProcessEvents.FishAndChipsReady.value).send_event_to(
make_fish_and_chips_step.on_event(FishAndChipsProcess.ProcessEvents.FishAndChipsReady).send_event_to(
ProcessFunctionTargetBuilder(pack_order_step)
)
pack_order_step.on_event(PackOrderStep.OutputEvents.FoodPacked.value).send_event_to(
pack_order_step.on_event(PackOrderStep.OutputEvents.FoodPacked).send_event_to(
ProcessFunctionTargetBuilder(external_step)
)
@@ -19,16 +19,16 @@ class CutFoodStep(KernelProcessStep):
def get_action_string(self, food: str, action: str) -> str:
return f"{food}_{action}"
@kernel_function(name=Functions.ChopFood.value)
@kernel_function(name=Functions.ChopFood)
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)
await context.emit_event(process_event=CutFoodStep.OutputEvents.ChoppingReady, data=food_actions)
@kernel_function(name=Functions.SliceFood.value)
@kernel_function(name=Functions.SliceFood)
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)
await context.emit_event(process_event=CutFoodStep.OutputEvents.SlicingReady, data=food_actions)
@@ -38,13 +38,13 @@ class CutFoodWithSharpeningStep(KernelProcessStep[CutFoodWithSharpeningState]):
def get_action_string(self, food: str, action: str) -> str:
return f"{food}_{action}"
@kernel_function(name=Functions.ChopFood.value)
@kernel_function(name=Functions.ChopFood)
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
process_event=CutFoodWithSharpeningStep.OutputEvents.KnifeNeedsSharpening, data=food_actions
)
return
@@ -55,17 +55,15 @@ class CutFoodWithSharpeningStep(KernelProcessStep[CutFoodWithSharpeningState]):
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
)
await context.emit_event(process_event=CutFoodWithSharpeningStep.OutputEvents.ChoppingReady, data=food_actions)
@kernel_function(name=Functions.SliceFood.value)
@kernel_function(name=Functions.SliceFood)
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
process_event=CutFoodWithSharpeningStep.OutputEvents.KnifeNeedsSharpening, data=food_actions
)
return
@@ -76,14 +74,10 @@ class CutFoodWithSharpeningStep(KernelProcessStep[CutFoodWithSharpeningState]):
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
)
await context.emit_event(process_event=CutFoodWithSharpeningStep.OutputEvents.SlicingReady, data=food_actions)
@kernel_function(name=Functions.SharpenKnife.value)
@kernel_function(name=Functions.SharpenKnife)
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
)
await context.emit_event(process_event=CutFoodWithSharpeningStep.OutputEvents.KnifeSharpened, data=food_actions)
@@ -23,7 +23,7 @@ class FryFoodStep(KernelProcessStep):
random_seed: int = Field(default_factory=Random)
@kernel_function(name=Functions.FryFood.value)
@kernel_function(name=Functions.FryFood)
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)
@@ -32,13 +32,13 @@ class FryFoodStep(KernelProcessStep):
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)
await context.emit_event(process_event=FryFoodStep.OutputEvents.FoodRuined, 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,
process_event=FryFoodStep.OutputEvents.FriedFoodReady,
data=food_actions,
visibility=KernelProcessEventVisibility.Public,
)
@@ -23,7 +23,7 @@ class GatherIngredientsStep(KernelProcessStep):
def __init__(self, ingredient: FoodIngredients):
super().__init__(ingredient=ingredient)
@kernel_function(name=Functions.GatherIngredients.value)
@kernel_function(name=Functions.GatherIngredients)
async def gather_ingredients(self, context: KernelProcessStepContext, food_actions: list[str]):
ingredient = self.ingredient.to_friendly_string()
updated_food_actions = []
@@ -34,7 +34,7 @@ class GatherIngredientsStep(KernelProcessStep):
print(f"GATHER_INGREDIENT: Gathered ingredient {ingredient}")
await context.emit_event(
process_event=GatherIngredientsStep.OutputEvents.IngredientsGathered.value, data=updated_food_actions
process_event=GatherIngredientsStep.OutputEvents.IngredientsGathered, data=updated_food_actions
)
@@ -59,7 +59,7 @@ class GatherIngredientsWithStockStep(KernelProcessStep[GatherIngredientsState]):
async def activate(self, state: KernelProcessStepState[GatherIngredientsState]) -> None:
self.state = state.state
@kernel_function(name=Functions.GatherIngredients.value)
@kernel_function(name=Functions.GatherIngredients)
async def gather_ingredients(self, context: KernelProcessStepContext, food_actions: list[str]):
ingredient = self.ingredient.to_friendly_string()
updated_food_actions = []
@@ -68,7 +68,7 @@ class GatherIngredientsWithStockStep(KernelProcessStep[GatherIngredientsState]):
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,
process_event=GatherIngredientsWithStockStep.OutputEvents.IngredientsOutOfStock,
data=updated_food_actions,
)
return
@@ -80,6 +80,6 @@ class GatherIngredientsWithStockStep(KernelProcessStep[GatherIngredientsState]):
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,
process_event=GatherIngredientsWithStockStep.OutputEvents.IngredientsGathered,
data=updated_food_actions,
)
@@ -0,0 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.processes.process_builder import ProcessBuilder
from semantic_kernel.processes.process_types import TState
__all__ = [
"ProcessBuilder",
"TState",
]
@@ -0,0 +1,23 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.processes.dapr_runtime.actors.event_buffer_actor import EventBufferActor
from semantic_kernel.processes.dapr_runtime.actors.external_event_buffer_actor import ExternalEventBufferActor
from semantic_kernel.processes.dapr_runtime.actors.message_buffer_actor import MessageBufferActor
from semantic_kernel.processes.dapr_runtime.actors.process_actor import ProcessActor
from semantic_kernel.processes.dapr_runtime.actors.step_actor import StepActor
from semantic_kernel.processes.dapr_runtime.dapr_actor_registration import (
register_fastapi_dapr_actors,
register_flask_dapr_actors,
)
from semantic_kernel.processes.dapr_runtime.dapr_kernel_process import start
__all__ = [
"EventBufferActor",
"ExternalEventBufferActor",
"MessageBufferActor",
"ProcessActor",
"StepActor",
"register_fastapi_dapr_actors",
"register_flask_dapr_actors",
"start",
]
@@ -0,0 +1,30 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
from semantic_kernel.utils.experimental_decorator import experimental_class
@experimental_class
class ActorStateKeys(Enum):
"""Keys used to store actor state in Dapr."""
# StepActor Keys
StepParentProcessId = "parentProcessId"
StepInfoState = "DaprStepInfo"
StepStateJson = "kernelStepStateJson"
StepStateType = "kernelStepStateType"
StepIncomingMessagesState = "incomingMessagesState"
# ProcessActor Keys
ProcessInfoState = "DaprProcessInfo"
StepActivatedState = "kernelStepActivated"
# MessageBufferActor Keys
MessageQueueState = "DaprMessageBufferState"
# ExternalEventBufferActor Keys
ExternalEventQueueState = "DaprExternalEventBufferState"
# EventBufferActor Keys
EventQueueState = "DaprEventBufferState"
@@ -0,0 +1,92 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
from queue import Queue
from dapr.actor import Actor
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.interfaces.message_buffer_interface import MessageBufferInterface
from semantic_kernel.utils.experimental_decorator import experimental_class
logger = logging.getLogger(__name__)
@experimental_class
class EventBufferActor(Actor, MessageBufferInterface):
"""Represents a message buffer actor that manages a queue of JSON strings representing events."""
queue: Queue = Queue()
async def enqueue(self, message: str) -> None:
"""Enqueues a JSON string message event into the buffer and updates the state.
Args:
message: The message event to enqueue as a JSON string.
Raises:
Exception: If an error occurs during the enqueue operation.
"""
try:
self.queue.put(message)
queue_list = list(self.queue.queue)
queue_json = json.dumps(queue_list)
await self._state_manager.try_add_state(ActorStateKeys.EventQueueState.value, queue_json)
await self._state_manager.save_state()
logger.info(f"Enqueued message and updated state for actor ID: {self.id.id}")
except Exception as e:
error_message = str(e)
logger.error(f"Error in enqueue: {error_message}")
raise Exception(error_message)
async def dequeue_all(self) -> list[str]:
"""Dequeues all process events from the buffer and returns them as JSON strings.
Returns:
A list of JSON strings representing the dequeued messages.
Raises:
Exception: If an error occurs during the dequeue operation.
"""
try:
items = []
while not self.queue.empty():
items.append(self.queue.get())
await self._state_manager.try_add_state(ActorStateKeys.EventQueueState.value, json.dumps([]))
await self._state_manager.save_state()
logger.info(f"Dequeued all messages and updated state for actor ID: {self.id.id}")
return items
except Exception as e:
error_message = str(e)
logger.error(f"Error in dequeue_all: {error_message}")
raise Exception(error_message)
async def _on_activate(self) -> None:
"""Activates the actor and initializes the queue state from Dapr storage.
Raises:
Exception: If an error occurs during actor activation.
"""
try:
logger.info(f"Activating actor with ID: {self.id.id}")
state_exists, queue_json = await self._state_manager.try_get_state(ActorStateKeys.EventQueueState.value)
if state_exists and queue_json:
queue_list = json.loads(queue_json)
self.queue = Queue()
for item in queue_list:
self.queue.put(item)
logger.info(f"Reconstructed queue from state for actor ID: {self.id.id}")
else:
self.queue = Queue()
logger.info(f"No existing state found. Initialized empty queue for actor ID: {self.id.id}")
except Exception as e:
error_message = str(e)
logger.error(f"Error in _on_activate: {error_message}")
raise Exception(error_message)
@@ -0,0 +1,106 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
from queue import Queue
from dapr.actor import Actor, ActorId
from dapr.actor.runtime.context import ActorRuntimeContext
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.interfaces.external_event_buffer_interface import (
ExternalEventBufferInterface,
)
from semantic_kernel.utils.experimental_decorator import experimental_class
logger = logging.getLogger(__name__)
@experimental_class
class ExternalEventBufferActor(Actor, ExternalEventBufferInterface):
"""Represents a message buffer actor that follows the MessageBuffer abstract class."""
def __init__(self, ctx: ActorRuntimeContext, actor_id: ActorId):
"""Initializes a new instance of ExternalEventBufferActor.
Args:
ctx: The actor runtime context.
actor_id: The unique ID for the actor.
"""
super().__init__(ctx, actor_id)
self.queue: Queue[str] = Queue()
async def enqueue(self, message: str) -> None:
"""Enqueues a message event into the buffer.
Args:
message: The message event to enqueue as a JSON string.
Raises:
Exception: If an error occurs during enqueue operation.
"""
try:
self.queue.put(message)
queue_list = list(self.queue.queue)
queue_state = json.dumps(queue_list)
await self._state_manager.try_add_state(ActorStateKeys.ExternalEventQueueState.value, queue_state)
await self._state_manager.save_state()
logger.info(f"Enqueued message and updated state for actor ID: {self.id.id}")
except Exception as e:
error_message = str(e)
logger.error(f"Error in enqueue: {error_message}")
raise Exception(error_message)
async def dequeue_all(self) -> list[str]:
"""Dequeues all process events from the buffer and returns them as JSON strings.
Returns:
A list of JSON strings representing the dequeued messages.
Raises:
Exception: If an error occurs during dequeue operation.
"""
try:
items = []
while not self.queue.empty():
items.append(self.queue.get())
await self._state_manager.try_add_state(ActorStateKeys.ExternalEventQueueState.value, json.dumps([]))
await self._state_manager.save_state()
logger.info(f"Dequeued all messages and updated state for actor ID: {self.id.id}")
return items
except Exception as e:
error_message = str(e)
logger.error(f"Error in dequeue_all: {error_message}")
raise Exception(error_message)
async def _on_activate(self) -> None:
"""Called when the actor is activated to initialize state.
Raises:
Exception: If an error occurs during actor activation.
"""
try:
logger.info(f"Activating actor with ID: {self.id.id}")
state_exists, queue_json = await self._state_manager.try_get_state(
ActorStateKeys.ExternalEventQueueState.value
)
if state_exists and queue_json:
queue_list = json.loads(queue_json)
self.queue = Queue()
for item in queue_list:
self.queue.put(item)
logger.info(f"Reconstructed queue from state for actor ID: {self.id.id}")
else:
self.queue = Queue()
logger.info(f"No existing state found. Initialized empty queue for actor ID: {self.id.id}")
except Exception as e:
error_message = str(e)
logger.error(f"Error in _on_activate: {error_message}")
raise Exception(error_message)
@@ -0,0 +1,77 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
from queue import Queue
from dapr.actor import Actor, ActorId
from dapr.actor.runtime.context import ActorRuntimeContext
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.interfaces.message_buffer_interface import MessageBufferInterface
from semantic_kernel.utils.experimental_decorator import experimental_class
logger: logging.Logger = logging.getLogger(__name__)
@experimental_class
class MessageBufferActor(Actor, MessageBufferInterface):
"""Represents a message buffer actor that follows the MessageBuffer abstract class."""
def __init__(self, ctx: ActorRuntimeContext, actor_id: ActorId):
"""Initializes a new instance of MessageBufferActor."""
super().__init__(ctx, actor_id)
self.queue: Queue[str] = Queue()
async def enqueue(self, message: str) -> None:
"""Enqueues a message event into the buffer and updates the state.
Args:
message (str): The message to enqueue.
"""
try:
self.queue.put(message)
queue_list = list(self.queue.queue)
await self._state_manager.try_add_state(ActorStateKeys.MessageQueueState.value, queue_list)
await self._state_manager.save_state()
except Exception as e:
error_message = str(e)
logger.error(f"Error in MessageBufferActor enqueue: {error_message}")
raise Exception(error_message)
async def dequeue_all(self) -> list[str]:
"""Dequeues all process events from the buffer and returns them as a list of strings."""
try:
items = []
while not self.queue.empty():
items.append(self.queue.get())
await self._state_manager.try_add_state(ActorStateKeys.MessageQueueState.value, json.dumps([]))
await self._state_manager.save_state()
return items
except Exception as e:
error_message = str(e)
logger.error(f"Error in MessageBufferActor dequeue_all: {error_message}")
raise Exception(error_message)
async def _on_activate(self) -> None:
"""Called when the actor is activated."""
try:
logger.info(f"Activating actor with ID: {self.id.id}")
has_value, queue_list = await self._state_manager.try_get_state(ActorStateKeys.MessageQueueState.value)
if has_value and queue_list:
self.queue = Queue()
for item_dict in queue_list:
self.queue.put(item_dict)
else:
self.queue = Queue()
except Exception as e:
error_message = str(e)
logger.error(f"Error in MessageBufferActor _on_activate: {error_message}")
raise Exception(error_message)
@@ -0,0 +1,427 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import contextlib
import json
import logging
import uuid
from queue import Queue
from typing import Any
from dapr.actor import ActorId, ActorProxy
from dapr.actor.runtime.context import ActorRuntimeContext
from semantic_kernel.exceptions.kernel_exceptions import KernelException
from semantic_kernel.exceptions.process_exceptions import ProcessEventUndefinedException
from semantic_kernel.kernel import Kernel
from semantic_kernel.processes.const import END_PROCESS_ID
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.actors.event_buffer_actor import EventBufferActor
from semantic_kernel.processes.dapr_runtime.actors.external_event_buffer_actor import ExternalEventBufferActor
from semantic_kernel.processes.dapr_runtime.actors.message_buffer_actor import MessageBufferActor
from semantic_kernel.processes.dapr_runtime.actors.step_actor import StepActor
from semantic_kernel.processes.dapr_runtime.dapr_process_info import DaprProcessInfo
from semantic_kernel.processes.dapr_runtime.dapr_step_info import DaprStepInfo
from semantic_kernel.processes.dapr_runtime.interfaces.event_buffer_interface import EventBufferInterface
from semantic_kernel.processes.dapr_runtime.interfaces.external_event_buffer_interface import (
ExternalEventBufferInterface,
)
from semantic_kernel.processes.dapr_runtime.interfaces.message_buffer_interface import MessageBufferInterface
from semantic_kernel.processes.dapr_runtime.interfaces.process_interface import ProcessInterface
from semantic_kernel.processes.dapr_runtime.interfaces.step_interface import StepInterface
from semantic_kernel.processes.kernel_process.kernel_process_event import (
KernelProcessEvent,
KernelProcessEventVisibility,
)
from semantic_kernel.processes.kernel_process.kernel_process_state import KernelProcessState
from semantic_kernel.processes.process_event import ProcessEvent
from semantic_kernel.processes.process_message import ProcessMessage
from semantic_kernel.processes.process_message_factory import ProcessMessageFactory
from semantic_kernel.utils.experimental_decorator import experimental_class
logger: logging.Logger = logging.getLogger(__name__)
@experimental_class
class ProcessActor(StepActor, ProcessInterface):
"""A local process that contains a collection of steps."""
def __init__(self, ctx: ActorRuntimeContext, actor_id: ActorId, kernel: Kernel):
"""Initializes a new instance of ProcessActor.
Args:
ctx: The actor runtime context.
actor_id: The unique ID for the actor.
kernel: The Kernel dependency to be injected.
"""
super().__init__(ctx, actor_id, kernel)
self.kernel = kernel
self.steps: list[StepInterface] = []
self.step_infos: list[DaprStepInfo] = []
self.initialize_task: bool | None = False
self.external_event_queue: Queue = Queue()
self.process_task: asyncio.Task | None = None
self.process: DaprProcessInfo | None = None
@property
def name(self) -> str:
"""Gets the name of the step."""
if self.process is None or self.process.state is None or self.process.state.name is None:
error_message = "The process must be initialized before accessing the name property."
logger.error(error_message)
raise KernelException(error_message)
return self.process.state.name
async def initialize_process(self, input: dict | str) -> None:
"""Initializes the process."""
if isinstance(input, str):
input = json.loads(input)
if not isinstance(input, dict):
raise TypeError("input must be a JSON string or a dictionary")
process_info_data = input.get("process_info")
parent_process_id = input.get("parent_process_id")
if process_info_data is None:
raise ValueError("The process info is not defined.")
if isinstance(process_info_data, str):
process_info_dict = json.loads(process_info_data)
elif isinstance(process_info_data, dict):
process_info_dict = process_info_data
else:
raise TypeError("process_info must be a JSON string or a dictionary")
# Reconstruct the DaprProcessInfo from the dictionary
dapr_process_info = DaprProcessInfo.model_validate(process_info_dict)
if dapr_process_info.steps is None:
raise ValueError("The process info does not contain any steps.")
if self.initialize_task:
return
await self._initialize_process_actor(dapr_process_info, parent_process_id)
try:
# Serialize dapr_process_info before saving
process_info_serialized = dapr_process_info.model_dump()
await self._state_manager.try_add_state(ActorStateKeys.ProcessInfoState.value, process_info_serialized)
await self._state_manager.try_add_state(
ActorStateKeys.StepParentProcessId.value, parent_process_id if parent_process_id else ""
)
await self._state_manager.try_add_state(ActorStateKeys.StepActivatedState.value, True)
await self._state_manager.save_state()
logger.info(f"Initialized process for: {dapr_process_info} and parent process ID: {parent_process_id}")
except Exception as ex:
error_message = str(ex)
logger.error(f"Error in initialize_process: {error_message}")
raise Exception(error_message)
async def start(self, keep_alive: bool = True) -> None:
"""Starts the process."""
if not self.initialize_task:
raise ValueError("The process has not been initialized.")
# Only create the task if it doesn't already exist or is not running
if not self.process_task or self.process_task.done():
self.process_task = asyncio.create_task(self.internal_execute(keep_alive=keep_alive))
async def run_once(self, process_event: str) -> None:
"""Starts the process with an initial event and waits for it to finish.
Args:
process_event: The initial event to start the process represented as a string of a KernelProcessEvent
"""
if process_event is None:
raise ProcessEventUndefinedException("The process event must be specified.")
external_event_queue: ExternalEventBufferActor = ActorProxy.create( # type: ignore
actor_type=f"{ExternalEventBufferActor.__name__}",
actor_id=ActorId(self.id.id),
actor_interface=ExternalEventBufferInterface,
)
try:
await external_event_queue.enqueue(process_event)
logger.info(f"Run once for process event: {process_event}")
await self.start(keep_alive=False)
if self.process_task:
try:
await self.process_task
except asyncio.CancelledError:
logger.error("Process task was cancelled")
except Exception as ex:
logger.error(f"Error in run_once: {ex}")
raise ex
async def stop(self):
"""Stops a running process."""
if not self.process_task or self.process_task.done():
return # Task is already finished or hasn't started
self.process_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await self.process_task
async def initialize_step(self):
"""Initializes the step."""
# The process does not need any further initialization
pass
async def activate_step(self):
"""Overrides the step's activate_step method."""
# The process does not need any further initialization
pass
async def _on_activate(self) -> None:
"""Called when the actor is activated."""
try:
has_value, existing_process_info = await self._state_manager.try_get_state(
ActorStateKeys.ProcessInfoState.value
)
if has_value and existing_process_info:
has_value, parent_process_id = await self._state_manager.try_get_state(
ActorStateKeys.StepParentProcessId.value
)
combined_input = {**existing_process_info, **parent_process_id} # type: ignore
await self.initialize_process(combined_input)
except Exception as e:
error_message = str(e)
logger.error(f"Error in _on_activate: {error_message}")
raise Exception(error_message)
async def send_message(self, process_event: KernelProcessEvent):
"""Sends a message to the process."""
if process_event is None:
raise ProcessEventUndefinedException("The process event must be specified.")
self.external_event_queue.put(process_event)
async def get_process_info(self):
"""Gets the process information."""
return await self.to_dapr_process_info()
async def to_dapr_process_info(self) -> DaprProcessInfo:
"""Converts the process to a Dapr process info."""
if self.process is None:
raise ValueError("The process must be initialized before converting to DaprProcessInfo.")
if self.inner_step_type is None:
raise ValueError("The inner step type must be defined before converting to DaprProcessInfo.")
process_state = KernelProcessState(self.name, self.id.id)
step_tasks = [step.to_dapr_step_info() for step in self.steps]
steps = await asyncio.gather(*step_tasks)
return DaprProcessInfo(
inner_step_python_type=self.inner_step_type, edges=self.process.edges, state=process_state, steps=steps
)
async def handle_message(self, message: ProcessMessage) -> None:
"""Handles a message."""
if message.target_event_id is None:
raise KernelException(
"Internal Process Error: The target event id must be specified when sending a message to a step."
)
event_id = message.target_event_id
if event_id in self.output_edges and self.output_edges[event_id] is not None:
for _ in self.output_edges[event_id]:
nested_event = KernelProcessEvent(
id=event_id, data=message.target_event_data, visibility=KernelProcessEventVisibility.Internal
)
await self.run_once(nested_event.model_dump_json())
async def _initialize_process_actor(
self, process_info: DaprProcessInfo, parent_process_id: str | None = None
) -> None:
"""Initializes the process actor."""
if process_info is None:
raise ValueError("The process info is not defined.")
if process_info.steps is None:
raise ValueError("The process info does not contain any steps.")
self.parent_process_id = parent_process_id
self.process = process_info
self.step_infos = list(self.process.steps)
self.output_edges = {kvp[0]: list(kvp[1]) for kvp in self.process.edges.items()}
for step in self.step_infos:
step_actor = None
# The current step should already have a name.
assert step.state and step.state.name is not None # nosec
if isinstance(step, DaprProcessInfo):
# The process will only have an Id if it's already been executed.
if not step.state.id:
step.state.id = str(uuid.uuid4().hex)
# Initialize the step as a process
scoped_process_id = self._scoped_actor_id(ActorId(step.state.id))
process_actor: ProcessInterface = ActorProxy.create( # type: ignore
actor_type=f"{ProcessActor.__name__}",
actor_id=scoped_process_id,
actor_interface=ProcessInterface,
)
process_payload: dict[str, Any] = {
"process_info": step.model_dump_json(),
"parent_process_id": self.id.id,
}
await process_actor.initialize_process(process_payload)
step_actor = ActorProxy.create( # type: ignore
actor_type=f"{ProcessActor.__name__}",
actor_id=scoped_process_id,
actor_interface=StepInterface,
)
else:
# The current step should already have an Id.
assert step.state and step.state.id is not None # nosec
scoped_step_id = self._scoped_actor_id(ActorId(step.state.id))
step_actor: StepInterface = ActorProxy.create( # type: ignore
actor_type=f"{StepActor.__name__}",
actor_id=scoped_step_id,
actor_interface=StepInterface,
)
assert step_actor is not None # nosec
step_dict = step.model_dump()
step_payload: dict[str, Any] = {"step_info": step_dict, "parent_process_id": self.id.id}
try:
await step_actor.initialize_step(json.dumps(step_payload))
except Exception as ex:
logger.error(f"Error initializing ProcessActor step: {ex}")
raise ex
# Add the local step to the list of steps
self.steps.append(step_actor) # type: ignore
self.initialize_task = True
def _scoped_actor_id(self, actor_id: ActorId, scope_to_parent: bool = False) -> ActorId:
"""Creates a scoped actor ID."""
if scope_to_parent and self.parent_process_id is None:
raise ValueError("The parent process Id must be set before scoping to the parent process.")
id = self.parent_process_id if scope_to_parent else self.id.id
return ActorId(f"{id}.{actor_id.id}")
async def internal_execute(self, max_supersteps: int = 100, keep_alive: bool = True):
"""Internal execution logic for the process."""
try:
for _ in range(max_supersteps):
if await self._is_end_message_sent():
# Exit the loop without cancelling the task
break
# Check for external events
await self._enqueue_external_messages()
# Prepare incoming messages for each step
step_preparation_tasks = [step.prepare_incoming_messages() for step in self.steps]
message_counts = await asyncio.gather(*step_preparation_tasks)
if sum(message_counts) == 0 and (not keep_alive or self.external_event_queue.empty()):
# Exit the loop without cancelling the task
break
# Process the incoming messages for each step
step_processing_tasks = [step.process_incoming_messages() for step in self.steps]
await asyncio.gather(*step_processing_tasks)
# Handle public events
await self.send_outgoing_public_events()
except Exception as ex:
logger.error(f"An error occurred while running the process: {ex}")
raise
def _scoped_event(self, dapr_event: ProcessEvent):
if dapr_event is None:
raise ValueError("The Dapr event must be specified.")
if self.process is None or self.process.state is None:
raise ValueError("The process must be initialized before scoping the event.")
dapr_event.namespace = f"{self.name}_{self.process.state.id}"
return dapr_event
async def send_outgoing_public_events(self) -> None:
"""Sends outgoing public events."""
if self.parent_process_id is not None:
event_queue: EventBufferActor = ActorProxy.create( # type: ignore
actor_type=f"{EventBufferActor.__name__}",
actor_id=ActorId(self.id.id),
actor_interface=EventBufferInterface,
)
all_events: list[str] = await event_queue.dequeue_all()
process_events = [ProcessEvent.model_validate(json.loads(e)) for e in all_events]
for e in process_events:
scoped_event = self._scoped_event(e)
if scoped_event.id in self.output_edges and self.output_edges[scoped_event.id] is not None:
for edge in self.output_edges[scoped_event.id]:
message: ProcessMessage = ProcessMessageFactory.create_from_edge(edge, e.data)
scoped_message_buffer_id = self._scoped_actor_id(
ActorId(edge.output_target.step_id), scope_to_parent=True
)
message_queue: MessageBufferActor = ActorProxy.create( # type: ignore
actor_id=scoped_message_buffer_id,
actor_type=f"{MessageBufferActor.__name__}",
actor_interface=MessageBufferInterface,
)
message_json = json.dumps(message.model_dump())
logger.info(f"Enqueueing message: {message_json}")
await message_queue.enqueue(message_json)
async def _is_end_message_sent(self) -> bool:
"""Checks if the end message has been sent."""
scoped_message_buffer_id = self._scoped_actor_id(ActorId(END_PROCESS_ID))
end_message_queue: MessageBufferActor = ActorProxy.create( # type: ignore
actor_type=f"{MessageBufferActor.__name__}",
actor_id=scoped_message_buffer_id,
actor_interface=MessageBufferInterface,
)
messages: list[str] = await end_message_queue.dequeue_all()
logger.info(f"End message sent: {len(messages) > 0}")
return len(messages) > 0
async def _enqueue_external_messages(self) -> None:
"""Enqueues external messages into the process."""
external_event_queue: ExternalEventBufferActor = ActorProxy.create( # type: ignore
actor_type=f"{ExternalEventBufferActor.__name__}",
actor_id=ActorId(self.id.id),
actor_interface=ExternalEventBufferInterface,
)
external_events_json = await external_event_queue.dequeue_all()
logger.info(f"External events dequeued: {len(external_events_json)} with json: {external_events_json}")
external_events = [KernelProcessEvent.model_validate(json.loads(e)) for e in external_events_json]
for external_event in external_events:
if external_event.id in self.output_edges and self.output_edges[external_event.id] is not None:
for edge in self.output_edges[external_event.id]:
message: ProcessMessage = ProcessMessageFactory.create_from_edge(edge, external_event.data)
scoped_message_buffer_id = self._scoped_actor_id(ActorId(edge.output_target.step_id))
message_queue: MessageBufferActor = ActorProxy.create( # type: ignore
actor_type=f"{MessageBufferActor.__name__}",
actor_id=scoped_message_buffer_id,
actor_interface=MessageBufferInterface,
)
message_json = json.dumps(message.model_dump())
logger.info(f"Enqueueing message: {message_json}")
await message_queue.enqueue(message_json)
@@ -0,0 +1,444 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import importlib
import json
import logging
from queue import Queue
from typing import Any
from dapr.actor import Actor, ActorId, ActorProxy
from dapr.actor.runtime.context import ActorRuntimeContext
from semantic_kernel.exceptions.kernel_exceptions import KernelException
from semantic_kernel.exceptions.process_exceptions import (
ProcessFunctionNotFoundException,
ProcessTargetFunctionNameMismatchException,
)
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.kernel import Kernel
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.actors.event_buffer_actor import EventBufferActor
from semantic_kernel.processes.dapr_runtime.actors.message_buffer_actor import MessageBufferActor
from semantic_kernel.processes.dapr_runtime.dapr_step_info import DaprStepInfo
from semantic_kernel.processes.dapr_runtime.interfaces.event_buffer_interface import EventBufferInterface
from semantic_kernel.processes.dapr_runtime.interfaces.message_buffer_interface import MessageBufferInterface
from semantic_kernel.processes.dapr_runtime.interfaces.step_interface import StepInterface
from semantic_kernel.processes.kernel_process.kernel_process_edge import KernelProcessEdge
from semantic_kernel.processes.kernel_process.kernel_process_event import (
KernelProcessEvent,
KernelProcessEventVisibility,
)
from semantic_kernel.processes.kernel_process.kernel_process_message_channel import KernelProcessMessageChannel
from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
from semantic_kernel.processes.process_event import ProcessEvent
from semantic_kernel.processes.process_message import ProcessMessage
from semantic_kernel.processes.process_message_factory import ProcessMessageFactory
from semantic_kernel.processes.process_types import get_generic_state_type
from semantic_kernel.processes.step_utils import find_input_channels, get_fully_qualified_name
from semantic_kernel.utils.experimental_decorator import experimental_class
logger: logging.Logger = logging.getLogger(__name__)
@experimental_class
class StepActor(Actor, StepInterface, KernelProcessMessageChannel):
"""Represents a step actor that follows the Step abstract class."""
def __init__(self, ctx: ActorRuntimeContext, actor_id: ActorId, kernel: Kernel):
"""Initializes a new instance of StepActor.
Args:
ctx: The actor runtime context.
actor_id: The unique ID for the actor.
kernel: The Kernel dependency to be injected.
"""
super().__init__(ctx, actor_id)
self.kernel = kernel
self.parent_process_id: str | None = None
self.step_info: DaprStepInfo | None = None
self.initialize_task: bool | None = False
self.event_namespace: str | None = None
self.inner_step_type: str | None = None
self.incoming_messages: Queue = Queue()
self.step_state: KernelProcessStepState | None = None
self.step_state_type: type | None = None
self.output_edges: dict[str, list[KernelProcessEdge]] = {}
self.functions: dict[str, KernelFunction] = {}
self.inputs: dict[str, dict[str, Any | None]] = {}
self.initial_inputs: dict[str, dict[str, Any | None]] = {}
self.init_lock: asyncio.Lock = asyncio.Lock()
self.step_activated: bool = False
@property
def name(self) -> str:
"""Gets the name of the step."""
if self.step_info is None or self.step_info.state is None or self.step_info.state.name is None:
error_message = "The step must be initialized before accessing the name property."
logger.error(error_message)
raise KernelException(error_message)
return self.step_info.state.name
async def initialize_step(self, input: str) -> None:
"""Initializes the step with the provided step information."""
if input is None:
raise ValueError("step_info must not be None")
if self.initialize_task:
return
try:
input_dict: dict[str, Any] = json.loads(input)
except (json.JSONDecodeError, TypeError):
raise ValueError("Input must be a valid JSON string representing a dictionary")
step_info = DaprStepInfo.model_validate(input_dict.get("step_info"))
await self._int_initialize_step(step_info, input_dict.get("parent_process_id"))
try:
await self._state_manager.try_add_state(ActorStateKeys.StepInfoState.value, step_info)
await self._state_manager.try_add_state(
ActorStateKeys.StepParentProcessId.value, input_dict.get("parent_process_id")
)
await self._state_manager.save_state()
except Exception as ex:
logger.error(f"Error in Step {self.name}: {ex!s}")
raise ex
async def _int_initialize_step(self, step_info: DaprStepInfo, parent_process_id: str | None = None) -> None:
"""Internal method to initialize the step with the provided step information.
Args:
step_info: The DaprStepInfo object to initialize the step with.
parent_process_id: Optional parent process ID if one exists.
"""
self.inner_step_type = step_info.inner_step_python_type
self.parent_process_id = parent_process_id
self.step_info = step_info
self.step_state = self.step_info.state
self.output_edges = {k: v for k, v in step_info.edges.items()}
self.event_namespace = f"{self.step_info.state.name}_{self.step_info.state.id}"
self.initialize_task = True
async def prepare_incoming_messages(self) -> int:
"""Prepares the incoming messages for processing."""
try:
message_queue: MessageBufferInterface = ActorProxy.create( # type: ignore
actor_type=f"{MessageBufferActor.__name__}",
actor_id=ActorId(self.id.id),
actor_interface=MessageBufferInterface,
)
incoming = await message_queue.dequeue_all()
messages = []
for message in incoming:
process_message = ProcessMessage.model_validate(json.loads(message))
messages.append(process_message)
for msg in messages:
self.incoming_messages.put(msg)
await self._state_manager.try_add_state(
ActorStateKeys.StepIncomingMessagesState.value,
incoming,
)
await self._state_manager.save_state()
return self.incoming_messages.qsize() if self.incoming_messages else 0
except Exception as e:
error_message = str(e)
logger.error(f"Error in prepare_incoming_messages: {error_message}")
raise Exception(error_message)
async def process_incoming_messages(self):
"""Processes the incoming messages for the step."""
while not self.incoming_messages.empty():
message = self.incoming_messages.get()
await self.handle_message(message)
messages_to_save = [json.dumps(msg.model_dump()) for msg in list(self.incoming_messages.queue)]
await self._state_manager.try_add_state(ActorStateKeys.StepIncomingMessagesState.value, messages_to_save)
await self._state_manager.save_state()
def _get_class_from_string(self, full_class_name: str):
"""Gets a class from a string."""
module_name, class_name = full_class_name.rsplit(".", 1)
module = importlib.import_module(module_name)
return getattr(module, class_name)
async def activate_step(self):
"""Initializes the step."""
# Instantiate an instance of the inner step object
step_cls = self._get_class_from_string(self.inner_step_type)
step_instance: KernelProcessStep = step_cls() # type: ignore
kernel_plugin = self.kernel.add_plugin(
step_instance, self.step_info.state.name if self.step_info.state else "default_name"
)
# Load the kernel functions
for name, f in kernel_plugin.functions.items():
self.functions[name] = f
# Initialize the input channels
self.initial_inputs = find_input_channels(channel=self, functions=self.functions)
self.inputs = {k: {kk: vv for kk, vv in v.items()} if v else {} for k, v in self.initial_inputs.items()}
# Use the existing state or create a new one if not provided
state_object = self.step_info.state
# Extract TState from inner_step_type
t_state = get_generic_state_type(step_cls)
if t_state is not None:
# Create state_type as KernelProcessStepState[TState]
state_type = KernelProcessStepState[t_state]
if state_object is None:
state_object = state_type(
name=step_cls.__name__,
id=step_cls.__name__,
state=None,
)
else:
# Make sure state_object is an instance of state_type
if not isinstance(state_object, KernelProcessStepState):
error_message = "State object is not of the expected type."
raise KernelException(error_message)
await self._state_manager.try_add_state(
ActorStateKeys.StepStateType.value,
get_fully_qualified_name(t_state),
)
await self._state_manager.try_add_state(
ActorStateKeys.StepStateJson.value,
json.dumps(state_object.model_dump()),
)
await self._state_manager.save_state()
# Make sure that state_object.state is not None
if state_object.state is None:
try:
state_object.state = t_state()
except Exception as e:
error_message = f"Cannot instantiate state of type {t_state}: {e}"
raise KernelException(error_message)
else:
# The step has no user-defined state; use the base KernelProcessStepState
state_type = KernelProcessStepState
if state_object is None:
state_object = state_type(
name=step_cls.__name__,
id=step_cls.__name__,
state=None,
)
if state_object is None:
error_message = "The state object for the KernelProcessStep could not be created."
raise KernelException(error_message)
# Set the step state and activate the step with the state object
self.step_state = state_object
await step_instance.activate(state_object)
async def handle_message(self, message: ProcessMessage):
"""Handles a LocalMessage that has been sent to the step."""
if message is None:
raise ValueError("The message is None.")
logger.info(f"Received message from `{message.source_id}` targeting function `{message.function_name}`.")
if not self.step_activated:
async with self.init_lock:
# Second check to ensure that initialization happens only once
# This avoids a race condition where multiple coroutines might
# reach the first check at the same time before any of them acquire the lock.
if not self.step_activated:
await self.activate_step()
self.step_activated = True
if self.functions is None or self.inputs is None or self.initial_inputs is None:
logger.error(f"The step `{self.name}` has not been initialized.")
raise ValueError("The step has not been initialized.")
message_log_parameters = ", ".join(f"{k}: {v}" for k, v in message.values.items())
logger.info(
f"Received message from `{message.source_id}` targeting function "
f"`{message.function_name}` and parameters `{message_log_parameters}`."
)
# Add the message values to the inputs for the function
for k, v in message.values.items():
if self.inputs.get(message.function_name) and self.inputs[message.function_name].get(k):
logger.info(
f"Step {self.name} already has input for `{message.function_name}.{k}`, "
f"it is being overwritten with a message from Step named `{message.source_id}`."
)
if message.function_name not in self.inputs:
self.inputs[message.function_name] = {}
self.inputs[message.function_name][k] = v
invocable_functions = [
k
for k, v in self.inputs.items()
if v is not None and (v == {} or all(val is not None for val in v.values()))
]
missing_keys = [
f"{outer_key}.{inner_key}"
for outer_key, outer_value in self.inputs.items()
for inner_key, inner_value in outer_value.items()
if inner_value is None
]
if not invocable_functions:
logger.info(f"No invocable functions, missing keys: {', '.join(missing_keys)}")
return
target_function = next((name for name in invocable_functions if name == message.function_name), None)
if not target_function:
raise ProcessTargetFunctionNameMismatchException(
f"A message targeting function `{message.function_name}` has resulted in a different function "
f"`{invocable_functions[0]}` becoming invocable. Check the function names."
)
logger.info(
f"Step with Id '{self.id}' received all required input for function [{target_function}] and is executing."
)
# Concatenate all inputs and run the function
arguments = self.inputs[target_function]
function = self.functions.get(target_function)
if function is None:
raise ProcessFunctionNotFoundException(f"Function {target_function} not found in plugin {self.name}")
invoke_result = None
event_name = None
event_value = None
try:
logger.info(
f"Invoking plugin `{function.plugin_name}` and function `{function.name}` with arguments: {arguments}"
)
invoke_result = await self.invoke_function(function, self.kernel, arguments)
event_name = f"{target_function}.OnResult"
event_value = invoke_result.value
if self.step_state is not None:
state_dict = self.step_state.model_dump()
await self._state_manager.set_state(ActorStateKeys.StepStateJson.value, json.dumps(state_dict))
await self._state_manager.save_state()
except Exception as ex:
logger.error(f"Error in Step {self.name}: {ex!s}")
event_name = f"{target_function}.OnError"
event_value = str(ex)
finally:
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()
async def invoke_function(self, function: "KernelFunction", kernel: "Kernel", arguments: dict[str, Any]):
"""Invokes the function."""
return await kernel.invoke(function, **arguments)
async def emit_event(self, process_event: KernelProcessEvent):
"""Emits an event from the step."""
if self.event_namespace is None:
raise ValueError("The event namespace must be initialized before emitting an event.")
await self.emit_process_event(ProcessEvent(inner_event=process_event, namespace=self.event_namespace))
async def emit_process_event(self, dapr_event: ProcessEvent):
"""Emits an event from the step."""
if dapr_event.visibility == KernelProcessEventVisibility.Public and self.parent_process_id is not None:
parent_process: EventBufferActor = ActorProxy.create( # type: ignore
actor_type=f"{EventBufferActor.__name__}",
actor_id=ActorId(self.parent_process_id),
actor_interface=EventBufferInterface,
)
await parent_process.enqueue(dapr_event.model_dump_json())
for edge in self.get_edge_for_event(dapr_event.id):
message: ProcessMessage = ProcessMessageFactory.create_from_edge(edge, dapr_event.data)
scoped_step_id = self._scoped_actor_id(ActorId(edge.output_target.step_id))
target_step: MessageBufferInterface = ActorProxy.create( # type: ignore
actor_type=f"{MessageBufferActor.__name__}",
actor_id=scoped_step_id,
actor_interface=MessageBufferInterface,
)
await target_step.enqueue(message.model_dump_json())
async def to_dapr_step_info(self) -> str:
"""Converts the step to a DaprStepInfo object."""
if not self.step_activated:
async with self.init_lock:
# Second check to ensure that initialization happens only once
# This avoids a race condition where multiple coroutines might
# reach the first check at the same time before any of them acquire the lock.
if not self.step_activated:
await self.activate_step()
self.step_activated = True
if self.step_info is None:
raise ValueError("The step must be initialized before converting to DaprStepInfo.")
if self.inner_step_type is None:
raise ValueError("The inner step type must be initialized before converting to DaprStepInfo.")
step_info = DaprStepInfo(
inner_step_python_type=self.inner_step_type, state=self.step_info.state, edges=self.step_info.edges
)
return step_info.model_dump_json()
async def _on_activate(self) -> None:
"""Override the Actor's on_activate method."""
try:
has_value, existing_step_info = await self._state_manager.try_get_state(ActorStateKeys.StepInfoState.value)
except Exception as ex:
logger.error(f"Error in Step {self.name}: {ex!s}")
raise ex
if has_value:
parent_process_id = await self._state_manager.get_state(ActorStateKeys.StepParentProcessId.value)
step_info = DaprStepInfo.model_validate(json.loads(existing_step_info)) # type: ignore
await self._int_initialize_step(step_info, parent_process_id=parent_process_id)
# Load persisted incoming messages
has_value, incoming_messages = await self._state_manager.try_get_state(
ActorStateKeys.StepIncomingMessagesState.value
)
if has_value:
messages = json.loads(incoming_messages) # type: ignore
for msg in messages:
process_message = ProcessMessage.model_validate(json.loads(msg))
self.incoming_messages.put(process_message)
def scoped_event(self, dapr_event: "ProcessEvent") -> "ProcessEvent":
"""Generates a scoped event for the step."""
if dapr_event is None:
raise ValueError("The Dapr event must be specified.")
dapr_event.namespace = f"{self.name}_{self.id.id}"
return dapr_event
def _scoped_actor_id(self, actor_id: ActorId) -> ActorId:
"""Generates a scoped actor ID for the step."""
return ActorId(f"{self.parent_process_id}.{actor_id.id}")
def get_edge_for_event(self, event_id: str) -> list["KernelProcessEdge"]:
"""Retrieves all edges that are associated with the provided event Id."""
if not self.output_edges:
return []
return self.output_edges.get(event_id, [])
@@ -0,0 +1,53 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import TYPE_CHECKING
from dapr.actor import ActorId
from dapr.actor.runtime.context import ActorRuntimeContext
from dapr.ext.fastapi import DaprActor as FastAPIDaprActor
from flask_dapr.actor import DaprActor as FlaskDaprActor
from semantic_kernel.processes.dapr_runtime import (
EventBufferActor,
ExternalEventBufferActor,
MessageBufferActor,
ProcessActor,
StepActor,
)
if TYPE_CHECKING:
from semantic_kernel.kernel import Kernel
def create_actor_factories(kernel: "Kernel") -> tuple:
"""Creates actor factories for ProcessActor and StepActor."""
def process_actor_factory(ctx: ActorRuntimeContext, actor_id: ActorId) -> ProcessActor:
return ProcessActor(ctx, actor_id, kernel)
def step_actor_factory(ctx: ActorRuntimeContext, actor_id: ActorId) -> StepActor:
return StepActor(ctx, actor_id, kernel=kernel)
return process_actor_factory, step_actor_factory
# Asynchronous registration for FastAPI
async def register_fastapi_dapr_actors(actor: FastAPIDaprActor, kernel: "Kernel") -> None:
"""Registers the actors with the Dapr runtime for use with a FastAPI app."""
process_actor_factory, step_actor_factory = create_actor_factories(kernel)
await actor.register_actor(ProcessActor, actor_factory=process_actor_factory)
await actor.register_actor(StepActor, actor_factory=step_actor_factory)
await actor.register_actor(EventBufferActor)
await actor.register_actor(MessageBufferActor)
await actor.register_actor(ExternalEventBufferActor)
# Synchronous registration for Flask
def register_flask_dapr_actors(actor: FlaskDaprActor, kernel: "Kernel") -> None:
"""Registers the actors with the Dapr runtime for use with a Flask app."""
process_actor_factory, step_actor_factory = create_actor_factories(kernel)
actor.register_actor(ProcessActor, actor_factory=process_actor_factory)
actor.register_actor(StepActor, actor_factory=step_actor_factory)
actor.register_actor(EventBufferActor)
actor.register_actor(MessageBufferActor)
actor.register_actor(ExternalEventBufferActor)
@@ -0,0 +1,42 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
from typing import TYPE_CHECKING
from semantic_kernel.exceptions.process_exceptions import ProcessInvalidConfigurationException
from semantic_kernel.processes.dapr_runtime.dapr_kernel_process_context import DaprKernelProcessContext
from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
from semantic_kernel.utils.experimental_decorator import experimental_function
if TYPE_CHECKING:
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
@experimental_function
async def start(
process: "KernelProcess",
initial_event: KernelProcessEvent | str | Enum,
process_id: str | None = None,
**kwargs,
) -> DaprKernelProcessContext:
"""Start the kernel process."""
if process is None:
raise ProcessInvalidConfigurationException("process cannot be None")
if process.state is None:
raise ProcessInvalidConfigurationException("process state cannot be empty")
if initial_event is None:
raise ProcessInvalidConfigurationException("initial_event cannot be None")
initial_event_str: str | KernelProcessEvent = (
initial_event.value if isinstance(initial_event, Enum) else initial_event
)
if isinstance(initial_event_str, str):
initial_event_str = KernelProcessEvent(id=initial_event_str, data=kwargs.get("data"))
if process_id is not None:
process.state.id = process_id
process_context = DaprKernelProcessContext(process=process)
await process_context.start_with_event(initial_event_str)
return process_context
@@ -0,0 +1,63 @@
# Copyright (c) Microsoft. All rights reserved.
import uuid
from dapr.actor import ActorId, ActorProxy
from semantic_kernel.processes.dapr_runtime.actors.process_actor import ProcessActor
from semantic_kernel.processes.dapr_runtime.dapr_process_info import DaprProcessInfo
from semantic_kernel.processes.dapr_runtime.interfaces.process_interface import ProcessInterface
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
from semantic_kernel.utils.experimental_decorator import experimental_class
@experimental_class
class DaprKernelProcessContext:
"""A Dapr kernel process context."""
dapr_process: ProcessInterface
process: KernelProcess
def __init__(self, process: KernelProcess):
"""Initialize a new instance of DaprKernelProcessContext."""
if process.state.name is None:
raise ValueError("Process state name must not be None")
if process.state.id is None or process.state.id == "":
process.state.id = str(uuid.uuid4().hex)
self.process = process
process_id = ActorId(process.state.id)
self.dapr_process = ActorProxy.create( # type: ignore
actor_type=f"{ProcessActor.__name__}",
actor_id=process_id,
actor_interface=ProcessInterface,
)
async def start_with_event(self, initial_event: KernelProcessEvent) -> None:
"""Starts the process with the provided initial event."""
dapr_process = DaprProcessInfo.from_kernel_process(self.process)
dapr_process_dict = dapr_process.model_dump_json()
payload = {
"process_info": dapr_process_dict,
"parent_process_id": None,
}
await self.dapr_process.initialize_process(payload)
initial_event_json = initial_event.model_dump_json()
await self.dapr_process.run_once(initial_event_json)
async def send_event(self, event: KernelProcessEvent) -> None:
"""Sends an event to the process."""
await self.dapr_process.send_message(event)
async def stop(self) -> None:
"""Stops the process."""
await self.dapr_process.stop()
async def get_state(self) -> KernelProcess:
"""Retrieves the current state of the process."""
dapr_process = await self.dapr_process.get_process_info()
return dapr_process.to_kernel_process()
@@ -0,0 +1,56 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Literal
from pydantic import Field
from semantic_kernel.processes.dapr_runtime.dapr_step_info import DaprStepInfo
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
from semantic_kernel.processes.kernel_process.kernel_process_state import KernelProcessState
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
from semantic_kernel.utils.experimental_decorator import experimental_class
@experimental_class
class DaprProcessInfo(DaprStepInfo):
"""A Dapr process info."""
type: Literal["DaprProcessInfo"] = Field("DaprProcessInfo") # type: ignore
steps: list["DaprStepInfo | DaprProcessInfo"] = Field(default_factory=list)
def to_kernel_process(self) -> KernelProcess:
"""Converts the Dapr process info to a kernel process."""
if not isinstance(self.state, KernelProcessState):
raise ValueError("State must be a kernel process state")
steps: list[KernelProcessStepInfo] = []
for step in self.steps:
if isinstance(step, DaprProcessInfo):
steps.append(step.to_kernel_process())
else:
steps.append(step.to_kernel_process_step_info())
return KernelProcess(state=self.state, steps=steps, edges=self.edges)
@classmethod
def from_kernel_process(cls, kernel_process: KernelProcess) -> "DaprProcessInfo":
"""Creates a Dapr process info from a kernel process."""
if kernel_process is None:
raise ValueError("Kernel process must be provided")
dapr_step_info = DaprStepInfo.from_kernel_step_info(kernel_process)
dapr_steps: list[DaprProcessInfo | DaprStepInfo] = []
for step in kernel_process.steps:
if isinstance(step, KernelProcess):
dapr_steps.append(DaprProcessInfo.from_kernel_process(step))
else:
dapr_steps.append(DaprStepInfo.from_kernel_step_info(step))
return DaprProcessInfo(
inner_step_python_type=dapr_step_info.inner_step_python_type,
state=dapr_step_info.state,
edges={key: list(value) for key, value in dapr_step_info.edges.items()},
steps=dapr_steps,
)
@@ -0,0 +1,53 @@
# Copyright (c) Microsoft. All rights reserved.
import importlib
from typing import Literal
from pydantic import Field
from semantic_kernel.exceptions.kernel_exceptions import KernelException
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.processes.kernel_process.kernel_process_edge import KernelProcessEdge
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
from semantic_kernel.processes.step_utils import get_fully_qualified_name
from semantic_kernel.utils.experimental_decorator import experimental_class
@experimental_class
class DaprStepInfo(KernelBaseModel):
"""A Dapr step info."""
type: Literal["DaprStepInfo"] = Field("DaprStepInfo")
inner_step_python_type: str
state: KernelProcessStepState
edges: dict[str, list[KernelProcessEdge]] = Field(default_factory=dict)
def to_kernel_process_step_info(self) -> KernelProcessStepInfo:
"""Converts the Dapr step info to a kernel process step info."""
inner_step_type = self._get_class_from_string(self.inner_step_python_type)
if inner_step_type is None:
raise KernelException(
f"Unable to create inner step type from assembly qualified name `{self.inner_step_python_type}`"
)
return KernelProcessStepInfo(inner_step_type=inner_step_type, state=self.state, output_edges=self.edges)
@classmethod
def from_kernel_step_info(cls, kernel_step_info: KernelProcessStepInfo) -> "DaprStepInfo":
"""Creates a Dapr step info from a kernel step info."""
if kernel_step_info is None:
raise KernelException("Kernel step info must be provided")
inner_step_type = get_fully_qualified_name(kernel_step_info.inner_step_type)
return DaprStepInfo(
inner_step_python_type=inner_step_type,
state=kernel_step_info.state,
edges={key: list(value) for key, value in kernel_step_info.edges.items()},
)
def _get_class_from_string(self, full_class_name: str):
"""Gets a class from a string."""
module_name, class_name = full_class_name.rsplit(".", 1)
module = importlib.import_module(module_name)
return getattr(module, class_name)
@@ -0,0 +1,32 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC, abstractmethod
from dapr.actor import ActorInterface, actormethod
from semantic_kernel.utils.experimental_decorator import experimental_class
@experimental_class
class EventBufferInterface(ActorInterface, ABC):
"""Abstract base class for an event buffer that follows the ActorInterface."""
@abstractmethod
@actormethod(name="enqueue")
async def enqueue(self, step_event: str) -> None:
"""Enqueues a `ProcessEvent` step event into the buffer.
Args:
step_event: The step event to enqueue.
"""
...
@abstractmethod
@actormethod(name="dequeue_all")
async def dequeue_all(self) -> list[str]:
"""Dequeues a step event from the buffer.
Returns:
The dequeued step event as a list of `ProcessEvent`.
"""
...
@@ -0,0 +1,35 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC, abstractmethod
from dapr.actor import ActorInterface, actormethod
from semantic_kernel.utils.experimental_decorator import experimental_class
@experimental_class
class ExternalEventBufferInterface(ActorInterface, ABC):
"""Abstract base class for an external event buffer that follows the ActorInterface."""
@abstractmethod
@actormethod(name="enqueue")
async def enqueue(self, external_event: str) -> None:
"""Enqueues an external event into the buffer.
Args:
external_event: The external event to enqueue.
"""
...
@abstractmethod
@actormethod(name="dequeue_all")
async def dequeue_all(self) -> list[str]:
"""Dequeues all external events from the buffer as.
The list is of string representations of KernelProcessEvent.
Returns:
The dequeued external event.
"""
...
@@ -0,0 +1,33 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC, abstractmethod
from dapr.actor import ActorInterface, actormethod
from semantic_kernel.utils.experimental_decorator import experimental_class
@experimental_class
class MessageBufferInterface(ActorInterface, ABC):
"""Abstract base class for a message event buffer that follows the ActorInterface."""
@abstractmethod
@actormethod(name="enqueue")
async def enqueue(self, message: str) -> None:
"""Enqueues a message event into the buffer.
Args:
message: The message event to enqueue.
"""
...
@abstractmethod
@actormethod(name="dequeue_all")
async def dequeue_all(self) -> list[str]:
"""Dequeues all process events from the buffer.
Returns:
The dequeued message event as a list of string
representing a ProcessEvent.
"""
...
@@ -0,0 +1,67 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC, abstractmethod
from dapr.actor import ActorInterface, actormethod
from semantic_kernel.processes.dapr_runtime.dapr_process_info import DaprProcessInfo
from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
from semantic_kernel.utils.experimental_decorator import experimental_class
@experimental_class
class ProcessInterface(ActorInterface, ABC):
"""Abstract base class for a process that follows the ActorInterface."""
@abstractmethod
@actormethod(name="initialize_process")
async def initialize_process(self, input: dict) -> None:
"""Initializes the process with the specified instance of DaprProcessInfo.
:param process_info: Used to initialize the process.
:param parent_process_id: The parent ID of the process if one exists.
"""
...
@abstractmethod
@actormethod(name="start")
async def start(self, keep_alive: bool) -> None:
"""Starts an initialized process.
:param keep_alive: Indicates if the process should wait for external events after it's finished processing.
"""
...
@abstractmethod
@actormethod(name="run_once")
async def run_once(self, process_event: KernelProcessEvent) -> None:
"""Starts the process with an initial event and then waits for the process to finish.
:param process_event: Required. The KernelProcessEvent to start the process with.
"""
...
@abstractmethod
@actormethod(name="stop")
async def stop(self) -> None:
"""Stops a running process, canceling and waiting for it to complete before returning."""
...
@abstractmethod
@actormethod(name="send_message")
async def send_message(self, process_event: "KernelProcessEvent") -> None:
"""Sends a message to the process without starting it if it is not already running.
:param process_event: Required. The KernelProcessEvent to queue for the process.
"""
...
@abstractmethod
@actormethod(name="get_process_info")
async def get_process_info(self) -> "DaprProcessInfo":
"""Retrieves the process information.
:return: An instance of DaprProcessInfo.
"""
...
@@ -0,0 +1,49 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC, abstractmethod
from dapr.actor import ActorInterface, actormethod
from semantic_kernel.utils.experimental_decorator import experimental_class
@experimental_class
class StepInterface(ActorInterface, ABC):
"""Abstract base class for a step in the process workflow."""
@abstractmethod
@actormethod(name="initialize_step")
async def initialize_step(self, input: str) -> None:
"""Initializes the step with the provided step information.
Args:
input: the DaprStepinfo and ParentProcessId dictionary as a str
"""
...
@abstractmethod
@actormethod(name="prepare_incoming_messages")
async def prepare_incoming_messages(self) -> int:
"""Triggers the step to dequeue all pending messages and prepare for processing.
Returns:
The number of messages that were dequeued.
"""
...
@abstractmethod
@actormethod(name="process_incoming_messages")
async def process_incoming_messages(self) -> None:
"""Triggers the step to process all prepared messages."""
...
@abstractmethod
@actormethod(name="to_dapr_step_info")
async def to_dapr_step_info(self) -> str:
"""Builds the current state of the step into a DaprStepInfo.
Returns:
The DaprStepInfo representing the current state of the step.
"""
...
@@ -0,0 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
from semantic_kernel.processes.kernel_process.kernel_process_event import (
KernelProcessEvent,
KernelProcessEventVisibility,
)
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
__all__ = [
"KernelProcess",
"KernelProcessEvent",
"KernelProcessEventVisibility",
"KernelProcessStep",
"KernelProcessStepContext",
"KernelProcessStepState",
]
@@ -3,6 +3,8 @@
from enum import Enum
from typing import Any
from pydantic import ConfigDict
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.utils.experimental_decorator import experimental_class
@@ -26,3 +28,5 @@ class KernelProcessEvent(KernelBaseModel):
id: str
data: Any | None = None
visibility: KernelProcessEventVisibility = KernelProcessEventVisibility.Internal
model_config = ConfigDict(use_enum_values=True)
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
from semantic_kernel.exceptions.process_exceptions import ProcessEventUndefinedException
from semantic_kernel.kernel_pydantic import KernelBaseModel
@@ -18,7 +19,7 @@ class KernelProcessStepContext(KernelBaseModel):
"""Initialize the step context."""
super().__init__(step_message_channel=channel)
async def emit_event(self, process_event: "KernelProcessEvent | str", **kwargs) -> None:
async def emit_event(self, process_event: "KernelProcessEvent | str | Enum", **kwargs) -> None:
"""Emit an event from the current step.
It is possible to either specify a `KernelProcessEvent` object or the ID of the event
@@ -33,6 +34,9 @@ class KernelProcessStepContext(KernelBaseModel):
if process_event is None:
raise ProcessEventUndefinedException("Process event cannot be None")
if isinstance(process_event, Enum):
process_event = process_event.value
if not isinstance(process_event, KernelProcessEvent):
process_event = KernelProcessEvent(id=process_event, **kwargs)
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
from typing import TYPE_CHECKING
from semantic_kernel.exceptions.process_exceptions import ProcessInvalidConfigurationException
@@ -14,7 +15,7 @@ if TYPE_CHECKING:
@experimental_function
async def start(
process: "KernelProcess", kernel: "Kernel", initial_event: KernelProcessEvent | str, **kwargs
process: "KernelProcess", kernel: "Kernel", initial_event: KernelProcessEvent | str | Enum, **kwargs
) -> LocalKernelProcessContext:
"""Start the kernel process."""
if process is None:
@@ -26,9 +27,13 @@ async def start(
if initial_event is None:
raise ProcessInvalidConfigurationException("initial_event cannot be None")
if isinstance(initial_event, str):
initial_event = KernelProcessEvent(id=initial_event, data=kwargs.get("data"))
initial_event_str: str | KernelProcessEvent = (
initial_event.value if isinstance(initial_event, Enum) else initial_event
)
if isinstance(initial_event_str, str):
initial_event_str = KernelProcessEvent(id=initial_event_str, data=kwargs.get("data"))
process_context = LocalKernelProcessContext(process, kernel)
await process_context.start_with_event(initial_event)
await process_context.start_with_event(initial_event_str)
return process_context
@@ -186,7 +186,7 @@ class LocalProcess(LocalStep):
await asyncio.gather(*message_tasks)
except Exception as ex:
print("An error occurred while running the process: %s." % ex)
logger.error(f"An error occurred while running the process: {ex}.")
raise
async def to_kernel_process(self) -> "KernelProcess":
@@ -20,12 +20,12 @@ from semantic_kernel.processes.kernel_process.kernel_process_edge import KernelP
from semantic_kernel.processes.kernel_process.kernel_process_event import KernelProcessEvent
from semantic_kernel.processes.kernel_process.kernel_process_message_channel import KernelProcessMessageChannel
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_info import KernelProcessStepInfo
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
from semantic_kernel.processes.local_runtime.local_event import LocalEvent
from semantic_kernel.processes.local_runtime.local_message import LocalMessage
from semantic_kernel.processes.process_types import get_generic_state_type
from semantic_kernel.processes.step_utils import find_input_channels
from semantic_kernel.utils.experimental_decorator import experimental_class
logger: logging.Logger = logging.getLogger(__name__)
@@ -197,7 +197,7 @@ class LocalStep(KernelProcessMessageChannel, KernelBaseModel):
self.functions[name] = f
# Initialize the input channels
self.initial_inputs = self.find_input_channels()
self.initial_inputs = find_input_channels(channel=self, functions=self.functions)
self.inputs = {k: {kk: vv for kk, vv in v.items()} if v else {} for k, v in self.initial_inputs.items()}
# Use the existing state or create a new one if not provided
@@ -248,27 +248,6 @@ class LocalStep(KernelProcessMessageChannel, KernelBaseModel):
self.step_state = state_object
await step_instance.activate(state_object)
def find_input_channels(self) -> dict[str, dict[str, Any | None]]:
"""Finds and creates input channels."""
if not self.functions:
raise ValueError("The step has not been initialized.")
inputs: dict[str, Any] = {}
for name, function in self.functions.items():
inputs[name] = {}
for param in function.metadata.parameters:
# Check for Kernel, and skip if necessary, since it is populated later on
if param.type_ == "Kernel":
continue
if not param.is_required:
continue
if param.type_ == "KernelProcessStepContext":
inputs[name][param.name] = KernelProcessStepContext(self)
else:
inputs[name][param.name] = None
return inputs
def get_all_events(self) -> list["LocalEvent"]:
"""Retrieves all events that have been emitted by this step in the previous superstep."""
all_events = []
@@ -3,6 +3,7 @@
import contextlib
import inspect
from copy import copy
from enum import Enum
from typing import TYPE_CHECKING
from pydantic import Field
@@ -73,23 +74,28 @@ class ProcessBuilder(ProcessStepBuilder):
return targets[0]
def where_input_event_is(self, event_id: str) -> "ProcessFunctionTargetBuilder":
def where_input_event_is(self, event_id: str | Enum) -> "ProcessFunctionTargetBuilder":
"""Filters the input event."""
if event_id not in self.external_event_target_map:
raise ValueError(f"The process named '{self.name}' does not expose an event with Id '{event_id}'")
event_id_str: str = event_id.value if isinstance(event_id, Enum) else event_id
target = self.external_event_target_map[event_id]
if event_id_str not in self.external_event_target_map:
raise ValueError(f"The process named '{self.name}' does not expose an event with Id '{event_id_str}'")
target = self.external_event_target_map[event_id_str]
target = copy(target)
target.step = self
target.target_event_id = event_id
target.target_event_id = event_id_str
return target
def on_input_event(self, event_id: str) -> "ProcessEdgeBuilder": # type: ignore
def on_input_event(self, event_id: str | Enum) -> "ProcessEdgeBuilder": # type: ignore
"""Creates a new ProcessEdgeBuilder for the input event."""
from semantic_kernel.processes.process_builder import ProcessBuilder # noqa: F401
ProcessEdgeBuilder.model_rebuild()
return ProcessEdgeBuilder(source=self, event_id=event_id)
event_id_str: str = event_id.value if isinstance(event_id, Enum) else event_id
return ProcessEdgeBuilder(source=self, event_id=event_id_str)
def link_to(self, event_id: str, edge_builder: ProcessStepEdgeBuilder) -> None:
"""Links to the given event ID."""
@@ -0,0 +1,31 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.processes.kernel_process.kernel_process_event import (
KernelProcessEvent,
KernelProcessEventVisibility,
)
class ProcessEvent(KernelBaseModel):
"""A wrapper around KernelProcessEvent that helps to manage the namespace of the event."""
namespace: str | None = None
inner_event: KernelProcessEvent
@property
def id(self) -> str:
"""The Id of the event."""
return f"{self.namespace}.{self.inner_event.id}"
@property
def data(self) -> Any | None:
"""The data of the event."""
return self.inner_event.data
@property
def visibility(self) -> "KernelProcessEventVisibility":
"""The visibility of the event."""
return self.inner_event.visibility
@@ -1,5 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.processes.kernel_process.kernel_process_function_target import KernelProcessFunctionTarget
from semantic_kernel.processes.process_end_step import EndStep
@@ -16,21 +18,25 @@ class ProcessFunctionTargetBuilder(KernelBaseModel):
parameter_name: str | None = None
target_event_id: str | None = None
def __init__(self, step: ProcessStepBuilder, function_name: str | None = None, parameter_name: str | None = None):
def __init__(
self, step: ProcessStepBuilder, function_name: str | Enum | None = None, parameter_name: str | None = None
):
"""Initializes a new instance of ProcessFunctionTargetBuilder."""
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata # noqa: F401
ProcessFunctionTargetBuilder.model_rebuild()
function_name_str: str | None = function_name.value if isinstance(function_name, Enum) else function_name
if isinstance(step, EndStep):
function_name = "END"
function_name_str = "END"
parameter_name = None
else:
target = step.resolve_function_target(function_name, parameter_name)
function_name = target.function_name
target = step.resolve_function_target(function_name_str, parameter_name)
function_name_str = target.function_name
parameter_name = target.parameter_name
super().__init__(step=step, function_name=function_name, parameter_name=parameter_name)
super().__init__(step=step, function_name=function_name_str, parameter_name=parameter_name)
def build(self) -> KernelProcessFunctionTarget:
"""Builds the KernelProcessFunctionTarget."""
@@ -0,0 +1,17 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.kernel_pydantic import KernelBaseModel
class ProcessMessage(KernelBaseModel):
"""Represents a message used in a process runtime."""
source_id: str
destination_id: str
function_name: str
values: dict[str, Any]
target_event_id: str | None = None
target_event_data: Any | None = None
@@ -0,0 +1,27 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.processes.kernel_process.kernel_process_edge import KernelProcessEdge
from semantic_kernel.processes.process_message import ProcessMessage
class ProcessMessageFactory:
"""Factory class for creating ProcessMessage instances."""
@staticmethod
def create_from_edge(edge: KernelProcessEdge, data: Any) -> ProcessMessage:
"""Creates a new ProcessMessage from a KernelProcessEdge."""
target = edge.output_target
parameter_value: dict[str, Any] = {}
if target.parameter_name is not None:
parameter_value[target.parameter_name] = data
return ProcessMessage(
source_id=edge.source_step_id,
destination_id=target.step_id,
function_name=target.function_name,
values=parameter_value,
target_event_id=target.target_event_id,
target_event_data=data,
)
@@ -2,6 +2,7 @@
import logging
import uuid
from enum import Enum
from typing import TYPE_CHECKING, Any, Generic
from pydantic import Field
@@ -65,17 +66,21 @@ class ProcessStepBuilder(KernelBaseModel, Generic[TState, TStep]):
**kwargs,
)
def on_input_event(self, event_id: str) -> "ProcessStepEdgeBuilder":
def on_input_event(self, event_id: str | Enum) -> "ProcessStepEdgeBuilder":
"""Creates a new ProcessStepEdgeBuilder for the input event."""
from semantic_kernel.processes.process_step_edge_builder import ProcessStepEdgeBuilder
return ProcessStepEdgeBuilder(source=self, event_id=event_id)
event_id_str: str = event_id.value if isinstance(event_id, Enum) else event_id
def on_event(self, event_id: str) -> "ProcessStepEdgeBuilder":
return ProcessStepEdgeBuilder(source=self, event_id=event_id_str)
def on_event(self, event_id: str | Enum) -> "ProcessStepEdgeBuilder":
"""Creates a new ProcessStepEdgeBuilder for the event."""
from semantic_kernel.processes.process_step_edge_builder import ProcessStepEdgeBuilder
scoped_event_id = self.get_scoped_event_id(event_id)
event_id_str: str = event_id.value if isinstance(event_id, Enum) else event_id
scoped_event_id = self.get_scoped_event_id(event_id_str)
return ProcessStepEdgeBuilder(source=self, event_id=scoped_event_id)
def resolve_function_target(
@@ -166,11 +171,8 @@ class ProcessStepBuilder(KernelBaseModel, Generic[TState, TStep]):
# Create state_object as KernelProcessStepState[TState]
state_type = KernelProcessStepState[t_state] # type: ignore
state_object = state_type(
name=self.name,
id=self.id,
state=self.initial_state, # Can be None
)
initial_state = self.initial_state or t_state()
state_object = state_type(name=self.name, id=self.id, state=initial_state)
else:
# The step has no user-defined state; use the base KernelProcessStepState
if self.initial_state is not None:
@@ -0,0 +1,36 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.processes.kernel_process.kernel_process_message_channel import KernelProcessMessageChannel
from semantic_kernel.processes.kernel_process.kernel_process_step_context import KernelProcessStepContext
def find_input_channels(
channel: KernelProcessMessageChannel, functions: dict[str, KernelFunction]
) -> dict[str, dict[str, Any | None]]:
"""Finds and creates input channels."""
if not functions:
raise ValueError("The step has not been initialized.")
inputs: dict[str, Any] = {}
for name, function in functions.items():
inputs[name] = {}
for param in function.metadata.parameters:
# Check for Kernel, and skip if necessary, since it is populated later on
if param.type_ == "Kernel":
continue
if not param.is_required:
continue
if param.type_ == "KernelProcessStepContext":
inputs[name][param.name] = KernelProcessStepContext(channel)
else:
inputs[name][param.name] = None
return inputs
def get_fully_qualified_name(cls) -> str:
"""Gets the fully qualified name of a class."""
return f"{cls.__module__}.{cls.__name__}"
@@ -0,0 +1,89 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
import pytest
from semantic_kernel.exceptions.process_exceptions import ProcessInvalidConfigurationException
from semantic_kernel.kernel import Kernel
from semantic_kernel.processes.dapr_runtime.dapr_kernel_process import start
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.kernel_process.kernel_process_state import KernelProcessState
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
class FakeDaprKernelProcessContext:
def __init__(self, process):
self.process = process
self.start_with_event = AsyncMock()
@pytest.mark.asyncio
async def test_start_with_valid_parameters():
state = MagicMock(spec=KernelProcessState)
state.name = "valid_state"
state.id = "state_1"
mock_step = MagicMock(spec=KernelProcessStepInfo)
process = KernelProcess(state=state, steps=[mock_step])
kernel = MagicMock(spec=Kernel)
initial_event = KernelProcessEvent(id="event_1", data="data_1")
with patch(
"semantic_kernel.processes.dapr_runtime.dapr_kernel_process.DaprKernelProcessContext",
new=FakeDaprKernelProcessContext,
):
result = await start(process=process, kernel=kernel, initial_event=initial_event)
assert isinstance(result, FakeDaprKernelProcessContext)
assert result.process == process
result.start_with_event.assert_called_once_with(initial_event)
@pytest.mark.asyncio
async def test_start_with_invalid_process():
kernel = MagicMock(spec=Kernel)
initial_event = KernelProcessEvent(id="event_1", data="data_1")
with pytest.raises(ProcessInvalidConfigurationException, match="process cannot be None"):
await start(process=None, kernel=kernel, initial_event=initial_event)
@pytest.mark.asyncio
async def test_start_with_invalid_initial_event():
state = MagicMock(spec=KernelProcessState)
type(state).name = PropertyMock(return_value="valid_state")
mock_step = MagicMock(spec=KernelProcessStepInfo)
process = KernelProcess(state=state, steps=[mock_step])
kernel = MagicMock(spec=Kernel)
with pytest.raises(ProcessInvalidConfigurationException, match="initial_event cannot be None"):
await start(process=process, kernel=kernel, initial_event=None)
@pytest.mark.asyncio
async def test_start_with_initial_event_as_string():
state = MagicMock(spec=KernelProcessState)
type(state).name = PropertyMock(return_value="valid_state")
mock_step = MagicMock(spec=KernelProcessStepInfo)
process = KernelProcess(state=state, steps=[mock_step])
kernel = MagicMock(spec=Kernel)
initial_event = "start_event"
with patch(
"semantic_kernel.processes.dapr_runtime.dapr_kernel_process.DaprKernelProcessContext", new=MagicMock()
) as mock_process_context_class:
mock_process_context_instance = mock_process_context_class.return_value
mock_process_context_instance.start_with_event = AsyncMock()
result = await start(process=process, kernel=kernel, initial_event=initial_event, data="event_data")
assert result == mock_process_context_instance
mock_process_context_instance.start_with_event.assert_called_once()
args, _ = mock_process_context_instance.start_with_event.call_args
actual_event = args[0]
assert isinstance(actual_event, KernelProcessEvent)
assert actual_event.id == "start_event"
assert actual_event.data == "event_data"
@@ -0,0 +1,98 @@
# Copyright (c) Microsoft. All rights reserved.
import uuid
from unittest.mock import AsyncMock, patch
import pytest
from semantic_kernel.processes.dapr_runtime.dapr_kernel_process_context import DaprKernelProcessContext
from semantic_kernel.processes.dapr_runtime.dapr_process_info import DaprProcessInfo
from semantic_kernel.processes.dapr_runtime.interfaces.process_interface import ProcessInterface
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.kernel_process.kernel_process_state import KernelProcessState
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
class DummyInnerStepType:
pass
@pytest.fixture
def process_context():
state = KernelProcessState(name="TestProcess", id=str(uuid.uuid4()))
step_state = KernelProcessStepState(name="TestStep", id="step1")
step = KernelProcessStepInfo(
state=step_state,
inner_step_type=DummyInnerStepType,
output_edges={},
)
steps = [step]
process = KernelProcess(state=state, steps=steps)
with patch("dapr.actor.ActorProxy.create") as mock_actor_proxy_create:
mock_dapr_process = AsyncMock(spec=ProcessInterface)
mock_actor_proxy_create.return_value = mock_dapr_process
context = DaprKernelProcessContext(process=process)
yield context, mock_dapr_process
@pytest.mark.asyncio
async def test_start_with_event(process_context):
context, mock_dapr_process = process_context
initial_event = KernelProcessEvent(id="event1", data="some data")
await context.start_with_event(initial_event)
dapr_process_info = DaprProcessInfo.from_kernel_process(context.process)
expected_payload = {
"process_info": dapr_process_info.model_dump_json(),
"parent_process_id": None,
}
mock_dapr_process.initialize_process.assert_awaited_once_with(expected_payload)
initial_event_json = initial_event.model_dump_json()
mock_dapr_process.run_once.assert_awaited_once_with(initial_event_json)
@pytest.mark.asyncio
async def test_send_event(process_context):
context, mock_dapr_process = process_context
event = KernelProcessEvent(id="event2", data="event data")
await context.send_event(event)
mock_dapr_process.send_message.assert_awaited_once_with(event)
@pytest.mark.asyncio
async def test_stop(process_context):
context, mock_dapr_process = process_context
await context.stop()
mock_dapr_process.stop.assert_awaited_once()
@pytest.mark.asyncio
async def test_get_state(process_context):
context, mock_dapr_process = process_context
dapr_process_info = DaprProcessInfo.from_kernel_process(context.process)
mock_dapr_process.get_process_info.return_value = dapr_process_info
result = await context.get_state()
mock_dapr_process.get_process_info.assert_awaited_once()
assert isinstance(result, KernelProcess)
assert result.state.id == context.process.state.id
assert result.state.name == context.process.state.name
@@ -0,0 +1,64 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from unittest.mock import AsyncMock
import pytest
from dapr.actor.id import ActorId
from dapr.actor.runtime._type_information import ActorTypeInformation
from dapr.actor.runtime.context import ActorRuntimeContext
from dapr.serializers import DefaultJSONSerializer
from semantic_kernel.processes.dapr_runtime.actors.event_buffer_actor import EventBufferActor
def _run(coro):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(coro)
@pytest.fixture
def actor_context():
actor_id = ActorId("test_actor")
actor_type_info = ActorTypeInformation.create(EventBufferActor)
message_serializer = DefaultJSONSerializer()
state_serializer = DefaultJSONSerializer()
mock_actor_client = AsyncMock()
runtime_context = ActorRuntimeContext(
actor_type_info=actor_type_info,
message_serializer=message_serializer,
state_serializer=state_serializer,
actor_client=mock_actor_client,
)
actor = EventBufferActor(runtime_context, actor_id)
_run(actor._on_activate())
return actor
def test_enqueue(actor_context):
_run(actor_context.enqueue('{"event": "test_event"}'))
queue_list = list(actor_context.queue.queue)
assert queue_list == ['{"event": "test_event"}']
def test_dequeue_all(actor_context):
_run(actor_context.enqueue('{"event": "event1"}'))
_run(actor_context.enqueue('{"event": "event2"}'))
messages = _run(actor_context.dequeue_all())
assert messages == ['{"event": "event1"}', '{"event": "event2"}']
assert actor_context.queue.empty()
def test_on_activate(actor_context):
new_actor = EventBufferActor(actor_context.runtime_ctx, actor_context.id)
_run(new_actor._on_activate())
assert new_actor.queue.empty()
@@ -0,0 +1,100 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
from unittest.mock import AsyncMock
import pytest
from dapr.actor.id import ActorId
from dapr.actor.runtime._type_information import ActorTypeInformation
from dapr.actor.runtime.context import ActorRuntimeContext
from dapr.serializers import DefaultJSONSerializer
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.actors.external_event_buffer_actor import ExternalEventBufferActor
def _run(coro):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(coro)
@pytest.fixture
def actor_context():
actor_id = ActorId("test_actor")
actor_type_info = ActorTypeInformation.create(ExternalEventBufferActor)
message_serializer = DefaultJSONSerializer()
state_serializer = DefaultJSONSerializer()
mock_actor_client = AsyncMock()
mock_state_manager = AsyncMock()
mock_state_manager.try_get_state.return_value = (True, json.dumps([]))
mock_state_manager.try_add_state = AsyncMock()
mock_state_manager.save_state = AsyncMock()
runtime_context = ActorRuntimeContext(
actor_type_info=actor_type_info,
message_serializer=message_serializer,
state_serializer=state_serializer,
actor_client=mock_actor_client,
)
actor = ExternalEventBufferActor(runtime_context, actor_id)
actor._state_manager = mock_state_manager
_run(actor._on_activate())
return actor
def test_enqueue(actor_context):
_run(actor_context.enqueue('{"event": "test_event"}'))
queue_list = list(actor_context.queue.queue)
assert queue_list == ['{"event": "test_event"}']
actor_context._state_manager.try_add_state.assert_called_with(
ActorStateKeys.ExternalEventQueueState.value, json.dumps(['{"event": "test_event"}'])
)
actor_context._state_manager.save_state.assert_called_once()
def test_dequeue_all(actor_context):
_run(actor_context.enqueue('{"event": "event1"}'))
_run(actor_context.enqueue('{"event": "event2"}'))
messages = _run(actor_context.dequeue_all())
assert messages == ['{"event": "event1"}', '{"event": "event2"}']
assert actor_context.queue.empty()
actor_context._state_manager.try_add_state.assert_called_with(
ActorStateKeys.ExternalEventQueueState.value, json.dumps([])
)
actor_context._state_manager.save_state.assert_called()
def test_on_activate_with_existing_state(actor_context):
actor_context._state_manager.try_get_state.return_value = (
True,
json.dumps(['{"event": "event1"}', '{"event": "event2"}']),
)
new_actor = ExternalEventBufferActor(actor_context.runtime_ctx, actor_context.id)
new_actor._state_manager = actor_context._state_manager
_run(new_actor._on_activate())
queue_list = list(new_actor.queue.queue)
assert queue_list == ['{"event": "event1"}', '{"event": "event2"}']
def test_on_activate_with_no_existing_state(actor_context):
actor_context._state_manager.try_get_state.return_value = (False, None)
new_actor = ExternalEventBufferActor(actor_context.runtime_ctx, actor_context.id)
new_actor._state_manager = actor_context._state_manager
_run(new_actor._on_activate())
assert new_actor.queue.empty()
@@ -0,0 +1,97 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from unittest.mock import AsyncMock
import pytest
from dapr.actor.id import ActorId
from dapr.actor.runtime._type_information import ActorTypeInformation
from dapr.actor.runtime.context import ActorRuntimeContext
from dapr.serializers import DefaultJSONSerializer
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.actors.message_buffer_actor import MessageBufferActor
def _run(coro):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(coro)
@pytest.fixture
def actor_context():
actor_id = ActorId("test_actor")
actor_type_info = ActorTypeInformation.create(MessageBufferActor)
message_serializer = DefaultJSONSerializer()
state_serializer = DefaultJSONSerializer()
mock_actor_client = AsyncMock()
mock_state_manager = AsyncMock()
mock_state_manager.try_get_state.return_value = (True, [])
mock_state_manager.try_add_state = AsyncMock()
mock_state_manager.save_state = AsyncMock()
runtime_context = ActorRuntimeContext(
actor_type_info=actor_type_info,
message_serializer=message_serializer,
state_serializer=state_serializer,
actor_client=mock_actor_client,
)
actor = MessageBufferActor(runtime_context, actor_id)
actor._state_manager = mock_state_manager
_run(actor._on_activate())
return actor
def test_enqueue(actor_context):
_run(actor_context.enqueue('{"event": "test_event"}'))
queue_list = list(actor_context.queue.queue)
assert queue_list == ['{"event": "test_event"}']
actor_context._state_manager.try_add_state.assert_called_with(
ActorStateKeys.MessageQueueState.value, ['{"event": "test_event"}']
)
actor_context._state_manager.save_state.assert_called_once()
def test_dequeue_all(actor_context):
_run(actor_context.enqueue('{"event": "event1"}'))
_run(actor_context.enqueue('{"event": "event2"}'))
actor_context._state_manager.reset_mock()
messages = _run(actor_context.dequeue_all())
assert messages == ['{"event": "event1"}', '{"event": "event2"}']
assert actor_context.queue.empty()
actor_context._state_manager.try_add_state.assert_called_with(ActorStateKeys.MessageQueueState.value, "[]")
actor_context._state_manager.save_state.assert_called_once()
def test_on_activate_with_existing_state(actor_context):
actor_context._state_manager.try_get_state.return_value = (True, ['{"event": "event1"}', '{"event": "event2"}'])
new_actor = MessageBufferActor(actor_context.runtime_ctx, actor_context.id)
new_actor._state_manager = actor_context._state_manager
_run(new_actor._on_activate())
queue_list = list(new_actor.queue.queue)
assert queue_list == ['{"event": "event1"}', '{"event": "event2"}']
def test_on_activate_with_no_existing_state(actor_context):
actor_context._state_manager.try_get_state.return_value = (False, None)
new_actor = MessageBufferActor(actor_context.runtime_ctx, actor_context.id)
new_actor._state_manager = actor_context._state_manager
_run(new_actor._on_activate())
assert new_actor.queue.empty()
@@ -0,0 +1,164 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from dapr.actor.id import ActorId
from dapr.actor.runtime._type_information import ActorTypeInformation
from dapr.actor.runtime.context import ActorRuntimeContext
from semantic_kernel.processes.dapr_runtime.actors.actor_state_key import ActorStateKeys
from semantic_kernel.processes.dapr_runtime.actors.process_actor import ProcessActor
from semantic_kernel.processes.dapr_runtime.dapr_process_info import DaprProcessInfo
from semantic_kernel.processes.dapr_runtime.dapr_step_info import DaprStepInfo
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
@pytest.fixture
def actor_context():
actor_id = ActorId("test_actor")
actor_type_info = ActorTypeInformation.create(ProcessActor)
runtime_context = ActorRuntimeContext(
actor_type_info=actor_type_info,
message_serializer=MagicMock(),
state_serializer=MagicMock(),
actor_client=MagicMock(),
)
kernel_mock = MagicMock()
actor = ProcessActor(runtime_context, actor_id, kernel=kernel_mock)
actor._state_manager = AsyncMock()
actor._state_manager.try_add_state = AsyncMock(return_value=True)
actor._state_manager.try_get_state = AsyncMock(return_value=(True, {}))
actor._state_manager.save_state = AsyncMock()
asyncio.run(actor._on_activate())
return actor
def clean_structure(data):
"""Recursively remove null values and empty dictionaries for direct comparison."""
if isinstance(data, dict):
return {k: clean_structure(v) for k, v in data.items() if v not in [None, {}]}
if isinstance(data, list):
return [clean_structure(item) for item in data]
return data
@pytest.mark.asyncio
async def test_initialize_process(actor_context):
input_data = {
"process_info": {
"type": "DaprProcessInfo",
"state": {"name": "Test Process", "id": "proc_123"},
"steps": [
{
"state": {"name": "Step1", "id": "step_123"},
"inner_step_python_type": "SomeStepType",
"type": "DaprStepInfo",
}
],
"inner_step_python_type": "SomeProcessType",
"edges": {},
},
"parent_process_id": "parent_123",
}
expected_process_info = clean_structure(input_data["process_info"])
dapr_process_info_instance = DaprProcessInfo(
inner_step_python_type="SomeProcessType",
state=KernelProcessStepState(name="Test Process", id="proc_123"),
edges={},
steps=[
DaprStepInfo(
inner_step_python_type="SomeStepType",
state=KernelProcessStepState(name="Step1", id="step_123"),
edges={},
)
],
)
with (
patch.multiple(
actor_context,
_initialize_process_actor=AsyncMock(),
_state_manager=actor_context._state_manager,
),
patch.object(actor_context._state_manager, "save_state", new=AsyncMock()) as mock_save_state,
):
await actor_context.initialize_process(input_data)
actual_calls = actor_context._state_manager.try_add_state.call_args_list
actual_process_info_call = next(
(call for call in actual_calls if call[0][0] == ActorStateKeys.ProcessInfoState.value), None
)
assert actual_process_info_call is not None, "ProcessInfoState call was not found."
actual_process_info = clean_structure(actual_process_info_call[0][1])
assert actual_process_info == expected_process_info, (
f"Expected: {json.dumps(expected_process_info)}, but got: {json.dumps(actual_process_info)}"
)
mock_save_state.assert_called_once()
actor_context._initialize_process_actor.assert_called_once_with(dapr_process_info_instance, "parent_123")
@pytest.mark.asyncio
async def test_start_process(actor_context):
actor_context.initialize_task = True
with patch.object(actor_context, "internal_execute", new=AsyncMock()) as mock_internal_execute:
await actor_context.start(keep_alive=False)
assert actor_context.process_task is not None
mock_internal_execute.assert_called_once()
assert not actor_context.process_task.done()
def test_run_once(actor_context):
actor_context.initialize_task = True
process_event = '{"event": "test_event"}'
with patch(
"semantic_kernel.processes.dapr_runtime.actors.process_actor.ActorProxy.create", return_value=AsyncMock()
) as mock_proxy:
asyncio.run(actor_context.run_once(process_event))
external_event_queue = mock_proxy.return_value
external_event_queue.enqueue.assert_called_once_with(process_event)
assert actor_context.process_task is not None
@pytest.mark.asyncio
async def test_stop(actor_context):
actor_context.initialize_task = True
actor_context.process_task = asyncio.create_task(asyncio.sleep(1))
await actor_context.stop()
assert actor_context.process_task.done()
def test_get_process_info(actor_context):
with patch.object(actor_context, "to_dapr_process_info", return_value=MagicMock()) as mock_to_dapr_process_info:
process_info = asyncio.run(actor_context.get_process_info())
mock_to_dapr_process_info.assert_called_once()
assert process_info is mock_to_dapr_process_info.return_value
def test_handle_message(actor_context):
message_mock = MagicMock()
message_mock.target_event_id = "test_event"
actor_context.output_edges = {"test_event": [MagicMock()]}
with patch.object(actor_context, "run_once", new=AsyncMock()) as mock_run_once:
asyncio.run(actor_context.handle_message(message_mock))
mock_run_once.assert_called_once()
@@ -0,0 +1,102 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from dapr.actor import ActorId
from semantic_kernel.processes.dapr_runtime.actors.step_actor import StepActor
from semantic_kernel.processes.dapr_runtime.dapr_step_info import DaprStepInfo
from semantic_kernel.processes.kernel_process.kernel_process_step_state import KernelProcessStepState
from semantic_kernel.processes.process_message import ProcessMessage
@pytest.fixture
def actor_context():
ctx = MagicMock()
actor_id = ActorId("test_actor")
kernel = MagicMock()
return StepActor(ctx, actor_id, kernel)
@pytest.mark.asyncio
async def test_initialize_step(actor_context):
input_data = json.dumps({
"step_info": {
"state": {"name": "TestStep", "id": "step_1"},
"inner_step_python_type": "SomeStepType",
"edges": {},
},
"parent_process_id": "parent_1",
})
with (
patch.object(actor_context._state_manager, "try_add_state", new=AsyncMock()) as mock_try_add_state,
patch.object(actor_context._state_manager, "save_state", new=AsyncMock()) as mock_save_state,
):
await actor_context.initialize_step(input_data)
assert actor_context.step_info is not None
mock_try_add_state.assert_any_call("DaprStepInfo", actor_context.step_info)
mock_save_state.assert_called_once()
@pytest.mark.asyncio
async def test_prepare_incoming_messages(actor_context):
message = ProcessMessage(
source_id="source_1",
destination_id="dest_1",
function_name="test_function",
values={"param1": "value1"},
)
mock_message_json = json.dumps(message.model_dump())
expected_state_key = "incomingMessagesState"
with (
patch("dapr.actor.ActorProxy.create", new=MagicMock()) as mock_actor_proxy,
patch.object(actor_context._state_manager, "try_add_state", new=AsyncMock()) as mock_try_add_state,
patch.object(actor_context._state_manager, "save_state", new=AsyncMock()) as mock_save_state,
):
mock_queue = AsyncMock()
mock_queue.dequeue_all.return_value = [mock_message_json]
mock_actor_proxy.return_value = mock_queue
incoming_message_count = await actor_context.prepare_incoming_messages()
assert incoming_message_count == 1
assert actor_context.incoming_messages.qsize() == 1
mock_try_add_state.assert_called_with(expected_state_key, [mock_message_json])
mock_save_state.assert_called_once()
@pytest.mark.asyncio
async def test_process_incoming_messages(actor_context):
actor_context.step_info = DaprStepInfo(
state=KernelProcessStepState(name="Test Step", id="step_123"),
inner_step_python_type="SomeStepType",
edges={},
)
message = ProcessMessage(
source_id="source_1",
destination_id="dest_1",
function_name="test_function",
values={"param1": "value1"},
)
actor_context.incoming_messages.put(message)
with (
patch.object(actor_context, "handle_message", new=AsyncMock()) as mock_handle_message,
patch.object(actor_context._state_manager, "save_state", new=AsyncMock()) as mock_save_state,
patch.object(
actor_context._state_manager, "try_add_state", new=AsyncMock(return_value=True)
) as mock_try_add_state,
):
await actor_context.process_incoming_messages()
mock_handle_message.assert_called_once_with(message)
mock_save_state.assert_called_once()
expected_messages = []
expected_messages = [json.dumps(msg.model_dump()) for msg in list(actor_context.incoming_messages.queue)]
mock_try_add_state.assert_any_call("incomingMessagesState", expected_messages)
@@ -34,7 +34,7 @@ def test_initialization_with_visibility():
# Assert
assert event.id == event_id
assert event.data == event_data
assert event.visibility == KernelProcessEventVisibility.Public
assert event.visibility == KernelProcessEventVisibility.Public.value
def test_invalid_visibility():
@@ -321,7 +321,7 @@ async def test_handle_message_with_valid_event_id(mock_process_with_output_edges
assert isinstance(event, KernelProcessEvent)
assert event.id == "valid_event_id"
assert event.data == message.target_event_data
assert event.visibility == KernelProcessEventVisibility.Internal
assert event.visibility == KernelProcessEventVisibility.Internal.value
END_PROCESS_ID = "END"
@@ -0,0 +1,18 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.processes.kernel_process.kernel_process_edge import KernelProcessEdge
from semantic_kernel.processes.kernel_process.kernel_process_function_target import KernelProcessFunctionTarget
from semantic_kernel.processes.process_message_factory import ProcessMessageFactory
def test_create_from_edge():
"""Test initialization of KernelProcessEdge with valid input."""
source_step_id = "step_1"
output_target = KernelProcessFunctionTarget(
step_id="step_2", function_name="process_data", parameter_name="input_data", target_event_id="event_1"
)
edge = KernelProcessEdge(source_step_id=source_step_id, output_target=output_target)
process_message = ProcessMessageFactory.create_from_edge(edge, "data")
assert process_message.source_id == source_step_id
@@ -0,0 +1,133 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import Callable
from typing import Any
from unittest.mock import MagicMock
import pytest
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel
from semantic_kernel.processes.kernel_process.kernel_process_message_channel import (
KernelProcessMessageChannel,
)
from semantic_kernel.processes.kernel_process.kernel_process_step_context import (
KernelProcessStepContext,
)
from semantic_kernel.processes.step_utils import find_input_channels, get_fully_qualified_name
@pytest.fixture
def mock_function() -> Callable[..., Any]:
@kernel_function
def mock_function(input: str, context: KernelProcessStepContext) -> None:
pass
return mock_function
def test_find_input_channels_empty_functions():
channel = MagicMock(spec=KernelProcessMessageChannel)
functions = {}
with pytest.raises(ValueError) as exc_info:
find_input_channels(channel, functions)
assert str(exc_info.value) == "The step has not been initialized."
def test_find_input_channels_with_required_params(mock_function):
channel = MagicMock(spec=KernelProcessMessageChannel)
function = KernelFunction.from_method(method=mock_function, plugin_name="test")
functions = {"TestFunction": function}
inputs = find_input_channels(channel, functions)
assert "TestFunction" in inputs
assert "input" in inputs["TestFunction"]
assert "context" in inputs["TestFunction"]
@kernel_function
def skipped_function(param1: "Kernel", param2: int = 0) -> None:
pass
def test_find_input_channels_with_only_skipped_params():
channel = MagicMock(spec=KernelProcessMessageChannel)
function = KernelFunction.from_method(method=skipped_function, plugin_name="test")
functions = {"SkippedFunction": function}
inputs = find_input_channels(channel, functions)
expected_inputs = {"SkippedFunction": {}}
assert inputs == expected_inputs
@kernel_function
def function1(param1: str) -> None:
pass
@kernel_function
def function2(param2: KernelProcessStepContext, param3: float = 0.0) -> None:
pass
def test_find_input_channels_multiple_functions():
channel = MagicMock(spec=KernelProcessMessageChannel)
function1_instance = KernelFunction.from_method(method=function1, plugin_name="test")
function2_instance = KernelFunction.from_method(method=function2, plugin_name="test")
functions = {
"Function1": function1_instance,
"Function2": function2_instance,
}
inputs = find_input_channels(channel, functions)
assert "Function1" in inputs
assert "param1" in inputs["Function1"]
assert inputs["Function1"]["param1"] is None
assert "Function2" in inputs
assert "param2" in inputs["Function2"]
assert isinstance(inputs["Function2"]["param2"], KernelProcessStepContext)
assert "param3" not in inputs["Function2"]
def test_get_fully_qualified_name_standard_class():
class TestClass:
pass
result = get_fully_qualified_name(TestClass)
expected = f"{TestClass.__module__}.TestClass"
assert result == expected
def test_get_fully_qualified_name_builtin_type():
result = get_fully_qualified_name(int)
expected = "builtins.int"
assert result == expected
def test_get_fully_qualified_name_third_party_class():
from unittest.mock import MagicMock
result = get_fully_qualified_name(MagicMock)
expected = "unittest.mock.MagicMock"
assert result == expected
def test_get_fully_qualified_name_nested_class():
class OuterClass:
class InnerClass:
pass
result = get_fully_qualified_name(OuterClass.InnerClass)
expected = f"{OuterClass.__module__}.InnerClass"
assert result == expected
+96 -17
View File
@@ -5,19 +5,25 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version < '3.13' and sys_platform == 'darwin'",
"python_full_version < '3.11' and sys_platform == 'darwin'",
"python_full_version == '3.11.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and sys_platform == 'darwin'",
"python_full_version < '3.11' and sys_platform == 'linux'",
"python_full_version == '3.11.*' and sys_platform == 'linux'",
"python_full_version < '3.11' and sys_platform == 'linux'",
"python_full_version == '3.11.*' and sys_platform == 'linux'",
"python_full_version < '3.13' and sys_platform == 'linux'",
"python_full_version < '3.11' and sys_platform == 'linux'",
"python_full_version == '3.11.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'linux'",
"python_full_version < '3.11' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version < '3.11' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version < '3.13' and sys_platform == 'win32'",
"python_full_version < '3.11' and sys_platform == 'win32'",
"python_full_version == '3.11.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
]
supported-markers = [
@@ -394,6 +400,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406 },
]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 },
]
[[package]]
name = "boto3"
version = "1.35.53"
@@ -827,6 +842,37 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/91/bb/cd2c13be3332e7af3cdf16154147952d39075b9f61ea5e6b5241bf4bf436/cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", size = 2988811 },
]
[[package]]
name = "dapr"
version = "1.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "grpcio-status", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/99/b9/a170cba83c3038b84cacea27556ac1cbfe6e8e9b0160671196ea942fc0d6/dapr-1.14.0.tar.gz", hash = "sha256:d901b787a5154f4b4e448e439825693f3352dda374889ef541281dd2727b8d61", size = 92544 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/44/7f728f4985a173edd70436f07a1a8a4a3d2d71ff4664d1d5cfe93bcafb9e/dapr-1.14.0-py3-none-any.whl", hash = "sha256:31bfa9587b58d410a575dd46e568cd731e790e235d7b61b18cb17420977e9c84", size = 131503 },
]
[[package]]
name = "dapr-ext-fastapi"
version = "1.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dapr", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "uvicorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/1a/3aada04990820a271a0c65a6c2bb883f8298a531238145a26baac9547ed4/dapr-ext-fastapi-1.14.0.tar.gz", hash = "sha256:162320f55bec3037534a0066f89924593abf8e6d58233b15990eb4d515aef3ce", size = 8781 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/3f/94cff4e36962b843a78a831770b9b44e476a5f9c76c0d53f7cf92053a1b4/dapr_ext_fastapi-1.14.0-py3-none-any.whl", hash = "sha256:88df67d6af33fd5adcf97d8799fa43373774b13cb9a1091ac4dd47e18a009ca0", size = 10458 },
]
[[package]]
name = "debugpy"
version = "1.8.7"
@@ -1020,6 +1066,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 },
]
[[package]]
name = "flask"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "itsdangerous", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "werkzeug", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/89/50/dff6380f1c7f84135484e176e0cac8690af72fa90e932ad2a0a60e28c69b/flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", size = 680824 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136", size = 102979 },
]
[[package]]
name = "flask-dapr"
version = "1.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dapr", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "flask", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/69/23/40884197b8b853a8c301235a88648f4ae319698ade2bf73f86393870806b/flask-dapr-1.14.0.tar.gz", hash = "sha256:e9e3209b716d9a41d53d9b644454a03e9f3408509c13ed67414f0c8b01ab6688", size = 8661 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/06/e4/63739afdaf6b8bfbf3f77fafc0d772d723d7826cdce2889b8f849bf9bb7b/flask_dapr-1.14.0-py3-none-any.whl", hash = "sha256:61d47f79e4f6c5742ddb22ef04917599c3e42b550da817801400edaca6a5d979", size = 10192 },
]
[[package]]
name = "flatbuffers"
version = "24.3.25"
@@ -1794,6 +1869,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 },
]
[[package]]
name = "jedi"
version = "0.19.1"
@@ -2613,7 +2697,6 @@ source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/7f/7fbae15a3982dc9595e49ce0f19332423b260045d0a6afe93cdbe2f1f624/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0f8aa1706812e00b9f19dfe0cdb3999b092ccb8ca168c0db5b8ea712456fd9b3", size = 363333771 },
{ url = "https://files.pythonhosted.org/packages/ae/71/1c91302526c45ab494c23f61c7a84aa568b8c1f9d196efa5993957faf906/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b", size = 363438805 },
{ url = "https://files.pythonhosted.org/packages/e2/2a/4f27ca96232e8b5269074a72e03b4e0d43aa68c9b965058b1684d07c6ff8/nvidia_cublas_cu12-12.4.5.8-py3-none-win_amd64.whl", hash = "sha256:5a796786da89203a0657eda402bcdcec6180254a8ac22d72213abc42069522dc", size = 396895858 },
]
[[package]]
@@ -2623,7 +2706,6 @@ source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/b5/9fb3d00386d3361b03874246190dfec7b206fd74e6e287b26a8fcb359d95/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:79279b35cf6f91da114182a5ce1864997fd52294a87a16179ce275773799458a", size = 12354556 },
{ url = "https://files.pythonhosted.org/packages/67/42/f4f60238e8194a3106d06a058d494b18e006c10bb2b915655bd9f6ea4cb1/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb", size = 13813957 },
{ url = "https://files.pythonhosted.org/packages/f3/79/8cf313ec17c58ccebc965568e5bcb265cdab0a1df99c4e674bb7a3b99bfe/nvidia_cuda_cupti_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:5688d203301ab051449a2b1cb6690fbe90d2b372f411521c86018b950f3d7922", size = 9938035 },
]
[[package]]
@@ -2633,7 +2715,6 @@ source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/aa/083b01c427e963ad0b314040565ea396f914349914c298556484f799e61b/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0eedf14185e04b76aa05b1fea04133e59f465b6f960c0cbf4e37c3cb6b0ea198", size = 24133372 },
{ url = "https://files.pythonhosted.org/packages/2c/14/91ae57cd4db3f9ef7aa99f4019cfa8d54cb4caa7e00975df6467e9725a9f/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338", size = 24640306 },
{ url = "https://files.pythonhosted.org/packages/7c/30/8c844bfb770f045bcd8b2c83455c5afb45983e1a8abf0c4e5297b481b6a5/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:a961b2f1d5f17b14867c619ceb99ef6fcec12e46612711bcec78eb05068a60ec", size = 19751955 },
]
[[package]]
@@ -2643,7 +2724,6 @@ source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/aa/b656d755f474e2084971e9a297def515938d56b466ab39624012070cb773/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:961fe0e2e716a2a1d967aab7caee97512f71767f852f67432d572e36cb3a11f3", size = 894177 },
{ url = "https://files.pythonhosted.org/packages/ea/27/1795d86fe88ef397885f2e580ac37628ed058a92ed2c39dc8eac3adf0619/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5", size = 883737 },
{ url = "https://files.pythonhosted.org/packages/a8/8b/450e93fab75d85a69b50ea2d5fdd4ff44541e0138db16f9cd90123ef4de4/nvidia_cuda_runtime_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:09c2e35f48359752dfa822c09918211844a3d93c100a715d79b59591130c5e1e", size = 878808 },
]
[[package]]
@@ -2655,7 +2735,6 @@ dependencies = [
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 },
{ url = "https://files.pythonhosted.org/packages/3f/d0/f90ee6956a628f9f04bf467932c0a25e5a7e706a684b896593c06c82f460/nvidia_cudnn_cu12-9.1.0.70-py3-none-win_amd64.whl", hash = "sha256:6278562929433d68365a07a4a1546c237ba2849852c0d4b2262a486e805b977a", size = 679925892 },
]
[[package]]
@@ -2668,7 +2747,6 @@ dependencies = [
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/8a/0e728f749baca3fbeffad762738276e5df60851958be7783af121a7221e7/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5dad8008fc7f92f5ddfa2101430917ce2ffacd86824914c82e28990ad7f00399", size = 211422548 },
{ url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117 },
{ url = "https://files.pythonhosted.org/packages/f6/ee/3f3f8e9874f0be5bbba8fb4b62b3de050156d159f8b6edc42d6f1074113b/nvidia_cufft_cu12-11.2.1.3-py3-none-win_amd64.whl", hash = "sha256:d802f4954291101186078ccbe22fc285a902136f974d369540fd4a5333d1440b", size = 210576476 },
]
[[package]]
@@ -2678,7 +2756,6 @@ source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/80/9c/a79180e4d70995fdf030c6946991d0171555c6edf95c265c6b2bf7011112/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1f173f09e3e3c76ab084aba0de819c49e56614feae5c12f69883f4ae9bb5fad9", size = 56314811 },
{ url = "https://files.pythonhosted.org/packages/8a/6d/44ad094874c6f1b9c654f8ed939590bdc408349f137f9b98a3a23ccec411/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b", size = 56305206 },
{ url = "https://files.pythonhosted.org/packages/1c/22/2573503d0d4e45673c263a313f79410e110eb562636b0617856fdb2ff5f6/nvidia_curand_cu12-10.3.5.147-py3-none-win_amd64.whl", hash = "sha256:f307cc191f96efe9e8f05a87096abc20d08845a841889ef78cb06924437f6771", size = 55799918 },
]
[[package]]
@@ -2693,7 +2770,6 @@ dependencies = [
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/6b/a5c33cf16af09166845345275c34ad2190944bcc6026797a39f8e0a282e0/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d338f155f174f90724bbde3758b7ac375a70ce8e706d70b018dd3375545fc84e", size = 127634111 },
{ url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057 },
{ url = "https://files.pythonhosted.org/packages/f2/be/d435b7b020e854d5d5a682eb5de4328fd62f6182507406f2818280e206e2/nvidia_cusolver_cu12-11.6.1.9-py3-none-win_amd64.whl", hash = "sha256:e77314c9d7b694fcebc84f58989f3aa4fb4cb442f12ca1a9bde50f5e8f6d1b9c", size = 125224015 },
]
[[package]]
@@ -2706,7 +2782,6 @@ dependencies = [
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/a9/c0d2f83a53d40a4a41be14cea6a0bf9e668ffcf8b004bd65633f433050c0/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9d32f62896231ebe0480efd8a7f702e143c98cfaa0e8a76df3386c1ba2b54df3", size = 207381987 },
{ url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763 },
{ url = "https://files.pythonhosted.org/packages/a2/e0/3155ca539760a8118ec94cc279b34293309bcd14011fc724f87f31988843/nvidia_cusparse_cu12-12.3.1.170-py3-none-win_amd64.whl", hash = "sha256:9bc90fb087bc7b4c15641521f31c0371e9a612fc2ba12c338d3ae032e6b6797f", size = 204684315 },
]
[[package]]
@@ -2724,7 +2799,6 @@ source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/45/239d52c05074898a80a900f49b1615d81c07fceadd5ad6c4f86a987c0bc4/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83", size = 20552510 },
{ url = "https://files.pythonhosted.org/packages/ff/ff/847841bacfbefc97a00036e0fce5a0f086b640756dc38caea5e1bb002655/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57", size = 21066810 },
{ url = "https://files.pythonhosted.org/packages/81/19/0babc919031bee42620257b9a911c528f05fb2688520dcd9ca59159ffea8/nvidia_nvjitlink_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:fd9020c501d27d135f983c6d3e244b197a7ccad769e34df53a42e276b0e25fa1", size = 95336325 },
]
[[package]]
@@ -2734,7 +2808,6 @@ source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/06/39/471f581edbb7804b39e8063d92fc8305bdc7a80ae5c07dbe6ea5c50d14a5/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7959ad635db13edf4fc65c06a6e9f9e55fc2f92596db928d169c0bb031e88ef3", size = 100417 },
{ url = "https://files.pythonhosted.org/packages/87/20/199b8713428322a2f22b722c62b8cc278cc53dffa9705d744484b5035ee9/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a", size = 99144 },
{ url = "https://files.pythonhosted.org/packages/54/1b/f77674fbb73af98843be25803bbd3b9a4f0a96c75b8d33a2854a5c7d2d77/nvidia_nvtx_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:641dccaaa1139f3ffb0d3164b4b84f9d253397e38246a4f2f36728b48566d485", size = 66307 },
]
[[package]]
@@ -3457,8 +3530,6 @@ version = "6.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/26/10/2a30b13c61e7cf937f4adf90710776b7918ed0a9c434e2c38224732af310/psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a", size = 508565 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/2b/f4dea5d993d9cd22ad958eea828a41d5d225556123d372f02547c29c4f97/psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e", size = 246648 },
{ url = "https://files.pythonhosted.org/packages/9f/14/4aa97a7f2e0ac33a050d990ab31686d651ae4ef8c86661fef067f00437b9/psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85", size = 249905 },
{ url = "https://files.pythonhosted.org/packages/01/9e/8be43078a171381953cfee33c07c0d628594b5dbfc5157847b85022c2c1b/psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688", size = 247762 },
{ url = "https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e", size = 248777 },
{ url = "https://files.pythonhosted.org/packages/65/8e/bcbe2025c587b5d703369b6a75b65d41d1367553da6e3f788aff91eaf5bd/psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38", size = 284259 },
@@ -4618,6 +4689,11 @@ azure = [
chroma = [
{ name = "chromadb", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
dapr = [
{ name = "dapr", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "dapr-ext-fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "flask-dapr", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
google = [
{ name = "google-cloud-aiplatform", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "google-generativeai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -4699,7 +4775,10 @@ requires-dist = [
{ name = "boto3", marker = "extra == 'aws'", specifier = ">=1.28.57" },
{ name = "chromadb", marker = "extra == 'chroma'", specifier = ">=0.5,<0.6" },
{ name = "cloudevents", specifier = "~=1.0" },
{ name = "dapr", marker = "extra == 'dapr'", specifier = ">=1.14.0" },
{ name = "dapr-ext-fastapi", marker = "extra == 'dapr'", specifier = ">=1.14.0" },
{ name = "defusedxml", specifier = "~=0.7" },
{ name = "flask-dapr", marker = "extra == 'dapr'", specifier = ">=1.14.0" },
{ name = "google-cloud-aiplatform", marker = "extra == 'google'", specifier = "~=1.60" },
{ name = "google-generativeai", marker = "extra == 'google'", specifier = "~=0.7" },
{ name = "ipykernel", marker = "extra == 'notebooks'", specifier = "~=6.29" },
@@ -5085,7 +5164,7 @@ dependencies = [
{ name = "nvidia-nccl-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" },
{ name = "nvidia-nvjitlink-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" },
{ name = "nvidia-nvtx-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" },
{ name = "setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "setuptools", marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" },
{ name = "sympy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "triton", marker = "(python_full_version < '3.13' and platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" },
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },