chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
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.feature_stage_decorator import experimental
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@experimental
|
||||
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.feature_stage_decorator import experimental
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@experimental
|
||||
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.feature_stage_decorator import experimental
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@experimental
|
||||
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,450 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from collections.abc import Callable, MutableSequence
|
||||
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.feature_stage_decorator import experimental
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@experimental
|
||||
class ProcessActor(StepActor, ProcessInterface):
|
||||
"""A local process that contains a collection of steps."""
|
||||
|
||||
max_supersteps: int = 100
|
||||
|
||||
def __init__(self, ctx: ActorRuntimeContext, actor_id: ActorId, kernel: Kernel, factories: dict[str, Callable]):
|
||||
"""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.
|
||||
factories: The factory dictionary that contains step types to factory methods.
|
||||
"""
|
||||
super().__init__(ctx, actor_id, kernel, factories)
|
||||
self.kernel = kernel
|
||||
self.factories = factories
|
||||
self.steps: MutableSequence[StepInterface] = []
|
||||
self.step_infos: MutableSequence[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")
|
||||
max_supersteps = input.get("max_supersteps", None)
|
||||
|
||||
if process_info_data is None:
|
||||
raise ValueError("The process info is not defined.")
|
||||
|
||||
if max_supersteps is not None:
|
||||
self.max_supersteps = max_supersteps
|
||||
|
||||
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")
|
||||
|
||||
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(max_supersteps=self.max_supersteps, keep_alive=keep_alive)
|
||||
)
|
||||
|
||||
async def run_once(self, process_event: KernelProcessEvent | str | None) -> 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.model_dump_json() if isinstance(process_event, KernelProcessEvent) else 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, input: str) -> None:
|
||||
"""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 = {
|
||||
"process_info": existing_process_info,
|
||||
"parent_process_id": parent_process_id,
|
||||
}
|
||||
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) -> dict:
|
||||
"""Gets the process information."""
|
||||
return await self.to_dapr_process_info()
|
||||
|
||||
async def to_dapr_process_info(self) -> dict:
|
||||
"""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.process.inner_step_python_type is None:
|
||||
raise ValueError("The inner step type must be defined before converting to DaprProcessInfo.")
|
||||
|
||||
process_state = KernelProcessState(name=self.name, version=self.process.state.version, id=self.id.id)
|
||||
|
||||
step_tasks = [step.to_dapr_step_info() for step in self.steps]
|
||||
steps_as_dicts = await asyncio.gather(*step_tasks)
|
||||
|
||||
dapr_process_info = DaprProcessInfo(
|
||||
inner_step_python_type=self.process.inner_step_python_type,
|
||||
edges=self.process.edges,
|
||||
state=process_state,
|
||||
steps=steps_as_dicts,
|
||||
)
|
||||
return dapr_process_info.model_dump()
|
||||
|
||||
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: StepInterface | None = 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 = 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."""
|
||||
logger.debug(f"Running process for {max_supersteps} supersteps.")
|
||||
|
||||
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,480 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Callable, Sequence
|
||||
from inspect import isawaitable
|
||||
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 (
|
||||
DEFAULT_ALLOWED_MODULE_PREFIXES,
|
||||
find_input_channels,
|
||||
get_fully_qualified_name,
|
||||
get_step_class_from_qualified_name,
|
||||
)
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@experimental
|
||||
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,
|
||||
factories: dict[str, Callable],
|
||||
allowed_module_prefixes: Sequence[str] | None = DEFAULT_ALLOWED_MODULE_PREFIXES,
|
||||
):
|
||||
"""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.
|
||||
factories: The factory dictionary to use for creating the step.
|
||||
allowed_module_prefixes: Sequence of module prefixes that are allowed
|
||||
for step class loading. Step classes must come from modules starting
|
||||
with one of these prefixes. Defaults to ("semantic_kernel.",). Pass
|
||||
None to allow any module (not recommended for production).
|
||||
"""
|
||||
super().__init__(ctx, actor_id)
|
||||
self.kernel = kernel
|
||||
self.factories: dict[str, Callable] = factories
|
||||
self.allowed_module_prefixes: Sequence[str] | None = allowed_module_prefixes
|
||||
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.model_dump_json())
|
||||
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()
|
||||
|
||||
async def activate_step(self):
|
||||
"""Initializes the step."""
|
||||
# Instantiate an instance of the inner step object and retrieve its class reference.
|
||||
if self.factories and self.inner_step_type in self.factories:
|
||||
step_object = self.factories[self.inner_step_type]()
|
||||
if isawaitable(step_object):
|
||||
step_object = await step_object
|
||||
step_cls = step_object.__class__
|
||||
step_instance: KernelProcessStep = step_object # type: ignore
|
||||
else:
|
||||
step_cls = get_step_class_from_qualified_name(
|
||||
self.inner_step_type,
|
||||
allowed_module_prefixes=self.allowed_module_prefixes,
|
||||
)
|
||||
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 using the class reference.
|
||||
t_state = get_generic_state_type(step_cls)
|
||||
|
||||
if t_state is not None:
|
||||
state_type = KernelProcessStepState[t_state]
|
||||
|
||||
if state_object is None:
|
||||
# Create a fresh step state object if none is provided.
|
||||
state_object = state_type(
|
||||
name=step_cls.__name__,
|
||||
id=step_cls.__name__,
|
||||
state=None,
|
||||
)
|
||||
else:
|
||||
# Ensure that state_object is an instance of the expected type.
|
||||
if not isinstance(state_object, KernelProcessStepState):
|
||||
error_message = "State object is not of the expected KernelProcessStepState 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()
|
||||
|
||||
# If state is None, instantiate it. If it exists but is not the right type, validate it.
|
||||
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:
|
||||
# Convert the existing state if it's not already an instance of t_state
|
||||
if not isinstance(state_object.state, t_state):
|
||||
try:
|
||||
state_object.state = t_state.model_validate(state_object.state)
|
||||
except Exception as e:
|
||||
error_message = f"Cannot validate 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: str = ""
|
||||
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)
|
||||
if invoke_result is None:
|
||||
raise KernelException(f"Function {target_function} returned None.")
|
||||
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) -> dict:
|
||||
"""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.")
|
||||
|
||||
if self.step_state is not None:
|
||||
self.step_info.state = self.step_state
|
||||
|
||||
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()
|
||||
|
||||
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, [])
|
||||
Reference in New Issue
Block a user