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,7 @@
import asyncio
from agent_framework import AgentRunUpdateEvent, WorkflowBuilder, WorkflowCompletedEvent
from agent_framework import AgentRunUpdateEvent, WorkflowBuilder, WorkflowOutputEvent
from agent_framework.azure import AzureChatClient
from azure.identity import AzureCliCredential
@@ -18,7 +18,7 @@ Show how to wire chat agents directly into a WorkflowBuilder pipeline where agen
Demonstrate:
- Automatic streaming of agent deltas via AgentRunUpdateEvent.
- A simple console aggregator that groups updates by executor id and prints them as they arrive.
- A final WorkflowCompletedEvent that contains the reviewer outcome after both agents finish.
- The workflow completes when idle and outputs are available in events.get_outputs().
Prerequisites:
- Azure OpenAI configured for AzureChatClient with required environment variables.
@@ -54,12 +54,10 @@ async def main():
workflow = WorkflowBuilder().set_start_executor(writer_agent).add_edge(writer_agent, reviewer_agent).build()
# Stream events from the workflow. We aggregate partial token updates per executor for readable output.
completed_event: WorkflowCompletedEvent | None = None
last_executor_id = None
async for event in workflow.run_stream(
"Create a slogan for a new electric SUV that is affordable and fun to drive."
):
events = workflow.run_stream("Create a slogan for a new electric SUV that is affordable and fun to drive.")
async for event in events:
if isinstance(event, AgentRunUpdateEvent):
# AgentRunUpdateEvent contains incremental text deltas from the underlying agent.
# Print a prefix when the executor changes, then append updates on the same line.
@@ -70,14 +68,9 @@ async def main():
print(f"{eid}:", end=" ", flush=True)
last_executor_id = eid
print(event.data, end="", flush=True)
elif isinstance(event, WorkflowCompletedEvent):
# Terminal event with the final reviewer output.
completed_event = event
# Print the final consolidated reviewer result.
if completed_event:
print("\n===== Final Output =====")
print(completed_event.data)
elif isinstance(event, WorkflowOutputEvent):
print("===== Final Output =====")
print(event.data)
"""
Sample Output:
@@ -7,7 +7,6 @@ from agent_framework import (
ChatMessage,
Executor,
WorkflowBuilder,
WorkflowCompletedEvent,
WorkflowContext,
handler,
)
@@ -23,7 +22,7 @@ then hands the conversation to a Reviewer agent which evaluates and finalizes th
Purpose:
Show how to wrap chat agents created by AzureChatClient inside workflow executors. Demonstrate the @handler pattern
with typed inputs and typed WorkflowContext[T] outputs, connect executors with the fluent WorkflowBuilder, and finish
by emitting a WorkflowCompletedEvent from the terminal node.
by yielding outputs from the terminal node.
Prerequisites:
- Azure OpenAI configured for AzureChatClient with required environment variables.
@@ -53,7 +52,7 @@ class Writer(Executor):
super().__init__(agent=agent, id=id)
@handler
async def handle(self, message: ChatMessage, ctx: WorkflowContext[list[ChatMessage]]) -> None:
async def handle(self, message: ChatMessage, ctx: WorkflowContext[list[ChatMessage], str]) -> None:
"""Generate content using the agent and forward the updated conversation.
Contract for this handler:
@@ -79,7 +78,7 @@ class Reviewer(Executor):
This class demonstrates:
- Consuming a typed payload produced upstream.
- Emitting a terminal WorkflowCompletedEvent with the final text outcome.
- Yielding the final text outcome to complete the workflow.
"""
agent: ChatAgent
@@ -94,14 +93,14 @@ class Reviewer(Executor):
super().__init__(agent=agent, id=id)
@handler
async def handle(self, messages: list[ChatMessage], ctx: WorkflowContext[str]) -> None:
async def handle(self, messages: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage], str]) -> None:
"""Review the full conversation transcript and complete with a final string.
This node consumes all messages so far. It uses its agent to produce the final text,
then signals completion by adding a WorkflowCompletedEvent to the event stream.
then signals completion by yielding the output.
"""
response = await self.agent.run(messages)
await ctx.add_event(WorkflowCompletedEvent(response.text))
await ctx.yield_output(response.text)
async def main():
@@ -118,12 +117,14 @@ async def main():
workflow = WorkflowBuilder().set_start_executor(writer).add_edge(writer, reviewer).build()
# Run the workflow with the user's initial message.
# For foundational clarity, use run (non streaming) and print the terminal event.
# For foundational clarity, use run (non streaming) and print the workflow output.
events = await workflow.run(
ChatMessage(role="user", text="Create a slogan for a new electric SUV that is affordable and fun to drive.")
)
# The terminal node emits a WorkflowCompletedEvent; print its contents.
print(events.get_completed_event())
# The terminal node yields output; print its contents.
outputs = events.get_outputs()
if outputs:
print(outputs[-1])
if __name__ == "__main__":
@@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable
from contextlib import AsyncExitStack
from typing import Any
from agent_framework import AgentRunUpdateEvent, WorkflowBuilder, WorkflowCompletedEvent
from agent_framework import AgentRunUpdateEvent, WorkflowBuilder, WorkflowOutputEvent
from agent_framework.foundry import FoundryChatClient
from azure.identity.aio import AzureCliCredential
@@ -21,7 +21,7 @@ Show how to wire chat agents directly into a WorkflowBuilder pipeline where agen
Demonstrate:
- Automatic streaming of agent deltas via AgentRunUpdateEvent.
- A simple console aggregator that groups updates by executor id and prints them as they arrive.
- A final WorkflowCompletedEvent that contains the reviewer outcome after both agents finish.
- The workflow completes when idle and outputs are available in events.get_outputs().
Prerequisites:
- Foundry Agent Service configured, along with the required environment variables.
@@ -69,12 +69,10 @@ async def main() -> None:
workflow = WorkflowBuilder().set_start_executor(writer).add_edge(writer, reviewer).build()
completed: WorkflowCompletedEvent | None = None
last_executor_id: str | None = None
async for event in workflow.run_stream(
"Create a slogan for a new electric SUV that is affordable and fun to drive."
):
events = workflow.run_stream("Create a slogan for a new electric SUV that is affordable and fun to drive.")
async for event in events:
if isinstance(event, AgentRunUpdateEvent):
eid = event.executor_id
if eid != last_executor_id:
@@ -83,13 +81,9 @@ async def main() -> None:
print(f"{eid}:", end=" ", flush=True)
last_executor_id = eid
print(event.data, end="", flush=True)
elif isinstance(event, WorkflowCompletedEvent):
completed = event
if completed:
print("\n===== Final Output =====")
print(completed.data)
elif isinstance(event, WorkflowOutputEvent):
print("\n===== Final output =====")
print(event.data)
finally:
await close()
@@ -39,7 +39,7 @@ This sample demonstrates how to build a workflow agent that escalates uncertain
decisions to a human manager. A Worker generates results, while a Reviewer
evaluates them. When the Reviewer is not confident, it escalates the decision
to a human via RequestInfoExecutor, receives the human response, and then
forwards that response back to the Worker.
forwards that response back to the Worker. The workflow completes when idle.
Prerequisites:
- OpenAI account configured and accessible for OpenAIChatClient.
@@ -27,7 +27,7 @@ This sample demonstrates how to wrap a workflow as an agent using WorkflowAgent.
It uses a reflection pattern where a Worker executor generates responses and a
Reviewer executor evaluates them. If the response is not approved, the Worker
regenerates the output based on feedback until the Reviewer approves it. Only
approved responses are emitted to the external consumer.
approved responses are emitted to the external consumer. The workflow completes when idle.
Key Concepts Demonstrated:
- WorkflowAgent: Wraps a workflow to behave like a regular agent.