mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
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:
Vendored
+20
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
Vendored
+14
@@ -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": [
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
+9
-9
@@ -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")
|
||||
)
|
||||
|
||||
|
||||
+10
-10
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
+16
-20
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
+15
-19
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
+21
-23
@@ -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)
|
||||
|
||||
+8
-14
@@ -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,
|
||||
)
|
||||
|
||||
+5
-5
@@ -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`.
|
||||
"""
|
||||
...
|
||||
+35
@@ -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
|
||||
Generated
+96
-17
@@ -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'" },
|
||||
|
||||
Reference in New Issue
Block a user