mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] Python: Refactor workflow events to unified discriminated union pattern (#3690)
* Refactor events * Merge main * Fixes * Cleanup * Update samples and tests * Remove unused imports * PR feedback * Merge main. Add properties for events to help typing * Formatting * Cleanup * use builtins.type to avoid shadowing by WorkflowEvent.type attribute * Final improvements
This commit is contained in:
committed by
GitHub
Unverified
parent
09f59b21ad
commit
0f3f4dbcaf
@@ -32,10 +32,8 @@ from agent_framework import (
|
||||
from agent_framework._clients import TOptions_co
|
||||
from agent_framework._workflows._agent_executor import AgentExecutorResponse
|
||||
from agent_framework._workflows._events import (
|
||||
ExecutorCompletedEvent,
|
||||
ExecutorFailedEvent,
|
||||
ExecutorInvokedEvent,
|
||||
WorkflowErrorDetails,
|
||||
WorkflowEvent,
|
||||
)
|
||||
from agent_framework.orchestrations import ConcurrentBuilder, SequentialBuilder
|
||||
|
||||
@@ -284,7 +282,8 @@ def _create_agent_executor_response(
|
||||
executor_id: str = "test_executor",
|
||||
response_text: str = "Executor response",
|
||||
) -> AgentExecutorResponse:
|
||||
"""Create an AgentExecutorResponse - the type that's nested in ExecutorCompletedEvent.data."""
|
||||
"""Create an AgentExecutorResponse - the type that's nested in
|
||||
executor_completed event (type='executor_completed').data."""
|
||||
agent_response = _create_agent_run_response(response_text)
|
||||
return AgentExecutorResponse(
|
||||
executor_id=executor_id,
|
||||
@@ -306,32 +305,32 @@ def create_agent_run_response(text: str = "Test response") -> AgentResponse:
|
||||
return _create_agent_run_response(text)
|
||||
|
||||
|
||||
def create_executor_invoked_event(executor_id: str = "test_executor") -> ExecutorInvokedEvent:
|
||||
"""Create an ExecutorInvokedEvent."""
|
||||
return ExecutorInvokedEvent(executor_id=executor_id)
|
||||
def create_executor_invoked_event(executor_id: str = "test_executor") -> WorkflowEvent[Any]:
|
||||
"""Create a WorkflowEvent(type='executor_invoked')."""
|
||||
return WorkflowEvent.executor_invoked(executor_id=executor_id)
|
||||
|
||||
|
||||
def create_executor_completed_event(
|
||||
executor_id: str = "test_executor",
|
||||
with_agent_response: bool = True,
|
||||
) -> ExecutorCompletedEvent:
|
||||
"""Create an ExecutorCompletedEvent with realistic nested data.
|
||||
) -> WorkflowEvent[Any]:
|
||||
"""Create a WorkflowEvent(type='executor_completed') with realistic nested data.
|
||||
|
||||
This creates the exact data structure that caused the serialization bug:
|
||||
ExecutorCompletedEvent.data contains AgentExecutorResponse which contains
|
||||
WorkflowEvent.data contains AgentExecutorResponse which contains
|
||||
AgentResponse and ChatMessage objects (SerializationMixin, not Pydantic).
|
||||
"""
|
||||
data = _create_agent_executor_response(executor_id) if with_agent_response else {"simple": "dict"}
|
||||
return ExecutorCompletedEvent(executor_id=executor_id, data=data)
|
||||
return WorkflowEvent.executor_completed(executor_id=executor_id, data=data)
|
||||
|
||||
|
||||
def create_executor_failed_event(
|
||||
executor_id: str = "test_executor",
|
||||
error_message: str = "Test error",
|
||||
) -> ExecutorFailedEvent:
|
||||
"""Create an ExecutorFailedEvent."""
|
||||
) -> WorkflowEvent[WorkflowErrorDetails]:
|
||||
"""Create a WorkflowEvent(type='executor_failed')."""
|
||||
details = WorkflowErrorDetails(error_type="TestError", message=error_message)
|
||||
return ExecutorFailedEvent(executor_id=executor_id, details=details)
|
||||
return WorkflowEvent.executor_failed(executor_id=executor_id, details=details)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -386,28 +385,28 @@ def agent_run_response() -> AgentResponse:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def executor_completed_event() -> ExecutorCompletedEvent:
|
||||
"""Create an ExecutorCompletedEvent with realistic nested data.
|
||||
def executor_completed_event() -> WorkflowEvent[Any]:
|
||||
"""Create a WorkflowEvent(type='executor_completed') with realistic nested data.
|
||||
|
||||
This creates the exact data structure that caused the serialization bug:
|
||||
ExecutorCompletedEvent.data contains AgentExecutorResponse which contains
|
||||
executor_completed event (type='executor_completed').data contains AgentExecutorResponse which contains
|
||||
AgentResponse and ChatMessage objects (SerializationMixin, not Pydantic).
|
||||
"""
|
||||
data = _create_agent_executor_response("test_executor")
|
||||
return ExecutorCompletedEvent(executor_id="test_executor", data=data)
|
||||
return WorkflowEvent.executor_completed(executor_id="test_executor", data=data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def executor_invoked_event() -> ExecutorInvokedEvent:
|
||||
"""Create an ExecutorInvokedEvent."""
|
||||
return ExecutorInvokedEvent(executor_id="test_executor")
|
||||
def executor_invoked_event() -> WorkflowEvent[Any]:
|
||||
"""Create a WorkflowEvent(type='executor_invoked')."""
|
||||
return WorkflowEvent.executor_invoked(executor_id="test_executor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def executor_failed_event() -> ExecutorFailedEvent:
|
||||
"""Create an ExecutorFailedEvent."""
|
||||
def executor_failed_event() -> WorkflowEvent[WorkflowErrorDetails]:
|
||||
"""Create a WorkflowEvent(type='executor_failed')."""
|
||||
details = WorkflowErrorDetails(error_type="TestError", message="Test error")
|
||||
return ExecutorFailedEvent(executor_id="test_executor", details=details)
|
||||
return WorkflowEvent.executor_failed(executor_id="test_executor", details=details)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -8,10 +8,8 @@ import pytest
|
||||
from agent_framework import (
|
||||
Executor,
|
||||
InMemoryCheckpointStorage,
|
||||
RequestInfoEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowStatusEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
@@ -428,13 +426,13 @@ class TestIntegration:
|
||||
# Run workflow until it reaches IDLE_WITH_PENDING_REQUESTS (after checkpoint is created)
|
||||
saw_request_event = False
|
||||
async for event in test_workflow.run(WorkflowTestData(value="test"), stream=True):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
if event.type == "request_info":
|
||||
saw_request_event = True
|
||||
# Wait for IDLE_WITH_PENDING_REQUESTS status (comes after checkpoint creation)
|
||||
if isinstance(event, WorkflowStatusEvent) and "IDLE_WITH_PENDING_REQUESTS" in str(event.state):
|
||||
if event.type == "status" and "IDLE_WITH_PENDING_REQUESTS" in str(event.state):
|
||||
break
|
||||
|
||||
assert saw_request_event, "Test workflow should have emitted RequestInfoEvent"
|
||||
assert saw_request_event, "Test workflow should have emitted request_info event (type='request_info')"
|
||||
|
||||
# Verify checkpoint was AUTOMATICALLY saved to our storage by the framework
|
||||
checkpoints_after = await checkpoint_storage.list_checkpoints()
|
||||
|
||||
@@ -292,7 +292,7 @@ async def test_full_pipeline_workflow_events_are_json_serializable():
|
||||
"""CRITICAL TEST: Verify ALL events from workflow execution can be JSON serialized.
|
||||
|
||||
This is particularly important for workflows with AgentExecutor because:
|
||||
- AgentExecutor produces ExecutorCompletedEvent with AgentExecutorResponse
|
||||
- AgentExecutor produces executor_completed event (type='executor_completed') with AgentExecutorResponse
|
||||
- AgentExecutorResponse contains AgentResponse and ChatMessage objects
|
||||
- These are SerializationMixin objects, not Pydantic, which caused the original bug
|
||||
|
||||
@@ -672,10 +672,10 @@ async def test_full_pipeline_concurrent_workflow(concurrent_workflow):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_pipeline_workflow_output_event_serialization():
|
||||
"""Test that WorkflowOutputEvent from ctx.yield_output() serializes correctly.
|
||||
"""Test that output event (type='output') from ctx.yield_output() serializes correctly.
|
||||
|
||||
This tests the pattern where executors yield output via ctx.yield_output(),
|
||||
which emits WorkflowOutputEvent that DevUI must serialize for SSE.
|
||||
which emits output event (type='output') that DevUI must serialize for SSE.
|
||||
"""
|
||||
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
|
||||
|
||||
|
||||
@@ -19,9 +19,8 @@ from agent_framework._types import (
|
||||
|
||||
# Import real workflow event classes - NOT mocks!
|
||||
from agent_framework._workflows._events import (
|
||||
ExecutorCompletedEvent,
|
||||
WorkflowStartedEvent,
|
||||
WorkflowStatusEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowRunState,
|
||||
)
|
||||
|
||||
# Import factory functions from conftest for parameterized test data creation
|
||||
@@ -261,7 +260,7 @@ async def test_agent_run_response_mapping(mapper: MessageMapper, test_request: A
|
||||
|
||||
|
||||
async def test_executor_invoked_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test ExecutorInvokedEvent using the REAL class from agent_framework."""
|
||||
"""Test WorkflowEvent(type='executor_invoked') using the REAL class from agent_framework."""
|
||||
# Use real class, not mock!
|
||||
event = create_executor_invoked_event(executor_id="exec_123")
|
||||
|
||||
@@ -277,9 +276,9 @@ async def test_executor_invoked_event(mapper: MessageMapper, test_request: Agent
|
||||
|
||||
|
||||
async def test_executor_completed_event_simple_data(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test ExecutorCompletedEvent with simple dict data."""
|
||||
"""Test WorkflowEvent(type='executor_completed') with simple dict data."""
|
||||
# Create event with simple data
|
||||
event = ExecutorCompletedEvent(executor_id="exec_123", data={"simple": "result"})
|
||||
event = WorkflowEvent.executor_completed(executor_id="exec_123", data={"simple": "result"})
|
||||
|
||||
# First need to invoke the executor to set up context
|
||||
invoke_event = create_executor_invoked_event(executor_id="exec_123")
|
||||
@@ -301,10 +300,10 @@ async def test_executor_completed_event_simple_data(mapper: MessageMapper, test_
|
||||
async def test_executor_completed_event_with_agent_response(
|
||||
mapper: MessageMapper, test_request: AgentFrameworkRequest
|
||||
) -> None:
|
||||
"""Test ExecutorCompletedEvent with nested AgentExecutorResponse.
|
||||
"""Test WorkflowEvent(type='executor_completed') with nested AgentExecutorResponse.
|
||||
|
||||
This is a REGRESSION TEST for the serialization bug where
|
||||
ExecutorCompletedEvent.data contained AgentExecutorResponse with nested
|
||||
WorkflowEvent.data contained AgentExecutorResponse with nested
|
||||
AgentResponse and ChatMessage objects (SerializationMixin) that
|
||||
Pydantic couldn't serialize.
|
||||
"""
|
||||
@@ -374,7 +373,7 @@ async def test_executor_completed_event_serialization_to_json(
|
||||
|
||||
|
||||
async def test_executor_failed_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test ExecutorFailedEvent using the REAL class."""
|
||||
"""Test WorkflowEvent(type='executor_failed') using the REAL class."""
|
||||
# First invoke the executor
|
||||
invoke_event = create_executor_invoked_event(executor_id="exec_fail")
|
||||
await mapper.convert_event(invoke_event, test_request)
|
||||
@@ -398,22 +397,21 @@ async def test_executor_failed_event(mapper: MessageMapper, test_request: AgentF
|
||||
|
||||
|
||||
async def test_workflow_started_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowStartedEvent using the REAL class."""
|
||||
"""Test WorkflowEvent(type='started') using the REAL class."""
|
||||
|
||||
event = WorkflowStartedEvent(data=None)
|
||||
event = WorkflowEvent.started()
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# WorkflowStartedEvent should emit response.created and response.in_progress
|
||||
# WorkflowEvent(type='started') should emit response.created and response.in_progress
|
||||
assert len(events) == 2
|
||||
assert events[0].type == "response.created"
|
||||
assert events[1].type == "response.in_progress"
|
||||
|
||||
|
||||
async def test_workflow_status_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowStatusEvent using the REAL class."""
|
||||
from agent_framework._workflows._events import WorkflowRunState
|
||||
"""Test WorkflowEvent(type='status') using the REAL class."""
|
||||
|
||||
event = WorkflowStatusEvent(state=WorkflowRunState.IN_PROGRESS)
|
||||
event = WorkflowEvent.status(state=WorkflowRunState.IN_PROGRESS)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# Should emit some status-related event
|
||||
@@ -421,20 +419,20 @@ async def test_workflow_status_event(mapper: MessageMapper, test_request: AgentF
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Magentic Event Tests - Testing WorkflowOutputEvent with additional_properties
|
||||
# Magentic Event Tests - Testing WorkflowEvent[AgentResponseUpdate] with additional_properties
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def test_magentic_agent_run_update_event_with_agent_delta_metadata(
|
||||
async def test_magentic_executor_event_with_agent_delta_metadata(
|
||||
mapper: MessageMapper, test_request: AgentFrameworkRequest
|
||||
) -> None:
|
||||
"""Test that WorkflowOutputEvent with magentic_event_type='agent_delta' is handled correctly.
|
||||
"""Test that WorkflowEvent[AgentResponseUpdate] with magentic_event_type='agent_delta' is handled correctly.
|
||||
|
||||
This tests the ACTUAL event format Magentic emits - not a fake MagenticAgentDeltaEvent class.
|
||||
Magentic uses WorkflowOutputEvent wrapping AgentResponseUpdate with additional_properties.
|
||||
Magentic uses WorkflowEvent.emit() with additional_properties containing magentic_event_type.
|
||||
"""
|
||||
from agent_framework._types import AgentResponseUpdate
|
||||
from agent_framework._workflows._events import WorkflowOutputEvent
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
# Create the REAL event format that Magentic emits
|
||||
update = AgentResponseUpdate(
|
||||
@@ -446,11 +444,11 @@ async def test_magentic_agent_run_update_event_with_agent_delta_metadata(
|
||||
"agent_id": "writer_agent",
|
||||
},
|
||||
)
|
||||
event = WorkflowOutputEvent(executor_id="magentic_executor", data=update)
|
||||
event = WorkflowEvent.emit(executor_id="magentic_executor", data=update)
|
||||
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# Should be treated as a regular WorkflowOutputEvent with text content
|
||||
# Should be treated as a regular WorkflowEvent[AgentResponseUpdate] with text content
|
||||
# The mapper should emit text delta events
|
||||
assert len(events) >= 1
|
||||
text_events = [e for e in events if getattr(e, "type", "") == "response.output_text.delta"]
|
||||
@@ -459,13 +457,13 @@ async def test_magentic_agent_run_update_event_with_agent_delta_metadata(
|
||||
|
||||
|
||||
async def test_magentic_orchestrator_message_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test that WorkflowOutputEvent with magentic_event_type='orchestrator_message' is handled.
|
||||
"""Test that WorkflowEvent[AgentResponseUpdate] with magentic_event_type='orchestrator_message' is handled.
|
||||
|
||||
Magentic emits orchestrator planning/instruction messages using WorkflowOutputEvent
|
||||
wrapping AgentResponseUpdate with additional_properties.
|
||||
Magentic emits orchestrator planning/instruction messages using WorkflowEvent.emit()
|
||||
with additional_properties containing magentic_event_type='orchestrator_message'.
|
||||
"""
|
||||
from agent_framework._types import AgentResponseUpdate
|
||||
from agent_framework._workflows._events import WorkflowOutputEvent
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
# Create orchestrator message event (REAL format from Magentic)
|
||||
update = AgentResponseUpdate(
|
||||
@@ -478,11 +476,11 @@ async def test_magentic_orchestrator_message_event(mapper: MessageMapper, test_r
|
||||
"orchestrator_id": "magentic_orchestrator",
|
||||
},
|
||||
)
|
||||
event = WorkflowOutputEvent(executor_id="magentic_orchestrator", data=update)
|
||||
event = WorkflowEvent.emit(executor_id="magentic_orchestrator", data=update)
|
||||
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# Currently, mapper treats this as regular WorkflowOutputEvent (no special handling)
|
||||
# Currently, mapper treats this as regular WorkflowEvent[AgentResponseUpdate] (no special handling)
|
||||
# This test documents the current behavior
|
||||
assert len(events) >= 1
|
||||
text_events = [e for e in events if getattr(e, "type", "") == "response.output_text.delta"]
|
||||
@@ -493,15 +491,15 @@ async def test_magentic_orchestrator_message_event(mapper: MessageMapper, test_r
|
||||
async def test_magentic_events_use_same_event_class_as_other_workflows(
|
||||
mapper: MessageMapper, test_request: AgentFrameworkRequest
|
||||
) -> None:
|
||||
"""Verify Magentic uses the same WorkflowOutputEvent class as other workflows.
|
||||
"""Verify Magentic uses the same WorkflowEvent class as other workflows.
|
||||
|
||||
This test documents that Magentic does NOT define separate event classes like
|
||||
MagenticAgentDeltaEvent - it reuses WorkflowOutputEvent with metadata in
|
||||
MagenticAgentDeltaEvent - it reuses WorkflowEvent with metadata in
|
||||
additional_properties. Any mapper code checking for 'MagenticAgentDeltaEvent'
|
||||
class names is dead code.
|
||||
"""
|
||||
from agent_framework._types import AgentResponseUpdate
|
||||
from agent_framework._workflows._events import WorkflowOutputEvent
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
# Create events the way different workflows do it
|
||||
# 1. Regular workflow (no additional_properties)
|
||||
@@ -509,7 +507,7 @@ async def test_magentic_events_use_same_event_class_as_other_workflows(
|
||||
contents=[Content.from_text(text="Regular workflow response")],
|
||||
role="assistant",
|
||||
)
|
||||
regular_event = WorkflowOutputEvent(executor_id="regular_executor", data=regular_update)
|
||||
regular_event = WorkflowEvent.emit(executor_id="regular_executor", data=regular_update)
|
||||
|
||||
# 2. Magentic workflow (with additional_properties)
|
||||
magentic_update = AgentResponseUpdate(
|
||||
@@ -517,12 +515,12 @@ async def test_magentic_events_use_same_event_class_as_other_workflows(
|
||||
role="assistant",
|
||||
additional_properties={"magentic_event_type": "agent_delta"},
|
||||
)
|
||||
magentic_event = WorkflowOutputEvent(executor_id="magentic_executor", data=magentic_update)
|
||||
magentic_event = WorkflowEvent.emit(executor_id="magentic_executor", data=magentic_update)
|
||||
|
||||
# Both should be the SAME class
|
||||
assert type(regular_event) is type(magentic_event)
|
||||
assert isinstance(regular_event, WorkflowOutputEvent)
|
||||
assert isinstance(magentic_event, WorkflowOutputEvent)
|
||||
assert isinstance(regular_event, WorkflowEvent)
|
||||
assert isinstance(magentic_event, WorkflowEvent)
|
||||
|
||||
# Both should be handled by the same isinstance check in mapper
|
||||
regular_events = await mapper.convert_event(regular_event, test_request)
|
||||
@@ -559,18 +557,18 @@ async def test_unknown_content_fallback(mapper: MessageMapper, test_request: Age
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# WorkflowOutputEvent Tests
|
||||
# output event (type='output') Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def test_workflow_output_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowOutputEvent is converted to output_item.added."""
|
||||
from agent_framework._workflows._events import WorkflowOutputEvent
|
||||
"""Test output event (type='output') is converted to output_item.added."""
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
event = WorkflowOutputEvent(data="Final workflow output", executor_id="final_executor")
|
||||
event = WorkflowEvent.output(executor_id="final_executor", data="Final workflow output")
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# WorkflowOutputEvent should emit output_item.added
|
||||
# output event (type='output') should emit output_item.added
|
||||
assert len(events) == 1
|
||||
assert events[0].type == "response.output_item.added"
|
||||
# Check item contains the output text
|
||||
@@ -580,16 +578,16 @@ async def test_workflow_output_event(mapper: MessageMapper, test_request: AgentF
|
||||
|
||||
|
||||
async def test_workflow_output_event_with_list_data(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowOutputEvent with list data (common for sequential/concurrent workflows)."""
|
||||
"""Test output event (type='output') with list data (common for sequential/concurrent workflows)."""
|
||||
from agent_framework import ChatMessage
|
||||
from agent_framework._workflows._events import WorkflowOutputEvent
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
# Sequential/Concurrent workflows often output list[ChatMessage]
|
||||
messages = [
|
||||
ChatMessage(role="user", contents=[Content.from_text(text="Hello")]),
|
||||
ChatMessage(role="assistant", contents=[Content.from_text(text="World")]),
|
||||
]
|
||||
event = WorkflowOutputEvent(data=messages, executor_id="complete")
|
||||
event = WorkflowEvent.output(executor_id="complete", data=messages)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
assert len(events) == 1
|
||||
@@ -597,23 +595,23 @@ async def test_workflow_output_event_with_list_data(mapper: MessageMapper, test_
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# WorkflowFailedEvent Tests
|
||||
# failed event (type='failed') Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def test_workflow_failed_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowFailedEvent is converted to response.failed."""
|
||||
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowFailedEvent
|
||||
"""Test failed event (type='failed') is converted to response.failed."""
|
||||
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowEvent
|
||||
|
||||
details = WorkflowErrorDetails(
|
||||
error_type="TestError",
|
||||
message="Workflow failed due to test error",
|
||||
executor_id="failing_executor",
|
||||
)
|
||||
event = WorkflowFailedEvent(details=details)
|
||||
event = WorkflowEvent.failed(details=details)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# WorkflowFailedEvent should emit response.failed
|
||||
# failed event (type='failed') should emit response.failed
|
||||
assert len(events) >= 1
|
||||
# Find the failed event
|
||||
failed_events = [e for e in events if getattr(e, "type", "") == "response.failed"]
|
||||
@@ -628,8 +626,8 @@ async def test_workflow_failed_event(mapper: MessageMapper, test_request: AgentF
|
||||
|
||||
|
||||
async def test_workflow_failed_event_with_extra(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowFailedEvent includes extra context when available."""
|
||||
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowFailedEvent
|
||||
"""Test failed event (type='failed') includes extra context when available."""
|
||||
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowEvent
|
||||
|
||||
details = WorkflowErrorDetails(
|
||||
error_type="ValidationError",
|
||||
@@ -637,7 +635,7 @@ async def test_workflow_failed_event_with_extra(mapper: MessageMapper, test_requ
|
||||
executor_id="validation_executor",
|
||||
extra={"field": "email", "reason": "invalid format"},
|
||||
)
|
||||
event = WorkflowFailedEvent(details=details)
|
||||
event = WorkflowEvent.failed(details=details)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
assert len(events) == 1
|
||||
@@ -650,8 +648,8 @@ async def test_workflow_failed_event_with_extra(mapper: MessageMapper, test_requ
|
||||
|
||||
|
||||
async def test_workflow_failed_event_with_traceback(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowFailedEvent includes traceback when available."""
|
||||
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowFailedEvent
|
||||
"""Test failed event (type='failed') includes traceback when available."""
|
||||
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowEvent
|
||||
|
||||
details = WorkflowErrorDetails(
|
||||
error_type="ValueError",
|
||||
@@ -659,7 +657,7 @@ async def test_workflow_failed_event_with_traceback(mapper: MessageMapper, test_
|
||||
traceback="Traceback (most recent call last):\n File ...\nValueError: Invalid input",
|
||||
executor_id="validation_executor",
|
||||
)
|
||||
event = WorkflowFailedEvent(details=details)
|
||||
event = WorkflowEvent.failed(details=details)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
assert len(events) == 1
|
||||
@@ -672,41 +670,41 @@ async def test_workflow_failed_event_with_traceback(mapper: MessageMapper, test_
|
||||
|
||||
|
||||
async def test_workflow_warning_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowWarningEvent is converted to trace event."""
|
||||
from agent_framework._workflows._events import WorkflowWarningEvent
|
||||
"""Test WorkflowEvent(type='warning') is converted to trace event."""
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
event = WorkflowWarningEvent(data="This is a warning message")
|
||||
event = WorkflowEvent.warning("This is a warning message")
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# WorkflowWarningEvent should emit a trace event
|
||||
# WorkflowEvent(type='warning') should emit a trace event
|
||||
assert len(events) == 1
|
||||
assert events[0].type == "response.trace.completed"
|
||||
assert events[0].data["event_type"] == "WorkflowWarningEvent"
|
||||
assert events[0].data["event_type"] == "warning"
|
||||
|
||||
|
||||
async def test_workflow_error_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test WorkflowErrorEvent is converted to trace event."""
|
||||
from agent_framework._workflows._events import WorkflowErrorEvent
|
||||
"""Test WorkflowEvent(type='error') is converted to trace event."""
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
event = WorkflowErrorEvent(data=ValueError("Something went wrong"))
|
||||
event = WorkflowEvent.error(ValueError("Something went wrong"))
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# WorkflowErrorEvent should emit a trace event
|
||||
# WorkflowEvent(type='error') should emit a trace event
|
||||
assert len(events) == 1
|
||||
assert events[0].type == "response.trace.completed"
|
||||
assert events[0].data["event_type"] == "WorkflowErrorEvent"
|
||||
assert events[0].data["event_type"] == "error"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# RequestInfoEvent Tests (Human-in-the-Loop)
|
||||
# request_info event (type='request_info') Tests (Human-in-the-Loop)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def test_request_info_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test RequestInfoEvent is converted to HIL request event."""
|
||||
from agent_framework._workflows._events import RequestInfoEvent
|
||||
"""Test request_info event (type='request_info') is converted to HIL request event."""
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
event = RequestInfoEvent(
|
||||
event = WorkflowEvent.request_info(
|
||||
request_id="req_123",
|
||||
source_executor_id="approval_executor",
|
||||
request_data={"action": "approve", "details": "Please approve this action"},
|
||||
@@ -714,7 +712,7 @@ async def test_request_info_event(mapper: MessageMapper, test_request: AgentFram
|
||||
)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# RequestInfoEvent should emit response.request_info.requested
|
||||
# request_info event (type='request_info') should emit response.request_info.requested
|
||||
assert len(events) >= 1
|
||||
# Check that request info is captured
|
||||
has_hil_event = any(getattr(e, "type", "") == "response.request_info.requested" for e in events)
|
||||
@@ -732,24 +730,24 @@ async def test_request_info_event(mapper: MessageMapper, test_request: AgentFram
|
||||
|
||||
|
||||
async def test_superstep_started_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test SuperStepStartedEvent is handled gracefully."""
|
||||
from agent_framework._workflows._events import SuperStepStartedEvent
|
||||
"""Test superstep_started event (type='superstep_started') is handled gracefully."""
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
event = SuperStepStartedEvent(iteration=1)
|
||||
event = WorkflowEvent.superstep_started(iteration=1)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# SuperStepStartedEvent may not emit events (internal workflow signal)
|
||||
# superstep_started event (type='superstep_started') may not emit events (internal workflow signal)
|
||||
# Just ensure it doesn't crash
|
||||
assert isinstance(events, list)
|
||||
|
||||
|
||||
async def test_superstep_completed_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test SuperStepCompletedEvent is handled gracefully."""
|
||||
from agent_framework._workflows._events import SuperStepCompletedEvent
|
||||
"""Test superstep_completed event (type='superstep_completed') is handled gracefully."""
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
event = SuperStepCompletedEvent(iteration=1)
|
||||
event = WorkflowEvent.superstep_completed(iteration=1)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# SuperStepCompletedEvent may not emit events (internal workflow signal)
|
||||
# superstep_completed event (type='superstep_completed') may not emit events (internal workflow signal)
|
||||
# Just ensure it doesn't crash
|
||||
assert isinstance(events, list)
|
||||
|
||||
Reference in New Issue
Block a user