mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Workflow event source updates (#789)
* Workflow event source updates * Add WorkflowLifecycleEvent TypeAlias. Update docstrings * Updates * Rename --------- Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
6a66ad517a
commit
960196cd52
@@ -31,12 +31,14 @@ from ._events import (
|
||||
ExecutorCompletedEvent,
|
||||
ExecutorEvent,
|
||||
ExecutorFailedEvent,
|
||||
ExecutorInvokeEvent,
|
||||
ExecutorInvokedEvent,
|
||||
RequestInfoEvent,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowErrorDetails,
|
||||
WorkflowEvent,
|
||||
WorkflowEventSource,
|
||||
WorkflowFailedEvent,
|
||||
WorkflowLifecycleEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStartedEvent,
|
||||
WorkflowStatusEvent,
|
||||
@@ -118,7 +120,7 @@ __all__ = [
|
||||
"ExecutorCompletedEvent",
|
||||
"ExecutorEvent",
|
||||
"ExecutorFailedEvent",
|
||||
"ExecutorInvokeEvent",
|
||||
"ExecutorInvokedEvent",
|
||||
"FanInEdgeGroup",
|
||||
"FanOutEdgeGroup",
|
||||
"FileCheckpointStorage",
|
||||
@@ -172,8 +174,10 @@ __all__ = [
|
||||
"WorkflowContext",
|
||||
"WorkflowErrorDetails",
|
||||
"WorkflowEvent",
|
||||
"WorkflowEventSource",
|
||||
"WorkflowExecutor",
|
||||
"WorkflowFailedEvent",
|
||||
"WorkflowLifecycleEvent",
|
||||
"WorkflowRunResult",
|
||||
"WorkflowRunState",
|
||||
"WorkflowStartedEvent",
|
||||
|
||||
@@ -27,12 +27,14 @@ from ._events import (
|
||||
ExecutorCompletedEvent,
|
||||
ExecutorEvent,
|
||||
ExecutorFailedEvent,
|
||||
ExecutorInvokeEvent,
|
||||
ExecutorInvokedEvent,
|
||||
RequestInfoEvent,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowErrorDetails,
|
||||
WorkflowEvent,
|
||||
WorkflowEventSource,
|
||||
WorkflowFailedEvent,
|
||||
WorkflowLifecycleEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStartedEvent,
|
||||
WorkflowStatusEvent,
|
||||
@@ -114,7 +116,7 @@ __all__ = [
|
||||
"ExecutorCompletedEvent",
|
||||
"ExecutorEvent",
|
||||
"ExecutorFailedEvent",
|
||||
"ExecutorInvokeEvent",
|
||||
"ExecutorInvokedEvent",
|
||||
"FanInEdgeGroup",
|
||||
"FanOutEdgeGroup",
|
||||
"FileCheckpointStorage",
|
||||
@@ -168,8 +170,10 @@ __all__ = [
|
||||
"WorkflowContext",
|
||||
"WorkflowErrorDetails",
|
||||
"WorkflowEvent",
|
||||
"WorkflowEventSource",
|
||||
"WorkflowExecutor",
|
||||
"WorkflowFailedEvent",
|
||||
"WorkflowLifecycleEvent",
|
||||
"WorkflowRunResult",
|
||||
"WorkflowRunState",
|
||||
"WorkflowStartedEvent",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import traceback as _traceback
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, TypeAlias
|
||||
|
||||
from agent_framework import AgentRunResponse, AgentRunResponseUpdate
|
||||
|
||||
@@ -11,32 +14,70 @@ if TYPE_CHECKING:
|
||||
from ._executor import RequestInfoMessage
|
||||
|
||||
|
||||
class WorkflowEventSource(str, Enum):
|
||||
"""Identifies whether a workflow event came from the framework or an executor.
|
||||
|
||||
Use `FRAMEWORK` for events emitted by built-in orchestration paths—even when the
|
||||
code that raises them lives in runner-related modules—and `EXECUTOR` for events
|
||||
surfaced by developer-provided executor implementations.
|
||||
"""
|
||||
|
||||
FRAMEWORK = "FRAMEWORK" # Framework-owned orchestration, regardless of module location
|
||||
EXECUTOR = "EXECUTOR" # User-supplied executor code and callbacks
|
||||
|
||||
|
||||
_event_origin_context: ContextVar[WorkflowEventSource] = ContextVar(
|
||||
"workflow_event_origin", default=WorkflowEventSource.EXECUTOR
|
||||
)
|
||||
|
||||
|
||||
def _current_event_origin() -> WorkflowEventSource:
|
||||
"""Return the origin to associate with newly created workflow events."""
|
||||
return _event_origin_context.get()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _framework_event_origin() -> Iterator[None]: # pyright: ignore[reportUnusedFunction]
|
||||
"""Temporarily mark subsequently created events as originating from the framework (internal)."""
|
||||
token = _event_origin_context.set(WorkflowEventSource.FRAMEWORK)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_event_origin_context.reset(token)
|
||||
|
||||
|
||||
class WorkflowEvent:
|
||||
"""Base class for workflow events."""
|
||||
|
||||
def __init__(self, data: Any | None = None):
|
||||
"""Initialize the workflow event with optional data."""
|
||||
self.data = data
|
||||
self.origin = _current_event_origin()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string representation of the workflow event."""
|
||||
return f"{self.__class__.__name__}(data={self.data if self.data is not None else 'None'})"
|
||||
data_repr = self.data if self.data is not None else "None"
|
||||
return f"{self.__class__.__name__}(origin={self.origin}, data={data_repr})"
|
||||
|
||||
|
||||
class WorkflowStartedEvent(WorkflowEvent):
|
||||
"""Event triggered when a workflow starts."""
|
||||
"""Built-in lifecycle event emitted when a workflow run begins."""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class WorkflowCompletedEvent(WorkflowEvent):
|
||||
"""Event triggered when a workflow completes."""
|
||||
"""Built-in lifecycle event emitted when a workflow run completes successfully.
|
||||
|
||||
Unlike the framework-only `WorkflowLifecycleEvent` union, this event can be
|
||||
emitted by developer-provided executors to return final workflow output.
|
||||
"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class WorkflowWarningEvent(WorkflowEvent):
|
||||
"""Event triggered when a warning occurs in the workflow."""
|
||||
"""Executor-origin event signaling a warning surfaced by user code."""
|
||||
|
||||
def __init__(self, data: str):
|
||||
"""Initialize the workflow warning event with optional data and warning message."""
|
||||
@@ -44,11 +85,11 @@ class WorkflowWarningEvent(WorkflowEvent):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string representation of the workflow warning event."""
|
||||
return f"{self.__class__.__name__}(message={self.data})"
|
||||
return f"{self.__class__.__name__}(message={self.data}, origin={self.origin})"
|
||||
|
||||
|
||||
class WorkflowErrorEvent(WorkflowEvent):
|
||||
"""Event triggered when an error occurs in the workflow."""
|
||||
"""Executor-origin event signaling an error surfaced by user code."""
|
||||
|
||||
def __init__(self, data: Exception):
|
||||
"""Initialize the workflow error event with optional data and error message."""
|
||||
@@ -56,7 +97,7 @@ class WorkflowErrorEvent(WorkflowEvent):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string representation of the workflow error event."""
|
||||
return f"{self.__class__.__name__}(exception={self.data})"
|
||||
return f"{self.__class__.__name__}(exception={self.data}, origin={self.origin})"
|
||||
|
||||
|
||||
class WorkflowRunState(str, Enum):
|
||||
@@ -108,14 +149,24 @@ class WorkflowRunState(str, Enum):
|
||||
|
||||
|
||||
class WorkflowStatusEvent(WorkflowEvent):
|
||||
"""Event indicating a transition in the workflow run state."""
|
||||
"""Built-in lifecycle event emitted for workflow run state transitions."""
|
||||
|
||||
def __init__(self, state: WorkflowRunState, data: Any | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
state: WorkflowRunState,
|
||||
data: Any | None = None,
|
||||
):
|
||||
"""Initialize the workflow status event with a new state and optional data.
|
||||
|
||||
Args:
|
||||
state: The new state of the workflow run.
|
||||
data: Optional additional data associated with the state change.
|
||||
"""
|
||||
super().__init__(data)
|
||||
self.state = state
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover - representation only
|
||||
return f"{self.__class__.__name__}(state={self.state}, data={self.data!r})"
|
||||
return f"{self.__class__.__name__}(state={self.state}, data={self.data!r}, origin={self.origin})"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -151,14 +202,18 @@ class WorkflowErrorDetails:
|
||||
|
||||
|
||||
class WorkflowFailedEvent(WorkflowEvent):
|
||||
"""Terminal failure event for a workflow run."""
|
||||
"""Built-in lifecycle event emitted when a workflow run terminates with an error."""
|
||||
|
||||
def __init__(self, details: WorkflowErrorDetails, data: Any | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
details: WorkflowErrorDetails,
|
||||
data: Any | None = None,
|
||||
):
|
||||
super().__init__(data)
|
||||
self.details = details
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover - representation only
|
||||
return f"{self.__class__.__name__}(details={self.details}, data={self.data!r})"
|
||||
return f"{self.__class__.__name__}(details={self.details}, data={self.data!r}, origin={self.origin})"
|
||||
|
||||
|
||||
class RequestInfoEvent(WorkflowEvent):
|
||||
@@ -208,12 +263,12 @@ class ExecutorEvent(WorkflowEvent):
|
||||
return f"{self.__class__.__name__}(executor_id={self.executor_id}, data={self.data})"
|
||||
|
||||
|
||||
class ExecutorInvokeEvent(ExecutorEvent):
|
||||
class ExecutorInvokedEvent(ExecutorEvent):
|
||||
"""Event triggered when an executor handler is invoked."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string representation of the executor handler invoke event."""
|
||||
return f"{self.__class__.__name__}(executor_id={self.executor_id})"
|
||||
return f"{self.__class__.__name__}(executor_id={self.executor_id}, data={self.data})"
|
||||
|
||||
|
||||
class ExecutorCompletedEvent(ExecutorEvent):
|
||||
@@ -221,13 +276,17 @@ class ExecutorCompletedEvent(ExecutorEvent):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string representation of the executor handler complete event."""
|
||||
return f"{self.__class__.__name__}(executor_id={self.executor_id})"
|
||||
return f"{self.__class__.__name__}(executor_id={self.executor_id}, data={self.data})"
|
||||
|
||||
|
||||
class ExecutorFailedEvent(ExecutorEvent):
|
||||
"""Event triggered when an executor handler raises an error."""
|
||||
|
||||
def __init__(self, executor_id: str, details: WorkflowErrorDetails):
|
||||
def __init__(
|
||||
self,
|
||||
executor_id: str,
|
||||
details: WorkflowErrorDetails,
|
||||
):
|
||||
super().__init__(executor_id, details)
|
||||
self.details = details
|
||||
|
||||
@@ -257,3 +316,6 @@ class AgentRunEvent(ExecutorEvent):
|
||||
def __repr__(self) -> str:
|
||||
"""Return a string representation of the agent run event."""
|
||||
return f"{self.__class__.__name__}(executor_id={self.executor_id}, data={self.data})"
|
||||
|
||||
|
||||
WorkflowLifecycleEvent: TypeAlias = WorkflowStartedEvent | WorkflowStatusEvent | WorkflowFailedEvent
|
||||
|
||||
@@ -21,8 +21,9 @@ from ._events import (
|
||||
AgentRunEvent,
|
||||
AgentRunUpdateEvent,
|
||||
ExecutorCompletedEvent,
|
||||
ExecutorInvokeEvent,
|
||||
ExecutorInvokedEvent,
|
||||
RequestInfoEvent,
|
||||
_framework_event_origin, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
from ._typing_utils import is_instance_of
|
||||
from ._workflow_context import WorkflowContext
|
||||
@@ -102,16 +103,22 @@ class Executor(AFBaseModel):
|
||||
# Lazy registration for SubWorkflowRequestInfo if we have interceptors
|
||||
if self._request_interceptors and message.__class__.__name__ == "SubWorkflowRequestInfo":
|
||||
# Directly handle SubWorkflowRequestInfo
|
||||
await context.add_event(ExecutorInvokeEvent(self.id))
|
||||
with _framework_event_origin():
|
||||
invoke_event = ExecutorInvokedEvent(self.id)
|
||||
await context.add_event(invoke_event)
|
||||
try:
|
||||
await self._handle_sub_workflow_request(message, context)
|
||||
except Exception as exc:
|
||||
# Surface structured executor failure before propagating
|
||||
from ._events import ExecutorFailedEvent, WorkflowErrorDetails
|
||||
|
||||
await context.add_event(ExecutorFailedEvent(self.id, WorkflowErrorDetails.from_exception(exc)))
|
||||
with _framework_event_origin():
|
||||
failure_event = ExecutorFailedEvent(self.id, WorkflowErrorDetails.from_exception(exc))
|
||||
await context.add_event(failure_event)
|
||||
raise
|
||||
await context.add_event(ExecutorCompletedEvent(self.id))
|
||||
with _framework_event_origin():
|
||||
completed_event = ExecutorCompletedEvent(self.id)
|
||||
await context.add_event(completed_event)
|
||||
return
|
||||
|
||||
handler: Callable[[Any, WorkflowContext[Any]], Any] | None = None
|
||||
@@ -122,16 +129,22 @@ class Executor(AFBaseModel):
|
||||
|
||||
if handler is None:
|
||||
raise RuntimeError(f"Executor {self.__class__.__name__} cannot handle message of type {type(message)}.")
|
||||
await context.add_event(ExecutorInvokeEvent(self.id))
|
||||
with _framework_event_origin():
|
||||
invoke_event = ExecutorInvokedEvent(self.id)
|
||||
await context.add_event(invoke_event)
|
||||
try:
|
||||
await handler(message, context)
|
||||
except Exception as exc:
|
||||
# Surface structured executor failure before propagating
|
||||
from ._events import ExecutorFailedEvent, WorkflowErrorDetails
|
||||
|
||||
await context.add_event(ExecutorFailedEvent(self.id, WorkflowErrorDetails.from_exception(exc)))
|
||||
with _framework_event_origin():
|
||||
failure_event = ExecutorFailedEvent(self.id, WorkflowErrorDetails.from_exception(exc))
|
||||
await context.add_event(failure_event)
|
||||
raise
|
||||
await context.add_event(ExecutorCompletedEvent(self.id))
|
||||
with _framework_event_origin():
|
||||
completed_event = ExecutorCompletedEvent(self.id)
|
||||
await context.add_event(completed_event)
|
||||
|
||||
def _discover_handlers(self) -> None:
|
||||
"""Discover message handlers and request interceptors in the executor class."""
|
||||
|
||||
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from ._edge import EdgeGroup
|
||||
from ._edge_runner import EdgeRunner, create_edge_runner
|
||||
from ._events import WorkflowCompletedEvent, WorkflowEvent
|
||||
from ._events import WorkflowCompletedEvent, WorkflowEvent, _framework_event_origin
|
||||
from ._executor import Executor
|
||||
from ._runner_context import Message, RunnerContext
|
||||
from ._shared_state import SharedState
|
||||
@@ -280,7 +280,9 @@ class Runner:
|
||||
if isinstance(message.data, AgentExecutorResponse):
|
||||
final_messages = message.data.agent_run_response.messages
|
||||
final_text = final_messages[-1].text if final_messages else "(no content)"
|
||||
await self._ctx.add_event(WorkflowCompletedEvent(final_text))
|
||||
with _framework_event_origin():
|
||||
completion_event = WorkflowCompletedEvent(final_text)
|
||||
await self._ctx.add_event(completion_event)
|
||||
continue # Terminal handled
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
logger.debug("Suppressed exception during terminal message type check: %s", exc)
|
||||
@@ -301,7 +303,9 @@ class Runner:
|
||||
# Emit a single completion event with final text (best-effort extraction)
|
||||
final_messages = message.data.agent_run_response.messages
|
||||
final_text = final_messages[-1].text if final_messages else "(no content)"
|
||||
await self._ctx.add_event(WorkflowCompletedEvent(final_text))
|
||||
with _framework_event_origin():
|
||||
completion_event = WorkflowCompletedEvent(final_text)
|
||||
await self._ctx.add_event(completion_event)
|
||||
continue
|
||||
except Exception as exc: # pragma: no cover
|
||||
logger.debug("Terminal completion emission failed: %s", exc)
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
import sys
|
||||
import uuid
|
||||
from collections.abc import AsyncIterable, Awaitable, Callable, Sequence
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@@ -35,6 +35,7 @@ from ._events import (
|
||||
WorkflowRunState,
|
||||
WorkflowStartedEvent,
|
||||
WorkflowStatusEvent,
|
||||
_framework_event_origin,
|
||||
)
|
||||
from ._executor import AgentExecutor, Executor, RequestInfoExecutor
|
||||
from ._runner import Runner
|
||||
@@ -52,14 +53,6 @@ else:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _default_edge_groups() -> list[EdgeGroup]:
|
||||
return []
|
||||
|
||||
|
||||
def _default_executors() -> dict[str, Executor]:
|
||||
return {}
|
||||
|
||||
|
||||
class WorkflowRunResult(list[WorkflowEvent]):
|
||||
"""A list of events generated during the workflow execution in non-streaming mode.
|
||||
|
||||
@@ -125,10 +118,10 @@ class Workflow(AFBaseModel):
|
||||
"""
|
||||
|
||||
edge_groups: list[EdgeGroup] = Field(
|
||||
default_factory=_default_edge_groups, description="List of edge groups that define the workflow edges"
|
||||
default_factory=list, description="List of edge groups that define the workflow edges"
|
||||
)
|
||||
executors: dict[str, Executor] = Field(
|
||||
default_factory=_default_executors, description="Dictionary mapping executor IDs to Executor instances"
|
||||
default_factory=dict, description="Dictionary mapping executor IDs to Executor instances"
|
||||
)
|
||||
start_executor_id: str = Field(min_length=1, description="The ID of the starting executor for the workflow")
|
||||
max_iterations: int = Field(
|
||||
@@ -190,20 +183,21 @@ class Workflow(AFBaseModel):
|
||||
|
||||
# Ensure WorkflowExecutor instances have their workflow field serialized
|
||||
if "executors" in data:
|
||||
executors_data = cast(dict[str, Any], data["executors"])
|
||||
executor_map: dict[str, Executor] = self.executors
|
||||
executors_data = data["executors"]
|
||||
for executor_id, executor_data in executors_data.items():
|
||||
# Check if this is a WorkflowExecutor that might be missing its workflow field
|
||||
if isinstance(executor_data, dict):
|
||||
executor_dict = cast(dict[str, Any], executor_data)
|
||||
if executor_dict.get("type") == "WorkflowExecutor" and "workflow" not in executor_dict:
|
||||
# Get the original executor object and serialize its workflow
|
||||
original_executor = executor_map.get(executor_id)
|
||||
if original_executor is not None and hasattr(original_executor, "workflow"):
|
||||
from ._executor import WorkflowExecutor
|
||||
if (
|
||||
isinstance(executor_data, dict)
|
||||
and executor_data.get("type") == "WorkflowExecutor"
|
||||
and "workflow" not in executor_data
|
||||
):
|
||||
# Get the original executor object and serialize its workflow
|
||||
original_executor = self.executors.get(executor_id)
|
||||
if original_executor and hasattr(original_executor, "workflow"):
|
||||
from ._executor import WorkflowExecutor
|
||||
|
||||
if isinstance(original_executor, WorkflowExecutor):
|
||||
executor_dict["workflow"] = original_executor.workflow.model_dump(**kwargs)
|
||||
if isinstance(original_executor, WorkflowExecutor):
|
||||
executor_data["workflow"] = original_executor.workflow.model_dump(**kwargs)
|
||||
|
||||
return data
|
||||
|
||||
@@ -252,8 +246,12 @@ class Workflow(AFBaseModel):
|
||||
# Add workflow started event (telemetry + surface state to consumers)
|
||||
workflow_tracer.add_workflow_event("workflow.started")
|
||||
# Emit explicit start/status events to the stream
|
||||
yield WorkflowStartedEvent()
|
||||
yield WorkflowStatusEvent(WorkflowRunState.IN_PROGRESS)
|
||||
with _framework_event_origin():
|
||||
started = WorkflowStartedEvent()
|
||||
yield started
|
||||
with _framework_event_origin():
|
||||
in_progress = WorkflowStatusEvent(WorkflowRunState.IN_PROGRESS)
|
||||
yield in_progress
|
||||
|
||||
# Reset context for a new run if supported
|
||||
if reset_context:
|
||||
@@ -274,22 +272,34 @@ class Workflow(AFBaseModel):
|
||||
|
||||
if isinstance(event, RequestInfoEvent) and not emitted_in_progress_pending and not saw_completed:
|
||||
emitted_in_progress_pending = True
|
||||
yield WorkflowStatusEvent(WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS)
|
||||
with _framework_event_origin():
|
||||
pending_status = WorkflowStatusEvent(WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS)
|
||||
yield pending_status
|
||||
|
||||
# Success path: emit a final status based on observed terminal signals
|
||||
if saw_completed:
|
||||
yield WorkflowStatusEvent(WorkflowRunState.COMPLETED)
|
||||
with _framework_event_origin():
|
||||
terminal_status = WorkflowStatusEvent(WorkflowRunState.COMPLETED)
|
||||
yield terminal_status
|
||||
elif saw_request:
|
||||
yield WorkflowStatusEvent(WorkflowRunState.IDLE_WITH_PENDING_REQUESTS)
|
||||
with _framework_event_origin():
|
||||
terminal_status = WorkflowStatusEvent(WorkflowRunState.IDLE_WITH_PENDING_REQUESTS)
|
||||
yield terminal_status
|
||||
else:
|
||||
yield WorkflowStatusEvent(WorkflowRunState.IDLE)
|
||||
with _framework_event_origin():
|
||||
terminal_status = WorkflowStatusEvent(WorkflowRunState.IDLE)
|
||||
yield terminal_status
|
||||
|
||||
workflow_tracer.add_workflow_event("workflow.completed")
|
||||
except Exception as e:
|
||||
# Surface structured failure details before propagating exception
|
||||
details = WorkflowErrorDetails.from_exception(e)
|
||||
yield WorkflowFailedEvent(details)
|
||||
yield WorkflowStatusEvent(WorkflowRunState.FAILED)
|
||||
with _framework_event_origin():
|
||||
failed_event = WorkflowFailedEvent(details)
|
||||
yield failed_event
|
||||
with _framework_event_origin():
|
||||
failed_status = WorkflowStatusEvent(WorkflowRunState.FAILED)
|
||||
yield failed_status
|
||||
workflow_tracer.add_workflow_error_event(e)
|
||||
raise
|
||||
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import Any, Generic, TypeVar
|
||||
import logging
|
||||
from typing import Any, Generic, TypeVar, cast, get_args
|
||||
|
||||
from opentelemetry.propagate import inject
|
||||
|
||||
from ._events import WorkflowEvent
|
||||
from ._events import (
|
||||
WorkflowEvent,
|
||||
WorkflowEventSource,
|
||||
WorkflowFailedEvent,
|
||||
WorkflowLifecycleEvent,
|
||||
WorkflowStartedEvent,
|
||||
WorkflowStatusEvent,
|
||||
WorkflowWarningEvent,
|
||||
)
|
||||
from ._runner_context import Message, RunnerContext
|
||||
from ._shared_state import SharedState
|
||||
from ._telemetry import workflow_tracer
|
||||
@@ -12,6 +21,20 @@ from ._telemetry import workflow_tracer
|
||||
T_Out = TypeVar("T_Out")
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_FRAMEWORK_LIFECYCLE_EVENT_TYPES: tuple[type[WorkflowEvent], ...] = cast(
|
||||
tuple[type[WorkflowEvent], ...],
|
||||
tuple(get_args(WorkflowLifecycleEvent))
|
||||
or (
|
||||
WorkflowStartedEvent,
|
||||
WorkflowStatusEvent,
|
||||
WorkflowFailedEvent,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class WorkflowContext(Generic[T_Out]):
|
||||
"""Context for executors in a workflow.
|
||||
|
||||
@@ -77,6 +100,16 @@ class WorkflowContext(Generic[T_Out]):
|
||||
|
||||
async def add_event(self, event: WorkflowEvent) -> None:
|
||||
"""Add an event to the workflow context."""
|
||||
if event.origin == WorkflowEventSource.EXECUTOR and isinstance(event, _FRAMEWORK_LIFECYCLE_EVENT_TYPES):
|
||||
event_name = event.__class__.__name__
|
||||
warning_msg = (
|
||||
f"Executor '{self._executor_id}' attempted to emit {event_name}, "
|
||||
"which is reserved for framework lifecycle notifications. The "
|
||||
"event was ignored."
|
||||
)
|
||||
logger.warning(warning_msg)
|
||||
await self._runner_context.add_event(WorkflowWarningEvent(warning_msg))
|
||||
return
|
||||
await self._runner_context.add_event(event)
|
||||
|
||||
async def get_shared_state(self, key: str) -> Any:
|
||||
|
||||
Reference in New Issue
Block a user