[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:
Evan Mattson
2026-02-06 16:47:20 +09:00
committed by GitHub
Unverified
parent 09f59b21ad
commit 0f3f4dbcaf
127 changed files with 1646 additions and 1703 deletions
@@ -2,7 +2,7 @@
import asyncio
from agent_framework import AgentResponseUpdate, WorkflowBuilder, WorkflowOutputEvent
from agent_framework import AgentResponseUpdate, WorkflowBuilder
from agent_framework.azure import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential
@@ -50,7 +50,7 @@ async def main() -> None:
async for event in events:
# The outputs of the workflow are whatever the agents produce. So the events are expected to
# contain `AgentResponseUpdate` from the agents in the workflow.
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
update = event.data
author = update.author_name
if author != last_author:
@@ -10,7 +10,6 @@ from agent_framework import (
ChatMessage,
WorkflowBuilder,
WorkflowContext,
WorkflowOutputEvent,
executor,
)
from agent_framework.azure import AzureOpenAIChatClient
@@ -128,7 +127,7 @@ async def main() -> None:
async for event in events:
# The outputs of the workflow are whatever the agents produce. So the events are expected to
# contain `AgentResponseUpdate` from the agents in the workflow.
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
update = event.data
author = update.author_name
if author != last_author:
@@ -2,7 +2,7 @@
import asyncio
from agent_framework import AgentResponseUpdate, WorkflowBuilder, WorkflowOutputEvent
from agent_framework import AgentResponseUpdate, WorkflowBuilder
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
@@ -49,7 +49,7 @@ async def main():
async for event in events:
# The outputs of the workflow are whatever the agents produce. So the events are expected to
# contain `AgentResponseUpdate` from the agents in the workflow.
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
update = event.data
author = update.author_name
if author != last_author:
@@ -9,16 +9,13 @@ from agent_framework import (
AgentExecutorRequest,
AgentExecutorResponse,
AgentResponse,
AgentRunUpdateEvent,
AgentResponseUpdate,
ChatAgent,
ChatMessage,
Executor,
FunctionCallContent,
FunctionResultContent,
RequestInfoEvent,
WorkflowBuilder,
WorkflowContext,
WorkflowOutputEvent,
WorkflowEvent,
handler,
response_handler,
tool,
@@ -36,7 +33,7 @@ writer_agent (uses Azure OpenAI tools) -> Coordinator -> writer_agent
-> Coordinator -> final_editor_agent -> Coordinator -> output
The writer agent calls tools to gather product facts before drafting copy. A custom executor
packages the draft and emits a RequestInfoEvent so a human can comment, then replays the human
packages the draft and emits a request_info event (type='request_info') so a human can comment, then replays the human
guidance back into the conversation before the final editor agent produces the polished output.
Demonstrates:
@@ -50,7 +47,9 @@ Prerequisites:
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py and
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def fetch_product_brief(
product_name: Annotated[str, Field(description="Product name to look up.")],
@@ -147,8 +146,7 @@ class Coordinator(Executor):
# Human approved the draft as-is; forward it unchanged.
await ctx.send_message(
AgentExecutorRequest(
messages=original_request.conversation
+ [ChatMessage("user", text="The draft is approved as-is.")],
messages=original_request.conversation + [ChatMessage("user", text="The draft is approved as-is.")],
should_respond=True,
),
target_id=self.final_editor_id,
@@ -194,15 +192,15 @@ def create_final_editor_agent() -> ChatAgent:
)
def display_agent_run_update(event: AgentRunUpdateEvent, last_executor: str | None) -> None:
def display_agent_run_update(event: WorkflowEvent, last_executor: str | None) -> None:
"""Display an AgentRunUpdateEvent in a readable format."""
printed_tool_calls: set[str] = set()
printed_tool_results: set[str] = set()
executor_id = event.executor_id
update = event.data
# Extract and print any new tool calls or results from the update.
function_calls = [c for c in update.contents if isinstance(c, FunctionCallContent)] # type: ignore[union-attr]
function_results = [c for c in update.contents if isinstance(c, FunctionResultContent)] # type: ignore[union-attr]
function_calls = [c for c in update.contents if c.type == "function_call"] # type: ignore[union-attr]
function_results = [c for c in update.contents if c.type == "function_result"] # type: ignore[union-attr]
if executor_id != last_executor:
if last_executor is not None:
print()
@@ -291,18 +289,22 @@ async def main() -> None:
requests: list[tuple[str, DraftFeedbackRequest]] = []
async for event in stream:
if isinstance(event, AgentRunUpdateEvent) and display_agent_run_update_switch:
if (
event.type == "output"
and isinstance(event.data, AgentResponseUpdate)
and display_agent_run_update_switch
):
display_agent_run_update(event, last_executor)
if isinstance(event, RequestInfoEvent) and isinstance(event.data, DraftFeedbackRequest):
if event.type == "request_info" and isinstance(event.data, DraftFeedbackRequest):
# Stash the request so we can prompt the human after the stream completes.
requests.append((event.request_id, event.data))
last_executor = None
elif isinstance(event, WorkflowOutputEvent):
elif event.type == "output" and not isinstance(event.data, AgentResponseUpdate):
# Only mark as completed for final outputs, not streaming updates
last_executor = None
response = event.data
print("\n===== Final output =====")
final_text = getattr(response, "text", str(response))
print(final_text.strip())
print(final_text, flush=True, end="")
completed = True
if requests and not completed:
@@ -2,8 +2,8 @@
import asyncio
from agent_framework import ConcurrentBuilder
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
"""
@@ -20,7 +20,7 @@ Demonstrates:
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
- Familiarity with Workflow events (WorkflowOutputEvent)
- Familiarity with Workflow events (WorkflowEvent with type "output")
"""
@@ -2,8 +2,9 @@
import asyncio
from agent_framework import ChatAgent, GroupChatBuilder
from agent_framework import ChatAgent
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
from agent_framework.orchestrations import GroupChatBuilder
"""
Sample: Group Chat Orchestration
@@ -42,7 +43,7 @@ async def main() -> None:
)
.participants([researcher, writer])
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowOutputEvent events
# Intermediate outputs will be emitted as WorkflowEvent with type "output" events
.with_intermediate_outputs()
.build()
)
@@ -8,12 +8,11 @@ from agent_framework import (
ChatAgent,
ChatMessage,
Content,
HandoffAgentUserRequest,
HandoffBuilder,
WorkflowAgent,
tool,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
"""Sample: Handoff Workflow as Agent with Human-in-the-Loop.
@@ -5,9 +5,9 @@ import asyncio
from agent_framework import (
ChatAgent,
HostedCodeInterpreterTool,
MagenticBuilder,
)
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
from agent_framework.orchestrations import MagenticBuilder
"""
Sample: Build a Magentic orchestration and wrap it as an agent.
@@ -62,7 +62,7 @@ async def main() -> None:
max_reset_count=2,
)
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowOutputEvent events
# Intermediate outputs will be emitted as WorkflowEvent with type "output" events
.with_intermediate_outputs()
.build()
)
@@ -2,8 +2,8 @@
import asyncio
from agent_framework import SequentialBuilder
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
"""
@@ -33,7 +33,9 @@ Prerequisites:
# Define tools that accept custom context via **kwargs
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity.
# Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_user_data(
query: Annotated[str, Field(description="What user data to retrieve")],
@@ -2,8 +2,9 @@
import asyncio
from agent_framework import AgentThread, ChatAgent, ChatMessageStore, SequentialBuilder
from agent_framework import AgentThread, ChatAgent, ChatMessageStore
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
"""
Sample: Workflow as Agent with Thread Conversation History and Checkpointing