[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
@@ -6,14 +6,12 @@ from typing import Annotated
from agent_framework import (
ChatMessage,
ConcurrentBuilder,
Content,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
tool,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import ConcurrentBuilder
"""
Sample: Concurrent Workflow with Tool Approval Requests
@@ -36,7 +34,7 @@ agents may independently trigger approval requests.
Demonstrate:
- Handling multiple approval requests from different agents in concurrent workflows.
- Handling RequestInfoEvent during concurrent agent execution.
- Handling during concurrent agent execution.
- Understanding that approval pauses only the agent that triggered it, not all agents.
Prerequisites:
@@ -89,12 +87,12 @@ def get_portfolio_balance() -> str:
return "Portfolio: $50,000 invested, $10,000 cash available. Holdings: AAPL, GOOGL, MSFT."
def _print_output(event: WorkflowOutputEvent) -> None:
def _print_output(event: WorkflowEvent) -> None:
if not event.data:
raise ValueError("WorkflowOutputEvent has no data")
raise ValueError("WorkflowEvent has no data")
if not isinstance(event.data, list) and not all(isinstance(msg, ChatMessage) for msg in event.data):
raise ValueError("WorkflowOutputEvent data is not a list of ChatMessage")
raise ValueError("WorkflowEvent data is not a list of ChatMessage")
messages: list[ChatMessage] = event.data # type: ignore
@@ -109,10 +107,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, Content] = {}
async for event in stream:
if isinstance(event, RequestInfoEvent) and isinstance(event.data, Content):
if event.type == "request_info" and isinstance(event.data, Content):
# We are only expecting tool approval requests in this sample
requests[event.request_id] = event.data
elif isinstance(event, WorkflowOutputEvent):
elif event.type == "output":
_print_output(event)
responses: dict[str, Content] = {}
@@ -7,14 +7,11 @@ from typing import Annotated, cast
from agent_framework import (
ChatMessage,
Content,
GroupChatBuilder,
GroupChatState,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
tool,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import GroupChatBuilder, GroupChatState
"""
Sample: Group Chat Workflow with Tool Approval Requests
@@ -36,7 +33,7 @@ different agents have different levels of tool access.
Demonstrate:
- Using set_select_speakers_func with agents that have approval-required tools.
- Handling RequestInfoEvent in group chat scenarios.
- Handling request_info events (type='request_info') in group chat scenarios.
- Multi-round group chat with tool approval interruption and resumption.
Prerequisites:
@@ -99,16 +96,16 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, Content] = {}
async for event in stream:
if isinstance(event, RequestInfoEvent) and isinstance(event.data, Content):
if event.type == "request_info" and isinstance(event.data, Content):
# We are only expecting tool approval requests in this sample
requests[event.request_id] = event.data
elif isinstance(event, WorkflowOutputEvent):
elif event.type == "output":
# The output of the workflow comes from the orchestrator and it's a list of messages
print("\n" + "=" * 60)
print("Workflow summary:")
outputs = cast(list[ChatMessage], event.data)
for msg in outputs:
speaker = msg.author_name or msg.role.value
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
responses: dict[str, Content] = {}
@@ -7,13 +7,11 @@ from typing import Annotated, cast
from agent_framework import (
ChatMessage,
Content,
RequestInfoEvent,
SequentialBuilder,
WorkflowEvent,
WorkflowOutputEvent,
tool,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
"""
Sample: Sequential Workflow with Tool Approval Requests
@@ -26,7 +24,7 @@ This sample works as follows:
1. A SequentialBuilder workflow is created with a single agent that has tools requiring approval.
2. The agent receives a user task and determines it needs to call a sensitive tool.
3. The tool call triggers a function_approval_request Content, pausing the workflow.
4. The sample simulates human approval by responding to the RequestInfoEvent.
4. The sample simulates human approval by responding to the .
5. Once approved, the tool executes and the agent completes its response.
6. The workflow outputs the final conversation with all messages.
@@ -36,7 +34,7 @@ requiring any additional builder configuration.
Demonstrate:
- Using @tool(approval_mode="always_require") for sensitive operations.
- Handling RequestInfoEvent with function_approval_request Content in sequential workflows.
- Handling with function_approval_request Content in sequential workflows.
- Resuming workflow execution after approval via send_responses_streaming.
Prerequisites:
@@ -55,7 +53,9 @@ def execute_database_query(
return f"Query executed successfully. Results: 3 rows affected by '{query}'"
# 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_database_schema() -> str:
"""Get the current database schema. Does not require approval."""
@@ -71,10 +71,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, Content] = {}
async for event in stream:
if isinstance(event, RequestInfoEvent) and isinstance(event.data, Content):
if event.type == "request_info" and isinstance(event.data, Content):
# We are only expecting tool approval requests in this sample
requests[event.request_id] = event.data
elif isinstance(event, WorkflowOutputEvent):
elif event.type == "output":
# The output of the workflow comes from the orchestrator and it's a list of messages
print("\n" + "=" * 60)
print("Workflow summary:")