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
@@ -2,7 +2,8 @@
import asyncio
from dataclasses import dataclass
from typing import Any
from typing_extensions import Never
from agent_framework import ( # Core chat primitives to build LLM requests
AgentExecutor, # Wraps an LLM agent for use inside a workflow
@@ -13,8 +14,8 @@ from agent_framework import ( # Core chat primitives to build LLM requests
Executor, # Base class for custom Python executors
Role, # Enum of chat roles (user, assistant, system)
WorkflowBuilder, # Fluent builder for wiring the workflow graph
WorkflowCompletedEvent, # Terminal event carrying the final result
WorkflowContext, # Per run context and event bus
WorkflowOutputEvent, # Event emitted when workflow yields output
handler, # Decorator to mark an Executor method as invokable
)
from agent_framework.azure import AzureChatClient # Client wrapper for Azure OpenAI chat models
@@ -75,7 +76,7 @@ class AggregateInsights(Executor):
self._expert_ids = expert_ids
@handler
async def aggregate(self, results: list[AgentExecutorResponse], ctx: WorkflowContext[Any]) -> None:
async def aggregate(self, results: list[AgentExecutorResponse], ctx: WorkflowContext[Never, str]) -> None:
# Map responses to text by executor id for a simple, predictable demo.
by_id: dict[str, str] = {}
for r in results:
@@ -101,7 +102,7 @@ class AggregateInsights(Executor):
f"Legal/Compliance Notes:\n{aggregated.legal}\n"
)
await ctx.add_event(WorkflowCompletedEvent(data=consolidated))
await ctx.yield_output(consolidated)
async def main() -> None:
@@ -151,17 +152,13 @@ async def main() -> None:
)
# 3) Run with a single prompt and print progress plus the final consolidated output
completion: WorkflowCompletedEvent | None = None
async for event in workflow.run_stream("We are launching a new budget-friendly electric bike for urban commuters."):
if isinstance(event, AgentRunEvent):
# Show which agent ran and what step completed for lightweight observability.
print(event)
if isinstance(event, WorkflowCompletedEvent):
completion = event
if completion:
print("===== Final Aggregated Output =====")
print(completion.data)
elif isinstance(event, WorkflowOutputEvent):
print("===== Final Aggregated Output =====")
print(event.data)
if __name__ == "__main__":
@@ -5,14 +5,15 @@ import asyncio
import os
from collections import defaultdict
from dataclasses import dataclass
from typing import Any
import aiofiles
from typing_extensions import Never
from agent_framework import (
Executor, # Base class for custom workflow steps
WorkflowBuilder, # Fluent graph builder for executors and edges
WorkflowCompletedEvent, # Terminal event that carries final output
WorkflowBuilder, # Fluent builder for executors and edges
WorkflowContext, # Per run context with shared state and messaging
WorkflowOutputEvent, # Event emitted when workflow yields output
WorkflowViz, # Utility to visualize a workflow graph
handler, # Decorator to expose an Executor method as a step
)
@@ -246,12 +247,12 @@ class Reduce(Executor):
class CompletionExecutor(Executor):
"""Joins all reducer outputs and emits the final completion event."""
"""Joins all reducer outputs and yields the final output."""
@handler
async def complete(self, data: list[ReduceCompleted], ctx: WorkflowContext[Any]) -> None:
"""Collect reducer output file paths and publish a terminal event."""
await ctx.add_event(WorkflowCompletedEvent(data=[result.file_path for result in data]))
async def complete(self, data: list[ReduceCompleted], ctx: WorkflowContext[Never, list[str]]) -> None:
"""Collect reducer output file paths and yield final output."""
await ctx.yield_output([result.file_path for result in data])
async def main():
@@ -303,14 +304,10 @@ async def main():
raw_text = await f.read()
# Step 4: Run the workflow with the raw text as input.
completion_event = None
async for event in workflow.run_stream(raw_text):
print(f"Event: {event}")
if isinstance(event, WorkflowCompletedEvent):
completion_event = event
if completion_event:
print(f"Completion Event: {completion_event}")
if isinstance(event, WorkflowOutputEvent):
print(f"Final Output: {event.data}")
if __name__ == "__main__":