chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# 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,
|
||||
)
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
class LocalEvent(KernelBaseModel):
|
||||
"""An event that is local to a namespace."""
|
||||
|
||||
namespace: str | None
|
||||
inner_event: KernelProcessEvent
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
"""The ID of the event."""
|
||||
return f"{self.namespace}.{self.inner_event.id}" if self.namespace else self.inner_event.id
|
||||
|
||||
@property
|
||||
def data(self) -> Any:
|
||||
"""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
|
||||
|
||||
@classmethod
|
||||
def from_kernel_process_event(cls, kernel_process_event: KernelProcessEvent, namespace: str) -> "LocalEvent":
|
||||
"""Create a local event from a kernel process event."""
|
||||
return cls(namespace=namespace, inner_event=kernel_process_event)
|
||||
@@ -0,0 +1,52 @@
|
||||
# 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.local_runtime.local_event import KernelProcessEvent
|
||||
from semantic_kernel.processes.local_runtime.local_kernel_process_context import LocalKernelProcessContext
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.kernel import Kernel
|
||||
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
|
||||
|
||||
|
||||
@experimental
|
||||
async def start(
|
||||
process: "KernelProcess",
|
||||
kernel: "Kernel",
|
||||
initial_event: KernelProcessEvent | str | Enum,
|
||||
max_supersteps: int | None = None,
|
||||
**kwargs,
|
||||
) -> LocalKernelProcessContext:
|
||||
"""Start the kernel process.
|
||||
|
||||
Args:
|
||||
process: The kernel process to start.
|
||||
kernel: The kernel instance.
|
||||
initial_event: The initial event to start the process with.
|
||||
max_supersteps: The maximum number of supersteps. This is the total number of times process steps will run.
|
||||
Defaults to None, and thus the process will run its steps 100 times.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
if process is None:
|
||||
raise ProcessInvalidConfigurationException("process cannot be None")
|
||||
if process.state is None or not process.state.name:
|
||||
raise ProcessInvalidConfigurationException("process state name cannot be empty")
|
||||
if kernel is None:
|
||||
raise ProcessInvalidConfigurationException("kernel cannot be None")
|
||||
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"))
|
||||
|
||||
process_context = LocalKernelProcessContext(process, kernel, max_supersteps)
|
||||
await process_context.start_with_event(initial_event_str)
|
||||
return process_context
|
||||
@@ -0,0 +1,76 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from semantic_kernel.kernel import Kernel
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
from semantic_kernel.processes.local_runtime.local_process import LocalProcess
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
|
||||
from semantic_kernel.processes.local_runtime.local_event import KernelProcessEvent
|
||||
|
||||
|
||||
@experimental
|
||||
class LocalKernelProcessContext(KernelBaseModel):
|
||||
"""A local kernel process context."""
|
||||
|
||||
local_process: LocalProcess
|
||||
|
||||
def __init__(self, process: "KernelProcess", kernel: "Kernel", max_supersteps: int | None = None) -> None:
|
||||
"""Initializes the local kernel process context.
|
||||
|
||||
Args:
|
||||
process: The kernel process to start.
|
||||
kernel: The kernel instance.
|
||||
max_supersteps: The maximum number of supersteps. This is the total number of times process steps will run.
|
||||
Defaults to None.
|
||||
"""
|
||||
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess # noqa: F401
|
||||
|
||||
LocalProcess.model_rebuild()
|
||||
|
||||
if not process or not process.state or not process.state.name.strip():
|
||||
raise ValueError("Process and process state must be provided and have a valid name")
|
||||
if not kernel:
|
||||
raise ValueError("Kernel must be provided")
|
||||
|
||||
local_process = LocalProcess(
|
||||
process=process,
|
||||
kernel=kernel,
|
||||
parent_process_id=None,
|
||||
factories=process.factories,
|
||||
max_supersteps=max_supersteps,
|
||||
)
|
||||
|
||||
super().__init__(local_process=local_process) # type: ignore
|
||||
|
||||
async def start_with_event(self, initial_event: "KernelProcessEvent") -> None:
|
||||
"""Starts the local process with an initial event."""
|
||||
await self.local_process.run_once(initial_event)
|
||||
|
||||
async def send_event(self, process_event: "KernelProcessEvent") -> None:
|
||||
"""Sends an event to the process."""
|
||||
await self.local_process.send_message(process_event)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stops the local process."""
|
||||
await self.local_process.stop()
|
||||
|
||||
async def get_state(self) -> "KernelProcess":
|
||||
"""Gets the current state of the process."""
|
||||
return await self.local_process.get_process_info()
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Enters the async context (used for resource management)."""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Exits the async context and disposes of resources."""
|
||||
await self.dispose()
|
||||
|
||||
async def dispose(self) -> None:
|
||||
"""Disposes of the resources used by the process."""
|
||||
if self.local_process:
|
||||
self.local_process.dispose()
|
||||
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
class LocalMessage(KernelBaseModel):
|
||||
"""A message that is local to a namespace."""
|
||||
|
||||
source_id: str = Field(...)
|
||||
destination_id: str = Field(...)
|
||||
function_name: str = Field(...)
|
||||
values: dict[str, Any | None] = Field(...)
|
||||
target_event_id: str | None = Field(default=None)
|
||||
target_event_data: Any | None = Field(default=None)
|
||||
@@ -0,0 +1,30 @@
|
||||
# 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.local_runtime.local_message import LocalMessage
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
class LocalMessageFactory:
|
||||
"""Factory class to create LocalMessage instances."""
|
||||
|
||||
@staticmethod
|
||||
def create_from_edge(edge: "KernelProcessEdge", data: Any | None = None) -> "LocalMessage":
|
||||
"""Creates a new LocalMessage instance from a KernelProcessEdge and a data object."""
|
||||
target = edge.output_target
|
||||
parameter_value: dict[str, Any | None] = {}
|
||||
|
||||
if target.parameter_name:
|
||||
parameter_value[target.parameter_name] = data
|
||||
|
||||
return LocalMessage(
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,265 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from queue import Queue
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from semantic_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.kernel_process.kernel_process_state import KernelProcessState
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_step_info import KernelProcessStepInfo
|
||||
from semantic_kernel.processes.local_runtime.local_event import (
|
||||
KernelProcessEvent,
|
||||
KernelProcessEventVisibility,
|
||||
LocalEvent,
|
||||
)
|
||||
from semantic_kernel.processes.local_runtime.local_message import LocalMessage
|
||||
from semantic_kernel.processes.local_runtime.local_message_factory import LocalMessageFactory
|
||||
from semantic_kernel.processes.local_runtime.local_step import LocalStep
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@experimental
|
||||
class LocalProcess(LocalStep):
|
||||
"""A local process that contains a collection of steps."""
|
||||
|
||||
kernel: Kernel
|
||||
steps: list[LocalStep] = Field(default_factory=list)
|
||||
step_infos: list[KernelProcessStepInfo] = Field(default_factory=list)
|
||||
process: "KernelProcess"
|
||||
initialize_task: bool | None = False
|
||||
external_event_queue: Queue = Field(default_factory=Queue)
|
||||
process_task: asyncio.Task | None = None
|
||||
factories: dict[str, Callable] = Field(default_factory=dict)
|
||||
max_supersteps: int = Field(
|
||||
default=100, ge=1, description="Maximum number of supersteps to execute before stopping the process."
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
process: "KernelProcess",
|
||||
kernel: Kernel,
|
||||
factories: dict[str, Callable] | None = None,
|
||||
parent_process_id: str | None = None,
|
||||
max_supersteps: int | None = None,
|
||||
):
|
||||
"""Initializes the local process."""
|
||||
args: dict[str, Any] = {
|
||||
"step_info": process,
|
||||
"kernel": kernel,
|
||||
"parent_process_id": parent_process_id,
|
||||
"step_infos": list(process.steps),
|
||||
"process": process,
|
||||
"initialize_task": False,
|
||||
}
|
||||
|
||||
if max_supersteps is not None:
|
||||
args["max_supersteps"] = max_supersteps
|
||||
|
||||
if factories:
|
||||
args["factories"] = factories
|
||||
|
||||
super().__init__(**args)
|
||||
|
||||
def ensure_initialized(self):
|
||||
"""Ensures the process is initialized lazily (synchronously)."""
|
||||
if not self.initialize_task:
|
||||
self.initialize_process()
|
||||
self.initialize_task = True
|
||||
|
||||
async def start(self, keep_alive: bool = True) -> None:
|
||||
"""Starts the process with an initial event.
|
||||
|
||||
Args:
|
||||
keep_alive: Indicates if the process should wait for external events after it's finished processing.
|
||||
"""
|
||||
self.ensure_initialized()
|
||||
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):
|
||||
"""Starts the process with an initial event and waits for it to finish.
|
||||
|
||||
Args:
|
||||
process_event: The KernelProcessEvent to start the process with.
|
||||
"""
|
||||
if process_event is None:
|
||||
raise ProcessEventUndefinedException("The process event must be specified.")
|
||||
self.external_event_queue.put(process_event)
|
||||
await self.start(keep_alive=False)
|
||||
if self.process_task:
|
||||
await self.process_task
|
||||
|
||||
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 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) -> "KernelProcess":
|
||||
"""Gets the process information."""
|
||||
return await self.to_kernel_process()
|
||||
|
||||
def initialize_process(self):
|
||||
"""Initializes the input and output edges for the process and initializes the steps."""
|
||||
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
|
||||
|
||||
# Initialize the input and output edges for the process
|
||||
self.output_edges = {kvp[0]: list(kvp[1]) for kvp in self.process.edges.items()}
|
||||
|
||||
# Initialize the steps within this process
|
||||
for step in self.step_infos:
|
||||
local_step = None
|
||||
|
||||
# The current step should already have a name.
|
||||
assert step.state and step.state.name is not None # nosec
|
||||
|
||||
if isinstance(step, KernelProcess):
|
||||
# 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)
|
||||
|
||||
# Create a new LocalProcess for the step
|
||||
process = LocalProcess(
|
||||
process=step,
|
||||
kernel=self.kernel,
|
||||
factories=self.factories,
|
||||
parent_process_id=self.id,
|
||||
)
|
||||
|
||||
local_step = process
|
||||
else:
|
||||
# The current step should already have an Id.
|
||||
assert step.state and step.state.id is not None # nosec
|
||||
|
||||
# Create a LocalStep for the step
|
||||
local_step = LocalStep( # type: ignore
|
||||
step_info=step,
|
||||
kernel=self.kernel,
|
||||
factories=self.factories,
|
||||
parent_process_id=self.id,
|
||||
)
|
||||
|
||||
# Add the local step to the list of steps
|
||||
self.steps.append(local_step)
|
||||
|
||||
async def handle_message(self, message: LocalMessage):
|
||||
"""Handles a LocalMessage that has been sent to the process."""
|
||||
if not message.target_event_id:
|
||||
error_message = (
|
||||
"Internal Process Error: The target event id must be specified when sending a message to a step."
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise KernelException(error_message)
|
||||
|
||||
event_id = message.target_event_id
|
||||
if event_id in self.output_edges:
|
||||
nested_event = KernelProcessEvent(
|
||||
id=event_id, data=message.target_event_data, visibility=KernelProcessEventVisibility.Internal
|
||||
)
|
||||
await self.run_once(nested_event)
|
||||
|
||||
async def internal_execute(self, max_supersteps: int = 100, keep_alive: bool = True):
|
||||
"""Internal execution logic for the process."""
|
||||
message_channel: Queue[LocalMessage] = Queue()
|
||||
|
||||
logger.debug(f"Running process for {max_supersteps} supersteps.")
|
||||
|
||||
try:
|
||||
for _ in range(max_supersteps):
|
||||
self.enqueue_external_messages(message_channel)
|
||||
for step in self.steps:
|
||||
await self.enqueue_step_messages(step, message_channel)
|
||||
|
||||
messages_to_process: list[LocalMessage] = []
|
||||
while not message_channel.empty():
|
||||
messages_to_process.append(message_channel.get())
|
||||
|
||||
if not messages_to_process and (not keep_alive or self.external_event_queue.empty()):
|
||||
break
|
||||
|
||||
message_tasks = []
|
||||
for message in messages_to_process:
|
||||
if message.destination_id == END_PROCESS_ID:
|
||||
break
|
||||
|
||||
destination_step = next(step for step in self.steps if step.id == message.destination_id)
|
||||
message_tasks.append(destination_step.handle_message(message))
|
||||
|
||||
await asyncio.gather(*message_tasks)
|
||||
|
||||
except Exception as ex:
|
||||
logger.error(f"An error occurred while running the process: {ex}.")
|
||||
raise
|
||||
|
||||
async def to_kernel_process(self) -> "KernelProcess":
|
||||
"""Builds a KernelProcess from the current LocalProcess."""
|
||||
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
|
||||
|
||||
process_state = KernelProcessState(name=self.name, version=self.step_state.version, id=self.id)
|
||||
step_tasks = [step.to_kernel_process_step_info() for step in self.steps]
|
||||
steps = await asyncio.gather(*step_tasks)
|
||||
return KernelProcess(state=process_state, steps=steps, edges=self.output_edges)
|
||||
|
||||
async def to_kernel_process_step_info(self) -> "KernelProcessStepInfo":
|
||||
"""Extracts the current state of the step and returns it as a KernelProcessStepInfo."""
|
||||
return await self.to_kernel_process()
|
||||
|
||||
def enqueue_external_messages(self, message_channel: Queue[LocalMessage]):
|
||||
"""Processes external events that have been sent to the process."""
|
||||
while not self.external_event_queue.empty():
|
||||
external_event: KernelProcessEvent = self.external_event_queue.get_nowait()
|
||||
if external_event.id in self.output_edges:
|
||||
edges = self.output_edges[external_event.id]
|
||||
for edge in edges:
|
||||
message = LocalMessageFactory.create_from_edge(edge, external_event.data)
|
||||
message_channel.put(message)
|
||||
|
||||
async def enqueue_step_messages(self, step: LocalStep, message_channel: Queue[LocalMessage]):
|
||||
"""Processes events emitted by the given step and enqueues them."""
|
||||
all_step_events = step.get_all_events()
|
||||
for step_event in all_step_events:
|
||||
# must come first because emitting the step event modifies its namespace
|
||||
for edge in step.get_edge_for_event(step_event.id):
|
||||
message = LocalMessageFactory.create_from_edge(edge, step_event.data)
|
||||
message_channel.put(message)
|
||||
|
||||
if step_event.visibility == KernelProcessEventVisibility.Public:
|
||||
if isinstance(step_event, KernelProcessEvent):
|
||||
await self.emit_event(step_event) # type: ignore
|
||||
elif isinstance(step_event, LocalEvent):
|
||||
await self.emit_local_event(step_event) # type: ignore
|
||||
|
||||
def dispose(self):
|
||||
"""Clean up resources."""
|
||||
if self.process_task:
|
||||
self.process_task.cancel()
|
||||
@@ -0,0 +1,303 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from inspect import isawaitable
|
||||
from queue import Queue
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field, model_validator
|
||||
|
||||
from semantic_kernel import Kernel
|
||||
from semantic_kernel.exceptions import KernelException
|
||||
from semantic_kernel.exceptions.process_exceptions import (
|
||||
ProcessFunctionNotFoundException,
|
||||
ProcessTargetFunctionNameMismatchException,
|
||||
)
|
||||
from semantic_kernel.functions import KernelFunction
|
||||
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_event import KernelProcessEvent
|
||||
from semantic_kernel.processes.kernel_process.kernel_process_message_channel import KernelProcessMessageChannel
|
||||
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, get_fully_qualified_name
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@experimental
|
||||
class LocalStep(KernelProcessMessageChannel, KernelBaseModel):
|
||||
"""A local step that is part of a local process."""
|
||||
|
||||
kernel: "Kernel"
|
||||
step_info: "KernelProcessStepInfo"
|
||||
outgoing_event_queue: Queue[LocalEvent] = Field(default_factory=Queue)
|
||||
initialize_task: bool | None = False
|
||||
event_namespace: str
|
||||
step_state: KernelProcessStepState
|
||||
inputs: dict[str, dict[str, Any | None]] = Field(default_factory=dict)
|
||||
initial_inputs: dict[str, dict[str, Any | None]] = Field(default_factory=dict)
|
||||
functions: dict[str, KernelFunction] = Field(default_factory=dict)
|
||||
output_edges: dict[str, list[KernelProcessEdge]] = Field(default_factory=dict)
|
||||
parent_process_id: str | None = None
|
||||
init_lock: asyncio.Lock = Field(default_factory=asyncio.Lock, exclude=True)
|
||||
factories: dict[str, Callable]
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def parse_initial_configuration(cls, data: Any) -> Any:
|
||||
"""Parses the initial configuration of the step."""
|
||||
if isinstance(data, dict):
|
||||
from semantic_kernel.processes.kernel_process.kernel_process import KernelProcess
|
||||
|
||||
step_info = data.get("step_info")
|
||||
assert step_info is not None # nosec
|
||||
|
||||
if step_info and isinstance(step_info, KernelProcess) and step_info.state.id is None:
|
||||
step_info.state.id = str(uuid.uuid4().hex)
|
||||
|
||||
data["step_state"] = step_info.state
|
||||
data["output_edges"] = {k: v for k, v in step_info.edges.items()}
|
||||
data["event_namespace"] = f"{step_info.state.name}_{step_info.state.id}"
|
||||
return data
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Gets the name of the step."""
|
||||
return self.step_info.state.name
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
"""Gets the ID of the step."""
|
||||
return self.step_info.state.id if self.step_info.state.id else ""
|
||||
|
||||
async def handle_message(self, message: LocalMessage):
|
||||
"""Handles a LocalMessage that has been sent to the step."""
|
||||
if message is None:
|
||||
raise ValueError("The message is None.")
|
||||
|
||||
if not self.initialize_task:
|
||||
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.initialize_task:
|
||||
await self.initialize_step()
|
||||
self.initialize_task = True
|
||||
|
||||
if self.functions is None or self.inputs is None or self.initial_inputs is None:
|
||||
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
|
||||
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."""
|
||||
await self.emit_local_event(LocalEvent.from_kernel_process_event(process_event, self.event_namespace))
|
||||
|
||||
async def emit_local_event(self, local_event: "LocalEvent"):
|
||||
"""Emits an event from the step."""
|
||||
scoped_event = self.scoped_event(local_event)
|
||||
self.outgoing_event_queue.put(scoped_event)
|
||||
|
||||
async def initialize_step(self):
|
||||
"""Initializes the step."""
|
||||
# Instantiate an instance of the inner step object
|
||||
step_cls = self.step_info.inner_step_type
|
||||
factory = (
|
||||
self.factories.get(get_fully_qualified_name(self.step_info.inner_step_type)) if self.factories else None
|
||||
)
|
||||
if factory:
|
||||
step_instance = factory()
|
||||
if isawaitable(step_instance):
|
||||
step_instance = await step_instance
|
||||
step_cls = type(step_instance)
|
||||
else:
|
||||
step_instance = 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)
|
||||
|
||||
# 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)
|
||||
|
||||
def get_all_events(self) -> list["LocalEvent"]:
|
||||
"""Retrieves all events that have been emitted by this step in the previous superstep."""
|
||||
all_events = []
|
||||
while not self.outgoing_event_queue.empty():
|
||||
all_events.append(self.outgoing_event_queue.get())
|
||||
return all_events
|
||||
|
||||
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, [])
|
||||
|
||||
async def to_kernel_process_step_info(self) -> "KernelProcessStepInfo":
|
||||
"""Extracts the current state of the step and returns it as a KernelProcessStepInfo."""
|
||||
if not self.initialize_task:
|
||||
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.initialize_task:
|
||||
await self.initialize_step()
|
||||
self.initialize_task = True
|
||||
|
||||
return KernelProcessStepInfo(
|
||||
inner_step_type=self.step_info.inner_step_type, state=self.step_state, output_edges=self.output_edges
|
||||
)
|
||||
|
||||
def scoped_event(self, local_event: "LocalEvent") -> "LocalEvent":
|
||||
"""Generates a scoped event for the step."""
|
||||
if local_event is None:
|
||||
raise ValueError("The local event must be specified.")
|
||||
local_event.namespace = f"{self.name}_{self.id}"
|
||||
return local_event
|
||||
|
||||
def scoped_event_from_kernel_process(self, process_event: "KernelProcessEvent") -> "LocalEvent":
|
||||
"""Generates a scoped event for the step from a KernelProcessEvent."""
|
||||
if process_event is None:
|
||||
raise ValueError("The process event must be specified.")
|
||||
return LocalEvent.from_kernel_process_event(process_event, f"{self.name}_{self.id}")
|
||||
Reference in New Issue
Block a user