Python: [Breaking] Remove WorkflowCompletedEvent, introduce workflow output and migrate to ctx.yield_output() + a huge refactoring (#845)

* Introduce input and output types for executor and workflow

* WorkflowOutputContext handles two types

* Remove can_handle_types from Executor

* Update validation

* Move workflow executor

* Move workflow executor

* Fix issues in WorkflowExecutor

* refactor executor

* update execute signature to create workflow context within Executor

* fix simple sub workflow test; fix validation

* fix output types in WorkflowExecutor

* fix issue in Executor handling of SubWorkflowRequestInfo

* update tests to use proper workflow output

* update orchestration patterns to use output

* Update sample -- not finished

* Update python/packages/main/tests/workflow/test_workflow_states.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/packages/main/tests/workflow/test_concurrent.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* address comments

* WorkflowOutputContext --> WorkflowContext

* remove WorkflowCompletedEvent

* update samples

* Update doc string for important classes; update WorkflowExecutor to support concurrent execution

* use Never instead of None for default type

* Update usage of WorkflowContext[None to WorkflowContext[Never

* address comments

* remove filter for None

* address comments, minor fixes

* quality of life improvement on interceptor types

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Eric Zhu
2025-09-23 13:52:53 -07:00
committed by GitHub
Unverified
parent 0f913bcdeb
commit 2133043f11
67 changed files with 2564 additions and 1648 deletions
@@ -23,11 +23,11 @@ from typing import Literal
from agent_framework import (
Executor,
WorkflowBuilder,
WorkflowCompletedEvent,
WorkflowContext,
handler,
)
from pydantic import BaseModel, Field
from typing_extensions import Never
class DataType(Enum):
@@ -315,7 +315,9 @@ class ValidationAggregator(Executor):
"""Aggregates validation results and decides on next steps."""
@handler
async def aggregate_validations(self, reports: list[ValidationReport], ctx: WorkflowContext[DataBatch]) -> None:
async def aggregate_validations(
self, reports: list[ValidationReport], ctx: WorkflowContext[DataBatch, str]
) -> None:
"""Aggregate all validation reports and make processing decision."""
if not reports:
return
@@ -345,11 +347,9 @@ class ValidationAggregator(Executor):
)
reason = " and ".join(failure_reason)
await ctx.add_event(
WorkflowCompletedEvent(
f"Batch {batch_id} failed validation: {reason}. "
f"Total issues: {total_issues}, Quality score: {quality_score:.2f}"
)
await ctx.yield_output(
f"Batch {batch_id} failed validation: {reason}. "
f"Total issues: {total_issues}, Quality score: {quality_score:.2f}"
)
return
@@ -584,10 +584,12 @@ class FinalProcessor(Executor):
"""Final processing stage that combines all results."""
@handler
async def process_final_results(self, assessments: list[QualityAssessment], ctx: WorkflowContext[None]) -> None:
async def process_final_results(
self, assessments: list[QualityAssessment], ctx: WorkflowContext[Never, str]
) -> None:
"""Generate final processing summary and complete workflow."""
if not assessments:
await ctx.add_event(WorkflowCompletedEvent("No quality assessments received"))
await ctx.yield_output("No quality assessments received")
return
batch_id = assessments[0].batch_id
@@ -618,7 +620,7 @@ class FinalProcessor(Executor):
f"🎖️ Final Status: {final_status}"
)
await ctx.add_event(WorkflowCompletedEvent(completion_message))
await ctx.yield_output(completion_message)
# Workflow Builder Helper
@@ -24,11 +24,11 @@ from agent_framework import (
Default,
Executor,
WorkflowBuilder,
WorkflowCompletedEvent,
WorkflowContext,
handler,
)
from pydantic import BaseModel, Field
from typing_extensions import Never
@dataclass
@@ -259,7 +259,7 @@ class FinalProcessor(Executor):
async def handle_processing_result(
self,
result: ProcessingResult,
ctx: WorkflowContext[None],
ctx: WorkflowContext[Never, str],
) -> None:
"""Complete the workflow with final processing and logging."""
await asyncio.sleep(1.5) # Simulate final processing time
@@ -278,7 +278,7 @@ class FinalProcessor(Executor):
f"Total time: {total_time:.1f}s"
)
await ctx.add_event(WorkflowCompletedEvent(completion_message))
await ctx.yield_output(completion_message)
# Create the workflow instance that DevUI can discover