[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
@@ -7,7 +7,7 @@ the task in a round-robin fashion.
import asyncio
from agent_framework import AgentResponseUpdate, WorkflowOutputEvent
from agent_framework import AgentResponseUpdate
async def run_autogen() -> None:
@@ -55,8 +55,8 @@ async def run_autogen() -> None:
async def run_agent_framework() -> None:
"""Agent Framework's SequentialBuilder for sequential agent orchestration."""
from agent_framework import SequentialBuilder
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
client = OpenAIChatClient(model_id="gpt-4.1-mini")
@@ -83,15 +83,14 @@ async def run_agent_framework() -> None:
print("[Agent Framework] Sequential conversation:")
current_executor = None
async for event in workflow.run("Create a brief summary about electric vehicles", stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
# Print executor name header when switching to a new agent
if current_executor != event.executor_id:
if current_executor is not None:
print() # Newline after previous agent's message
print(f"---------- {event.executor_id} ----------")
current_executor = event.executor_id
if isinstance(event.data, AgentResponseUpdate):
print(event.data.text, end="", flush=True)
print(event.data.text, end="", flush=True)
print() # Final newline after conversation
@@ -100,9 +99,9 @@ async def run_agent_framework_with_cycle() -> None:
from agent_framework import (
AgentExecutorRequest,
AgentExecutorResponse,
AgentResponseUpdate,
WorkflowBuilder,
WorkflowContext,
WorkflowOutputEvent,
executor,
)
from agent_framework.openai import OpenAIChatClient
@@ -154,7 +153,10 @@ async def run_agent_framework_with_cycle() -> None:
print("[Agent Framework with Cycle] Cyclic conversation:")
current_executor = None
async for event in workflow.run("Create a brief summary about electric vehicles", stream=True):
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
if event.type == "output" and not isinstance(event.data, AgentResponseUpdate):
print("\n---------- Workflow Output ----------")
print(event.data)
elif event.type == "output" and isinstance(event.data, AgentResponseUpdate):
# Print executor name header when switching to a new agent
if current_executor != event.executor_id:
if current_executor is not None:
@@ -7,7 +7,7 @@ which agent should speak next based on the conversation context.
import asyncio
from agent_framework import AgentResponseUpdate, WorkflowOutputEvent
from agent_framework import AgentResponseUpdate
async def run_autogen() -> None:
@@ -61,8 +61,8 @@ async def run_autogen() -> None:
async def run_agent_framework() -> None:
"""Agent Framework's GroupChatBuilder with LLM-based speaker selection."""
from agent_framework import GroupChatBuilder
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import GroupChatBuilder
client = OpenAIChatClient(model_id="gpt-4.1-mini")
@@ -102,7 +102,7 @@ async def run_agent_framework() -> None:
print("[Agent Framework] Group chat conversation:")
current_executor = None
async for event in workflow.run("How do I connect to a PostgreSQL database using Python?", stream=True):
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
# Print executor name header when switching to a new agent
if current_executor != event.executor_id:
if current_executor is not None:
@@ -7,7 +7,8 @@ to other specialized agents based on the task requirements.
import asyncio
from agent_framework import AgentResponseUpdate, HandoffAgentUserRequest, WorkflowOutputEvent
from agent_framework import WorkflowEvent
from orderedmultidict import Any
async def run_autogen() -> None:
@@ -98,12 +99,11 @@ async def run_autogen() -> None:
async def run_agent_framework() -> None:
"""Agent Framework's HandoffBuilder for agent coordination."""
from agent_framework import (
HandoffBuilder,
RequestInfoEvent,
AgentResponseUpdate,
WorkflowRunState,
WorkflowStatusEvent,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
client = OpenAIChatClient(model_id="gpt-4.1-mini")
@@ -159,10 +159,10 @@ async def run_agent_framework() -> None:
current_executor = None
stream_line_open = False
pending_requests: list[RequestInfoEvent] = []
pending_requests: list[WorkflowEvent] = []
async for event in workflow.run(scripted_responses[0], stream=True):
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
# Print executor name header when switching to a new agent
if current_executor != event.executor_id:
if stream_line_open:
@@ -173,10 +173,10 @@ async def run_agent_framework() -> None:
stream_line_open = True
if event.data:
print(event.data.text, end="", flush=True)
elif isinstance(event, RequestInfoEvent):
elif event.type == "request_info":
if isinstance(event.data, HandoffAgentUserRequest):
pending_requests.append(event)
elif isinstance(event, WorkflowStatusEvent):
elif event.type == "status":
if event.state in {WorkflowRunState.IDLE_WITH_PENDING_REQUESTS} and stream_line_open:
print()
stream_line_open = False
@@ -188,13 +188,13 @@ async def run_agent_framework() -> None:
print("---------- user ----------")
print(user_response)
responses = {req.request_id: user_response for req in pending_requests}
responses: dict[str, Any] = {req.request_id: user_response for req in pending_requests} # type: ignore
pending_requests = []
current_executor = None
stream_line_open = False
async for event in workflow.send_responses_streaming(responses):
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
# Print executor name header when switching to a new agent
if current_executor != event.executor_id:
if stream_line_open:
@@ -205,10 +205,10 @@ async def run_agent_framework() -> None:
stream_line_open = True
if event.data:
print(event.data.text, end="", flush=True)
elif isinstance(event, RequestInfoEvent):
elif event.type == "request_info":
if isinstance(event.data, HandoffAgentUserRequest):
pending_requests.append(event)
elif isinstance(event, WorkflowStatusEvent):
elif event.type == "status":
if (
event.state in {WorkflowRunState.IDLE_WITH_PENDING_REQUESTS, WorkflowRunState.IDLE}
and stream_line_open
@@ -12,10 +12,9 @@ from typing import cast
from agent_framework import (
AgentResponseUpdate,
ChatMessage,
MagenticOrchestratorEvent,
MagenticProgressLedger,
WorkflowOutputEvent,
WorkflowEvent,
)
from agent_framework.orchestrations import MagenticProgressLedger
async def run_autogen() -> None:
@@ -67,8 +66,8 @@ async def run_autogen() -> None:
async def run_agent_framework() -> None:
"""Agent Framework's MagenticBuilder for orchestrated collaboration."""
from agent_framework import MagenticBuilder
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import MagenticBuilder
client = OpenAIChatClient(model_id="gpt-4.1-mini")
@@ -110,10 +109,10 @@ async def run_agent_framework() -> None:
# Run complex task
last_message_id: str | None = None
output_event: WorkflowOutputEvent | None = None
output_event: WorkflowEvent | None = None
print("[Agent Framework] Magentic conversation:")
async for event in workflow.run("Research Python async patterns and write a simple example", stream=True):
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
message_id = event.data.message_id
if message_id != last_message_id:
if last_message_id is not None:
@@ -122,21 +121,21 @@ async def run_agent_framework() -> None:
last_message_id = message_id
print(event.data, end="", flush=True)
elif isinstance(event, MagenticOrchestratorEvent):
print(f"\n[Magentic Orchestrator Event] Type: {event.event_type.name}")
if isinstance(event.data, ChatMessage):
print(f"Please review the plan:\n{event.data.text}")
elif isinstance(event.data, MagenticProgressLedger):
print(f"Please review progress ledger:\n{json.dumps(event.data.to_dict(), indent=2)}")
elif event.type == "magentic_orchestrator":
print(f"\n[Magentic Orchestrator Event] Type: {event.data.event_type.name}")
if isinstance(event.data.content, ChatMessage):
print(f"Please review the plan:\n{event.data.content.text}")
elif isinstance(event.data.content, MagenticProgressLedger):
print(f"Please review progress ledger:\n{json.dumps(event.data.content.to_dict(), indent=2)}")
else:
print(f"Unknown data type in MagenticOrchestratorEvent: {type(event.data)}")
print(f"Unknown data type in MagenticOrchestratorEvent: {type(event.data.content)}")
# Block to allow user to read the plan/progress before continuing
# Note: this is for demonstration only and is not the recommended way to handle human interaction.
# Please refer to `with_plan_review` for proper human interaction during planning phases.
await asyncio.get_event_loop().run_in_executor(None, input, "Press Enter to continue...")
elif isinstance(event, WorkflowOutputEvent):
elif event.type == "output":
output_event = event
if not output_event:
@@ -48,12 +48,10 @@ from _tools import (
from agent_framework import (
AgentExecutorResponse,
AgentResponseUpdate,
AgentRunUpdateEvent,
ChatMessage,
Executor,
WorkflowBuilder,
WorkflowContext,
WorkflowOutputEvent,
executor,
handler,
)
@@ -355,7 +353,7 @@ async def _process_workflow_events(events, conversation_ids, response_ids):
workflow_output = None
async for event in events:
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
workflow_output = event.data
# Handle Unicode characters that may not be displayable in Windows console
try:
@@ -364,7 +362,7 @@ async def _process_workflow_events(events, conversation_ids, response_ids):
output_str = str(event.data).encode("ascii", "replace").decode("ascii")
print(f"\nWorkflow Output: {output_str}\n")
elif isinstance(event, AgentRunUpdateEvent):
elif event.type == "output" and isinstance(event.data, AgentResponseUpdate):
_track_agent_ids(event, event.executor_id, response_ids, conversation_ids)
return workflow_output
@@ -6,7 +6,7 @@ from agent_framework import (
Executor,
WorkflowBuilder,
WorkflowContext,
WorkflowOutputEvent,
handler,
)
from agent_framework.observability import configure_otel_providers, get_tracer
@@ -93,7 +93,7 @@ async def run_sequential_workflow() -> None:
output_event = None
async for event in workflow.run("Hello world", stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
# The WorkflowOutputEvent contains the final result.
output_event = event
@@ -57,9 +57,9 @@ from agent_framework.orchestrations import (
**Sequential orchestration note**: Sequential orchestration uses a few small adapter nodes for plumbing:
- `input-conversation` normalizes input to `list[ChatMessage]`
- `to-conversation:<participant>` converts agent responses into the shared conversation
- `complete` publishes the final `WorkflowOutputEvent`
- `complete` publishes the final output event (type='output')
These may appear in event streams (ExecutorInvoke/Completed). They're analogous to concurrent's dispatcher and aggregator and can be ignored if you only care about agent activity.
These may appear in event streams (executor_invoked/executor_completed). They're analogous to concurrent's dispatcher and aggregator and can be ignored if you only care about agent activity.
## Environment Variables
@@ -23,7 +23,7 @@ Demonstrates:
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
- Familiarity with Workflow events (WorkflowOutputEvent)
- Familiarity with Workflow events (WorkflowEvent)
"""
@@ -7,7 +7,6 @@ from agent_framework import (
AgentResponseUpdate,
ChatAgent,
ChatMessage,
WorkflowOutputEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import GroupChatBuilder
@@ -74,7 +73,7 @@ async def main() -> None:
# The agent orchestrator will intelligently decide when to end before this limit but just in case
.with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 4)
# 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()
)
@@ -88,7 +87,7 @@ async def main() -> None:
# Keep track of the last response to format output nicely in streaming mode
last_response_id: str | None = None
async for event in workflow.run(task, stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
data = event.data
if isinstance(data, AgentResponseUpdate):
rid = data.response_id
@@ -98,7 +97,7 @@ async def main() -> None:
print(f"{data.author_name}:", end=" ", flush=True)
last_response_id = rid
print(data.text, end="", flush=True)
else:
elif event.type == "output":
# The output of the group chat workflow is a collection of chat messages from all participants
outputs = cast(list[ChatMessage], event.data)
print("\n" + "=" * 80)
@@ -8,7 +8,6 @@ from agent_framework import (
AgentResponseUpdate,
ChatAgent,
ChatMessage,
WorkflowOutputEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import GroupChatBuilder
@@ -214,7 +213,7 @@ Share your perspective authentically. Feel free to:
.participants([farmer, developer, teacher, activist, spiritual_leader, artist, immigrant, doctor])
.with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 10)
# 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()
)
@@ -241,7 +240,7 @@ Share your perspective authentically. Feel free to:
# Keep track of the last response to format output nicely in streaming mode
last_response_id: str | None = None
async for event in workflow.run(f"Please begin the discussion on: {topic}", stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
data = event.data
if isinstance(data, AgentResponseUpdate):
rid = data.response_id
@@ -251,7 +250,7 @@ Share your perspective authentically. Feel free to:
print(f"{data.author_name}:", end=" ", flush=True)
last_response_id = rid
print(data.text, end="", flush=True)
else:
elif event.type == "output":
# The output of the group chat workflow is a collection of chat messages from all participants
outputs = cast(list[ChatMessage], event.data)
print("\n" + "=" * 80)
@@ -7,7 +7,6 @@ from agent_framework import (
AgentResponseUpdate,
ChatAgent,
ChatMessage,
WorkflowOutputEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import GroupChatBuilder, GroupChatState
@@ -92,7 +91,7 @@ async def main() -> None:
# have nothing to add, but for demo purposes we want to see at least one full round of interaction.
.with_termination_condition(lambda conversation: len(conversation) >= 6)
# 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()
)
@@ -106,7 +105,7 @@ async def main() -> None:
# Keep track of the last response to format output nicely in streaming mode
last_response_id: str | None = None
async for event in workflow.run(task, stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
data = event.data
if isinstance(data, AgentResponseUpdate):
rid = data.response_id
@@ -116,7 +115,7 @@ async def main() -> None:
print(f"{data.author_name}:", end=" ", flush=True)
last_response_id = rid
print(data.text, end="", flush=True)
else:
elif event.type == "output":
# The output of the group chat workflow is a collection of chat messages from all participants
outputs = cast(list[ChatMessage], event.data)
print("\n" + "=" * 80)
@@ -8,8 +8,6 @@ from agent_framework import (
AgentResponseUpdate,
ChatAgent,
ChatMessage,
HandoffSentEvent,
WorkflowOutputEvent,
resolve_agent_id,
)
from agent_framework.azure import AzureOpenAIChatClient
@@ -112,9 +110,9 @@ async def main() -> None:
last_response_id: str | None = None
async for event in workflow.run(request, stream=True):
if isinstance(event, HandoffSentEvent):
print(f"\nHandoff Event: from {event.source} to {event.target}\n")
elif isinstance(event, WorkflowOutputEvent):
if event.type == "handoff_sent":
print(f"\nHandoff Event: from {event.data.source} to {event.data.target}\n")
elif event.type == "output":
data = event.data
if isinstance(data, AgentResponseUpdate):
if not data.text:
@@ -128,8 +126,8 @@ async def main() -> None:
print(f"{data.author_name}:", end=" ", flush=True)
last_response_id = rid
print(data.text, end="", flush=True)
else:
# The output of the group chat workflow is a collection of chat messages from all participants
elif event.type == "output":
# The output of the handoff workflow is a collection of chat messages from all participants
outputs = cast(list[ChatMessage], event.data)
print("\n" + "=" * 80)
print("\nFinal Conversation Transcript:\n")
@@ -8,16 +8,13 @@ from agent_framework import (
AgentResponse,
ChatAgent,
ChatMessage,
RequestInfoEvent,
Workflow,
WorkflowEvent,
WorkflowOutputEvent,
WorkflowRunState,
WorkflowStatusEvent,
tool,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder, HandoffSentEvent
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
logging.basicConfig(level=logging.ERROR)
@@ -107,35 +104,35 @@ def create_return_agent() -> ChatAgent:
)
def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
def _handle_events(events: list[WorkflowEvent]) -> list[WorkflowEvent[HandoffAgentUserRequest]]:
"""Process workflow events and extract any pending user input requests.
This function inspects each event type and:
- Prints workflow status changes (IDLE, IDLE_WITH_PENDING_REQUESTS, etc.)
- Displays final conversation snapshots when workflow completes
- Prints user input request prompts
- Collects all RequestInfoEvent instances for response handling
- Collects all request_info events for response handling
Args:
events: List of WorkflowEvent to process
Returns:
List of RequestInfoEvent representing pending user input requests
List of WorkflowEvent[HandoffAgentUserRequest] representing pending user input requests
"""
requests: list[RequestInfoEvent] = []
requests: list[WorkflowEvent[HandoffAgentUserRequest]] = []
for event in events:
if isinstance(event, HandoffSentEvent):
# HandoffSentEvent: Indicates a handoff has been initiated
print(f"\n[Handoff from {event.source} to {event.target} initiated.]")
elif isinstance(event, WorkflowStatusEvent) and event.state in {
if event.type == "handoff_sent":
# handoff_sent event: Indicates a handoff has been initiated
print(f"\n[Handoff from {event.data.source} to {event.data.target} initiated.]")
elif event.type == "status" and event.state in {
WorkflowRunState.IDLE,
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
}:
# WorkflowStatusEvent: Indicates workflow state changes
# Status event: Indicates workflow state changes
print(f"\n[Workflow Status] {event.state.name}")
elif isinstance(event, WorkflowOutputEvent):
# WorkflowOutputEvent: Contains contents generated by the workflow
elif event.type == "output":
# Output event: Contains contents generated by the workflow
data = event.data
if isinstance(data, AgentResponse):
for message in data.messages:
@@ -144,7 +141,7 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
continue
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text}")
else:
elif event.type == "output":
# The output of the handoff workflow is a collection of chat messages from all participants
conversation = cast(list[ChatMessage], event.data)
if isinstance(conversation, list):
@@ -153,11 +150,11 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text or [content.type for content in message.contents]}")
print("===================================")
elif isinstance(event, RequestInfoEvent):
# RequestInfoEvent: Workflow is requesting user input
elif event.type == "request_info":
# Request info event: Workflow is requesting user input
if isinstance(event.data, HandoffAgentUserRequest):
_print_handoff_agent_user_request(event.data.agent_response)
requests.append(event)
requests.append(cast(WorkflowEvent[HandoffAgentUserRequest], event))
return requests
@@ -7,15 +7,12 @@ from agent_framework import (
AgentResponse,
ChatAgent,
ChatMessage,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
WorkflowRunState,
WorkflowStatusEvent,
tool,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder, HandoffSentEvent
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
"""Sample: Simple handoff workflow.
@@ -102,35 +99,35 @@ def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAg
return triage_agent, refund_agent, order_agent, return_agent
def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
def _handle_events(events: list[WorkflowEvent]) -> list[WorkflowEvent[HandoffAgentUserRequest]]:
"""Process workflow events and extract any pending user input requests.
This function inspects each event type and:
- Prints workflow status changes (IDLE, IDLE_WITH_PENDING_REQUESTS, etc.)
- Displays final conversation snapshots when workflow completes
- Prints user input request prompts
- Collects all RequestInfoEvent instances for response handling
- Collects all request_info events for response handling
Args:
events: List of WorkflowEvent to process
Returns:
List of RequestInfoEvent representing pending user input requests
List of WorkflowEvent[HandoffAgentUserRequest] representing pending user input requests
"""
requests: list[RequestInfoEvent] = []
requests: list[WorkflowEvent[HandoffAgentUserRequest]] = []
for event in events:
if isinstance(event, HandoffSentEvent):
# HandoffSentEvent: Indicates a handoff has been initiated
print(f"\n[Handoff from {event.source} to {event.target} initiated.]")
elif isinstance(event, WorkflowStatusEvent) and event.state in {
if event.type == "handoff_sent":
# handoff_sent event: Indicates a handoff has been initiated
print(f"\n[Handoff from {event.data.source} to {event.data.target} initiated.]")
elif event.type == "status" and event.state in {
WorkflowRunState.IDLE,
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
}:
# WorkflowStatusEvent: Indicates workflow state changes
print(f"\n[Workflow Status] {event.state.name}")
elif isinstance(event, WorkflowOutputEvent):
# WorkflowOutputEvent: Contains contents generated by the workflow
# Status event: Indicates workflow state changes
print(f"\n[Workflow Status] {event.state}")
elif event.type == "output":
# Output event: Contains contents generated by the workflow
data = event.data
if isinstance(data, AgentResponse):
for message in data.messages:
@@ -139,7 +136,7 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
continue
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text}")
else:
elif event.type == "output":
# The output of the handoff workflow is a collection of chat messages from all participants
conversation = cast(list[ChatMessage], event.data)
if isinstance(conversation, list):
@@ -148,11 +145,9 @@ def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text or [content.type for content in message.contents]}")
print("===================================")
elif isinstance(event, RequestInfoEvent):
# RequestInfoEvent: Workflow is requesting user input
if isinstance(event.data, HandoffAgentUserRequest):
_print_handoff_agent_user_request(event.data.agent_response)
requests.append(event)
elif event.type == "request_info" and isinstance(event.data, HandoffAgentUserRequest):
_print_handoff_agent_user_request(event.data.agent_response)
requests.append(cast(WorkflowEvent[HandoffAgentUserRequest], event))
return requests
@@ -6,7 +6,7 @@ Handoff Workflow with Code Interpreter File Generation Sample
This sample demonstrates retrieving file IDs from code interpreter output
in a handoff workflow context. A triage agent routes to a code specialist
that generates a text file, and we verify the file_id is captured correctly
from the streaming WorkflowOutputEvent events.
from the streaming workflow events.
Verifies GitHub issue #2718: files generated by code interpreter in
HandoffBuilder workflows can be properly retrieved.
@@ -34,13 +34,9 @@ from agent_framework import (
AgentResponseUpdate,
ChatAgent,
ChatMessage,
HandoffSentEvent,
HostedCodeInterpreterTool,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
WorkflowRunState,
WorkflowStatusEvent,
)
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity.aio import AzureCliCredential
@@ -54,30 +50,29 @@ async def _drain(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:
return [event async for event in stream]
def _handle_events(events: list[WorkflowEvent]) -> tuple[list[RequestInfoEvent], list[str]]:
def _handle_events(events: list[WorkflowEvent]) -> tuple[list[WorkflowEvent[HandoffAgentUserRequest]], list[str]]:
"""Process workflow events and extract file IDs and pending requests.
Returns:
Tuple of (pending_requests, file_ids_found)
"""
requests: list[RequestInfoEvent] = []
requests: list[WorkflowEvent[HandoffAgentUserRequest]] = []
file_ids: list[str] = []
for event in events:
if isinstance(event, HandoffSentEvent):
# HandoffSentEvent: Indicates a handoff has been initiated
print(f"\n[Handoff from {event.source} to {event.target} initiated.]")
elif isinstance(event, WorkflowStatusEvent) and event.state in {
if event.type == "handoff_sent":
print(f"\n[Handoff from {event.data.source} to {event.data.target} initiated.]")
elif event.type == "status" and event.state in {
WorkflowRunState.IDLE,
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
}:
# WorkflowStatusEvent: Indicates workflow state changes
print(f"\n[Workflow Status] {event.state.name}")
elif isinstance(event, WorkflowOutputEvent):
# WorkflowOutputEvent: Contains contents generated by the workflow
print(f"[status] {event.state.name}")
elif event.type == "request_info" and isinstance(event.data, HandoffAgentUserRequest):
requests.append(cast(WorkflowEvent[HandoffAgentUserRequest], event))
elif event.type == "output":
data = event.data
if isinstance(data, AgentResponseUpdate):
# AgentResponseUpdate: Intermediate output from an agent
for content in data.contents:
if content.type == "hosted_file":
file_ids.append(content.file_id) # type: ignore
@@ -87,8 +82,7 @@ def _handle_events(events: list[WorkflowEvent]) -> tuple[list[RequestInfoEvent],
file_id = annotation["file_id"] # type: ignore
file_ids.append(file_id)
print(f"[Found file annotation: file_id={file_id}]")
else:
# The output of the handoff workflow is a collection of chat messages from all participants
elif event.type == "output":
conversation = cast(list[ChatMessage], event.data)
if isinstance(conversation, list):
print("\n=== Final Conversation Snapshot ===")
@@ -96,9 +90,6 @@ def _handle_events(events: list[WorkflowEvent]) -> tuple[list[RequestInfoEvent],
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text or [content.type for content in message.contents]}")
print("===================================")
elif isinstance(event, RequestInfoEvent):
# RequestInfoEvent: Workflow is requesting user input
requests.append(event)
return requests, file_ids
@@ -9,12 +9,11 @@ from agent_framework import (
AgentResponseUpdate,
ChatAgent,
ChatMessage,
GroupChatRequestSentEvent,
HostedCodeInterpreterTool,
WorkflowOutputEvent,
WorkflowEvent,
)
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
from agent_framework.orchestrations import MagenticBuilder, MagenticOrchestratorEvent, MagenticProgressLedger
from agent_framework.orchestrations import GroupChatRequestSentEvent, MagenticBuilder, MagenticProgressLedger
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)
@@ -85,7 +84,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 events
.with_intermediate_outputs()
.build()
)
@@ -104,41 +103,44 @@ async def main() -> None:
# Keep track of the last executor to format output nicely in streaming mode
last_response_id: str | None = None
output_event: WorkflowEvent | None = None
async for event in workflow.run(task, stream=True):
if isinstance(event, MagenticOrchestratorEvent):
print(f"\n[Magentic Orchestrator Event] Type: {event.event_type.name}")
if isinstance(event.data, ChatMessage):
print(f"Please review the plan:\n{event.data.text}")
elif isinstance(event.data, MagenticProgressLedger):
print(f"Please review progress ledger:\n{json.dumps(event.data.to_dict(), indent=2)}")
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
response_id = event.data.response_id
if response_id != last_response_id:
if last_response_id is not None:
print("\n")
print(f"- {event.executor_id}:", end=" ", flush=True)
last_response_id = response_id
print(event.data, end="", flush=True)
elif event.type == "magentic_orchestrator":
print(f"\n[Magentic Orchestrator Event] Type: {event.data.event_type.name}")
if isinstance(event.data.content, ChatMessage):
print(f"Please review the plan:\n{event.data.content.text}")
elif isinstance(event.data.content, MagenticProgressLedger):
print(f"Please review progress ledger:\n{json.dumps(event.data.content.to_dict(), indent=2)}")
else:
print(f"Unknown data type in MagenticOrchestratorEvent: {type(event.data)}")
print(f"Unknown data type in MagenticOrchestratorEvent: {type(event.data.content)}")
# Block to allow user to read the plan/progress before continuing
# Note: this is for demonstration only and is not the recommended way to handle human interaction.
# Please refer to `with_plan_review` for proper human interaction during planning phases.
await asyncio.get_event_loop().run_in_executor(None, input, "Press Enter to continue...")
elif isinstance(event, GroupChatRequestSentEvent):
print(f"\n[REQUEST SENT ({event.round_index})] to agent: {event.participant_name}")
elif event.type == "group_chat" and isinstance(event.data, GroupChatRequestSentEvent):
print(f"\n[REQUEST SENT ({event.data.round_index})] to agent: {event.data.participant_name}")
elif isinstance(event, WorkflowOutputEvent):
data = event.data
if isinstance(data, AgentResponseUpdate):
response_id = data.response_id
if response_id != last_response_id:
if last_response_id is not None:
print("\n")
print(f"- {event.executor_id}:", end=" ", flush=True)
last_response_id = response_id
print(event.data, end="", flush=True)
else:
# The output of the magentic workflow is a collection of chat messages from all participants
outputs = cast(list[ChatMessage], event.data)
print("\n" + "=" * 80)
print("\nFinal Conversation Transcript:\n")
for message in outputs:
print(f"{message.author_name or message.role}: {message.text}\n")
elif event.type == "output":
output_event = event
if output_event:
# The output of the magentic workflow is a collection of chat messages from all participants
outputs = cast(list[ChatMessage], output_event.data)
print("\n" + "=" * 80)
print("\nFinal Conversation Transcript:\n")
for message in outputs:
print(f"{message.author_name or message.role}: {message.text}\n")
if __name__ == "__main__":
@@ -9,11 +9,9 @@ from agent_framework import (
ChatAgent,
ChatMessage,
FileCheckpointStorage,
RequestInfoEvent,
WorkflowCheckpoint,
WorkflowOutputEvent,
WorkflowEvent,
WorkflowRunState,
WorkflowStatusEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import MagenticBuilder, MagenticPlanReviewRequest
@@ -105,16 +103,16 @@ async def main() -> None:
print("\n=== Stage 1: run until plan review request (checkpointing active) ===")
workflow = build_workflow(checkpoint_storage)
# Run the workflow until the first RequestInfoEvent is surfaced. The event carries the
# Run the workflow until the first is surfaced. The event carries the
# request_id we must reuse on resume. In a real system this is where the UI would present
# the plan for human review.
plan_review_request: MagenticPlanReviewRequest | None = None
async for event in workflow.run(TASK, stream=True):
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
if event.type == "request_info" and event.request_type is MagenticPlanReviewRequest:
plan_review_request = event.data
print(f"Captured plan review request: {event.request_id}")
if isinstance(event, WorkflowStatusEvent) and event.state is WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
if event.type == "status" and event.state is WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
break
if plan_review_request is None:
@@ -147,9 +145,9 @@ async def main() -> None:
approval = plan_review_request.approve()
# Resume execution and capture the re-emitted plan review request.
request_info_event: RequestInfoEvent | None = None
request_info_event: WorkflowEvent | None = None
async for event in resumed_workflow.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
if isinstance(event, RequestInfoEvent) and isinstance(event.data, MagenticPlanReviewRequest):
if event.type == "request_info" and isinstance(event.data, MagenticPlanReviewRequest):
request_info_event = event
if request_info_event is None:
@@ -158,9 +156,9 @@ async def main() -> None:
print(f"Resumed plan review request: {request_info_event.request_id}")
# Supply the approval and continue to run to completion.
final_event: WorkflowOutputEvent | None = None
final_event: WorkflowEvent | None = None
async for event in resumed_workflow.send_responses_streaming({request_info_event.request_id: approval}):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
final_event = event
if final_event is None:
@@ -218,12 +216,12 @@ async def main() -> None:
if pending_messages == 0:
print("Checkpoint has no pending messages; no additional work expected on resume.")
final_event_post: WorkflowOutputEvent | None = None
final_event_post: WorkflowEvent | None = None
post_emitted_events = False
post_plan_workflow = build_workflow(checkpoint_storage)
async for event in post_plan_workflow.run(checkpoint_id=post_plan_checkpoint.checkpoint_id, stream=True):
post_emitted_events = True
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
final_event_post = event
if final_event_post is None:
@@ -9,9 +9,7 @@ from agent_framework import (
AgentResponseUpdate,
ChatAgent,
ChatMessage,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import MagenticBuilder, MagenticPlanReviewRequest, MagenticPlanReviewResponse
@@ -46,10 +44,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
requests: dict[str, MagenticPlanReviewRequest] = {}
async for event in stream:
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
if event.type == "request_info" and event.request_type is MagenticPlanReviewRequest:
requests[event.request_id] = cast(MagenticPlanReviewRequest, event.data)
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
data = event.data
if isinstance(data, AgentResponseUpdate):
rid = data.response_id
@@ -68,7 +66,7 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
# To make the type checker happy, we cast event.data to the expected type
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, MagenticPlanReviewResponse] = {}
@@ -129,7 +127,7 @@ async def main() -> None:
# Request human input for plan review
.with_plan_review()
# 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"
.with_intermediate_outputs()
.build()
)
@@ -3,7 +3,7 @@
import asyncio
from typing import cast
from agent_framework import ChatMessage, WorkflowOutputEvent
from agent_framework import ChatMessage
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
@@ -48,7 +48,7 @@ async def main() -> None:
# 3) Run and collect outputs
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("Write a tagline for a budget-friendly eBike.", stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
outputs.append(cast(list[ChatMessage], event.data))
if outputs:
@@ -102,7 +102,7 @@ Tool approval samples demonstrate using `@tool(approval_mode="always_require")`
| Sample | File | Concepts |
| ------------------------ | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Executor I/O Observation | [observability/executor_io_observation.py](./observability/executor_io_observation.py) | Observe executor input/output data via ExecutorInvokedEvent and ExecutorCompletedEvent without modifying executor code |
| Executor I/O Observation | [observability/executor_io_observation.py](./observability/executor_io_observation.py) | Observe executor input/output data via executor_invoked events (type='executor_invoked') and executor_completed events (type='executor_completed') without modifying executor code |
For additional observability samples in Agent Framework, see the [observability getting started samples](../observability/README.md). The [sample](../observability/workflow_observability.py) demonstrates integrating observability into workflows.
@@ -162,8 +162,8 @@ Sequential orchestration uses a few small adapter nodes for plumbing:
- "input-conversation" normalizes input to `list[ChatMessage]`
- "to-conversation:<participant>" converts agent responses into the shared conversation
- "complete" publishes the final `WorkflowOutputEvent`
These may appear in event streams (ExecutorInvoke/Completed). Theyre analogous to
- "complete" publishes the final output event (type='output')
These may appear in event streams (executor_invoked/executor_completed). They're analogous to
concurrents dispatcher and aggregator and can be ignored if you only care about agent activity.
### Environment Variables
@@ -140,7 +140,7 @@ class ExclamationAdder(Executor):
super().__init__(id=id)
@handler(input=str, output=str)
async def add_exclamation(self, message: str, ctx: WorkflowContext) -> None:
async def add_exclamation(self, message, ctx) -> None: # type: ignore
"""Add exclamation marks to the input.
Note: The input=str and output=str are explicitly specified on @handler,
@@ -149,7 +149,7 @@ class ExclamationAdder(Executor):
on @handler take precedence.
"""
result = f"{message}!!!"
await ctx.send_message(result)
await ctx.send_message(result) # type: ignore
async def main():
@@ -57,7 +57,6 @@ async def main():
# of `AgentResponse` from the agents in the workflow.
outputs = cast(list[AgentResponse], outputs)
for output in outputs:
# TODO: author_name should be available in AgentResponse
print(f"{output.messages[0].author_name}: {output.text}\n")
# Summarize the final run state (e.g., COMPLETED)
@@ -66,7 +65,7 @@ async def main():
"""
writer: "Charge Ahead: Affordable Adventure Awaits!"
reviewer: - Consider emphasizing both affordability and fun in a more dynamic way.
reviewer: - Consider emphasizing both affordability and fun in a more dynamic way.
- Try using a catchy phrase that includes a play on words, like “Electrify Your Drive: Fun Meets Affordability!”
- Ensure the slogan is succinct while capturing the essence of the car's unique selling proposition.
@@ -3,7 +3,6 @@
import asyncio
from agent_framework import AgentResponseUpdate, ChatMessage, WorkflowBuilder
from agent_framework._workflows._events import WorkflowOutputEvent
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
@@ -58,7 +57,7 @@ async def main():
):
# 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:
@@ -8,7 +8,6 @@ from agent_framework import (
Executor,
WorkflowBuilder,
WorkflowContext,
WorkflowOutputEvent,
executor,
handler,
)
@@ -87,7 +86,7 @@ async def main():
async for event in workflow.run("hello world", stream=True):
# 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
if first_update:
print(f"{update.author_name}: {update.text}", end="", flush=True)
@@ -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
@@ -1,9 +1,16 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, override
from typing import Any
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
# NOTE: the Azure client imports above are real dependencies. When running this
# sample outside of Azure-enabled environments you may wish to swap in the
@@ -15,13 +22,10 @@ from agent_framework import (
ChatMessage,
Executor,
FileCheckpointStorage,
RequestInfoEvent,
Workflow,
WorkflowBuilder,
WorkflowCheckpoint,
WorkflowContext,
WorkflowOutputEvent,
WorkflowStatusEvent,
get_checkpoint_summary,
handler,
response_handler,
@@ -53,7 +57,7 @@ Typical pause/resume flow
3. Later, restart the script, select that checkpoint, and provide the stored
human decision when prompted to pre-supply responses.
Doing so applies the answer immediately on resume, so the system does **not**
re-emit the same `RequestInfoEvent`.
re-emit the same ``.
"""
# Directory used for the sample's temporary checkpoint files. We isolate the
@@ -259,11 +263,11 @@ async def run_interactive_session(
raise ValueError("Either initial_message or checkpoint_id must be provided")
async for event in event_stream:
if isinstance(event, WorkflowStatusEvent):
if event.type == "status":
print(event)
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
completed_output = event.data
if isinstance(event, RequestInfoEvent):
if event.type == "request_info":
if isinstance(event.data, HumanApprovalRequest):
requests[event.request_id] = event.data
else:
@@ -24,21 +24,25 @@ Prerequisites:
"""
import asyncio
import sys
from dataclasses import dataclass
from random import random
from typing import Any, override
from typing import Any
from agent_framework import (
Executor,
InMemoryCheckpointStorage,
SuperStepCompletedEvent,
WorkflowBuilder,
WorkflowCheckpoint,
WorkflowContext,
WorkflowOutputEvent,
handler,
)
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
@dataclass
class ComputeTask:
@@ -126,12 +130,12 @@ async def main():
output: str | None = None
async for event in event_stream:
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
output = event.data
break
if isinstance(event, SuperStepCompletedEvent) and random() < 0.5:
if event.type == "superstep_completed" and random() < 0.5:
# Randomly simulate system interruptions
# The `SuperStepCompletedEvent` ensures we only interrupt after
# The type="superstep_completed" event ensures we only interrupt after
# the current super-step is fully complete and checkpointed.
# If we interrupt mid-step, the workflow may resume from an earlier point.
print("\n** Simulating workflow interruption. Stopping execution. **")
@@ -12,15 +12,12 @@ from agent_framework import (
ChatMessage,
Content,
FileCheckpointStorage,
HandoffAgentUserRequest,
HandoffBuilder,
RequestInfoEvent,
Workflow,
WorkflowOutputEvent,
WorkflowStatusEvent,
WorkflowEvent,
tool,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
"""
@@ -153,7 +150,7 @@ def _print_function_approval_request(request: Content, request_id: str) -> None:
def _build_responses_for_requests(
pending_requests: list[RequestInfoEvent],
pending_requests: list[WorkflowEvent],
*,
user_response: str | None,
approve_tools: bool | None,
@@ -161,11 +158,15 @@ def _build_responses_for_requests(
"""Create response payloads for each pending request."""
responses: dict[str, object] = {}
for request in pending_requests:
if isinstance(request.data, HandoffAgentUserRequest):
if isinstance(request.data, HandoffAgentUserRequest) and request.request_id:
if user_response is None:
raise ValueError("User response is required for HandoffAgentUserRequest")
responses[request.request_id] = user_response
elif isinstance(request.data, Content) and request.data.type == "function_approval_request":
elif (
isinstance(request.data, Content)
and request.data.type == "function_approval_request"
and request.request_id
):
if approve_tools is None:
raise ValueError("Approval decision is required for function approval request")
responses[request.request_id] = request.data.to_function_approval_response(approved=approve_tools)
@@ -178,14 +179,14 @@ async def run_until_user_input_needed(
workflow: Workflow,
initial_message: str | None = None,
checkpoint_id: str | None = None,
) -> tuple[list[RequestInfoEvent], str | None]:
) -> tuple[list[WorkflowEvent], str | None]:
"""
Run the workflow until it needs user input or approval, or completes.
Returns:
Tuple of (pending_requests, checkpoint_id_to_use_for_resume)
"""
pending_requests: list[RequestInfoEvent] = []
pending_requests: list[WorkflowEvent] = []
latest_checkpoint_id: str | None = checkpoint_id
if initial_message:
@@ -198,17 +199,17 @@ async def run_until_user_input_needed(
raise ValueError("Must provide either initial_message or checkpoint_id")
async for event in event_stream:
if isinstance(event, WorkflowStatusEvent):
if event.type == "status":
print(f"[Status] {event.state}")
elif isinstance(event, RequestInfoEvent):
elif event.type == "request_info":
pending_requests.append(event)
if isinstance(event.data, HandoffAgentUserRequest):
_print_handoff_request(event.data, event.request_id)
elif isinstance(event.data, Content) and event.data.type == "function_approval_request":
_print_function_approval_request(event.data, event.request_id)
elif isinstance(event, WorkflowOutputEvent):
elif event.type == "output":
print("\n[Workflow Completed]")
if event.data:
print(f"Final conversation length: {len(event.data)} messages")
@@ -225,7 +226,7 @@ async def resume_with_responses(
checkpoint_storage: FileCheckpointStorage,
user_response: str | None = None,
approve_tools: bool | None = None,
) -> tuple[list[RequestInfoEvent], str | None]:
) -> tuple[list[WorkflowEvent], str | None]:
"""
Two-step resume pattern (answers customer questions and tool approvals):
@@ -255,10 +256,10 @@ async def resume_with_responses(
print(f"Step 1: Restoring checkpoint {latest_checkpoint.checkpoint_id}")
# Step 1: Restore the checkpoint to load pending requests into memory
# The checkpoint restoration re-emits pending RequestInfoEvents
restored_requests: list[RequestInfoEvent] = []
# The checkpoint restoration re-emits pending request_info events
restored_requests: list[WorkflowEvent] = []
async for event in workflow.run(checkpoint_id=latest_checkpoint.checkpoint_id, stream=True): # type: ignore[attr-defined]
if isinstance(event, RequestInfoEvent):
if event.type == "request_info":
restored_requests.append(event)
if isinstance(event.data, HandoffAgentUserRequest):
_print_handoff_request(event.data, event.request_id)
@@ -275,13 +276,13 @@ async def resume_with_responses(
)
print(f"Step 2: Sending responses for {len(responses)} request(s)")
new_pending_requests: list[RequestInfoEvent] = []
new_pending_requests: list[WorkflowEvent] = []
async for event in workflow.send_responses_streaming(responses):
if isinstance(event, WorkflowStatusEvent):
if event.type == "status":
print(f"[Status] {event.state}")
elif isinstance(event, WorkflowOutputEvent):
elif event.type == "output":
print("\n[Workflow Output Event - Conversation Update]")
if event.data and isinstance(event.data, list) and all(isinstance(msg, ChatMessage) for msg in event.data): # type: ignore
# Now safe to cast event.data to list[ChatMessage]
@@ -291,7 +292,7 @@ async def resume_with_responses(
text = msg.text[:100] + "..." if len(msg.text) > 100 else msg.text
print(f" {author}: {text}")
elif isinstance(event, RequestInfoEvent):
elif event.type == "request_info":
new_pending_requests.append(event)
if isinstance(event.data, HandoffAgentUserRequest):
_print_handoff_request(event.data, event.request_id)
@@ -3,29 +3,33 @@
import asyncio
import contextlib
import json
import sys
import uuid
from dataclasses import dataclass, field, replace
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, override
from typing import Any
from agent_framework import (
Executor,
FileCheckpointStorage,
RequestInfoEvent,
SubWorkflowRequestMessage,
SubWorkflowResponseMessage,
Workflow,
WorkflowBuilder,
WorkflowContext,
WorkflowEvent,
WorkflowExecutor,
WorkflowOutputEvent,
WorkflowRunState,
WorkflowStatusEvent,
handler,
response_handler,
)
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
CHECKPOINT_DIR = Path(__file__).with_suffix("").parent / "tmp" / "sub_workflow_checkpoints"
"""
@@ -335,10 +339,10 @@ async def main() -> None:
request_id: str | None = None
async for event in workflow.run("Contoso Gadget Launch", stream=True):
if isinstance(event, RequestInfoEvent) and request_id is None:
if event.type == "request_info" and request_id is None:
request_id = event.request_id
print(f"Captured review request id: {request_id}")
if isinstance(event, WorkflowStatusEvent) and event.state is WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
if event.type == "status" and event.state is WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
break
if request_id is None:
@@ -364,9 +368,9 @@ async def main() -> None:
# Rebuild fresh instances to mimic a separate process resuming
workflow2 = build_parent_workflow(storage)
request_info_event: RequestInfoEvent | None = None
request_info_event: WorkflowEvent | None = None
async for event in workflow2.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
if isinstance(event, RequestInfoEvent):
if event.type == "request_info":
request_info_event = event
if request_info_event is None:
@@ -375,9 +379,9 @@ async def main() -> None:
print("\n=== Stage 3: approve draft ==")
approval_response = "approve"
output_event: WorkflowOutputEvent | None = None
output_event: WorkflowEvent | None = None
async for event in workflow2.send_responses_streaming({request_info_event.request_id: approval_response}):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
output_event = event
if output_event is None:
@@ -30,9 +30,9 @@ from agent_framework import (
ChatAgent,
ChatMessageStore,
InMemoryCheckpointStorage,
SequentialBuilder,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
async def basic_checkpointing() -> None:
@@ -157,7 +157,12 @@ async def streaming_with_checkpoints() -> None:
print(f"\nCheckpoints created during stream: {len(checkpoints)}")
async def main() -> None:
"""Run all checkpoint examples."""
await basic_checkpointing()
await checkpointing_with_thread()
await streaming_with_checkpoints()
if __name__ == "__main__":
asyncio.run(basic_checkpointing())
asyncio.run(checkpointing_with_thread())
asyncio.run(streaming_with_checkpoints())
asyncio.run(main())
@@ -6,12 +6,11 @@ from typing import Annotated, Any
from agent_framework import (
ChatMessage,
SequentialBuilder,
WorkflowExecutor,
WorkflowOutputEvent,
tool,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
"""
Sample: Sub-Workflow kwargs Propagation
@@ -32,7 +31,9 @@ Prerequisites:
# Define tools that access 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_authenticated_data(
resource: Annotated[str, "The resource to fetch"],
@@ -129,7 +130,7 @@ async def main() -> None:
user_token=user_token,
service_config=service_config,
):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
output_data = event.data
if isinstance(output_data, list):
for item in output_data: # type: ignore
@@ -140,6 +141,50 @@ async def main() -> None:
print("Sample Complete - kwargs successfully flowed through sub-workflow!")
print("=" * 70)
"""
Sample Output:
======================================================================
Sub-Workflow kwargs Propagation Demo
======================================================================
Context being passed to parent workflow:
user_token: {
"user_name": "alice@contoso.com",
"access_level": "admin",
"session_id": "sess_12345"
}
service_config: {
"services": {
"users": "https://api.example.com/v1/users",
"orders": "https://api.example.com/v1/orders",
"inventory": "https://api.example.com/v1/inventory"
},
"timeout": 30
}
----------------------------------------------------------------------
Workflow Execution (kwargs flow: parent -> sub-workflow -> agent -> tool):
----------------------------------------------------------------------
[get_authenticated_data] kwargs keys: ['user_token', 'service_config']
[get_authenticated_data] User: alice@contoso.com, Access: admin
[call_configured_service] kwargs keys: ['user_token', 'service_config']
[call_configured_service] Available services: ['users', 'orders', 'inventory']
[Final Answer]: Please fetch my profile data and then call the users service.
[Final Answer]: - Your profile data has been fetched.
- The users service has been called.
Would you like details from either the profile data or the users service response?
======================================================================
Sample Complete - kwargs successfully flowed through sub-workflow!
======================================================================
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -3,16 +3,16 @@
import asyncio
import uuid
from dataclasses import dataclass
from typing import Literal
from typing import Any, Literal
from agent_framework import (
Executor,
RequestInfoEvent,
SubWorkflowRequestMessage,
SubWorkflowResponseMessage,
Workflow,
WorkflowBuilder,
WorkflowContext,
WorkflowEvent,
WorkflowExecutor,
handler,
response_handler,
@@ -192,7 +192,7 @@ class ResourceAllocator(Executor):
super().__init__(id)
self._cache: dict[str, int] = {"cpu": 10, "memory": 50, "disk": 100}
# Record pending requests to match responses
self._pending_requests: dict[str, RequestInfoEvent] = {}
self._pending_requests: dict[str, WorkflowEvent[Any]] = {}
async def _handle_resource_request(self, request: ResourceRequest) -> ResourceResponse | None:
"""Allocates resources based on request and available cache."""
@@ -207,7 +207,7 @@ class ResourceAllocator(Executor):
self, request: SubWorkflowRequestMessage, ctx: WorkflowContext[SubWorkflowResponseMessage]
) -> None:
"""Handles requests from sub-workflows."""
source_event: RequestInfoEvent = request.source_event
source_event: WorkflowEvent[Any] = request.source_event
if not isinstance(source_event.data, ResourceRequest):
return
@@ -246,14 +246,14 @@ class PolicyEngine(Executor):
"disk": 1000, # Liberal disk policy
}
# Record pending requests to match responses
self._pending_requests: dict[str, RequestInfoEvent] = {}
self._pending_requests: dict[str, WorkflowEvent[Any]] = {}
@handler
async def handle_subworkflow_request(
self, request: SubWorkflowRequestMessage, ctx: WorkflowContext[SubWorkflowResponseMessage]
) -> None:
"""Handles requests from sub-workflows."""
source_event: RequestInfoEvent = request.source_event
source_event: WorkflowEvent[Any] = request.source_event
if not isinstance(source_event.data, PolicyRequest):
return
@@ -11,7 +11,6 @@ from agent_framework import (
WorkflowBuilder,
WorkflowContext,
WorkflowExecutor,
WorkflowOutputEvent,
handler,
response_handler,
)
@@ -303,7 +302,7 @@ async def main() -> None:
for email in test_emails:
print(f"\n🚀 Processing email to '{email.recipient}'")
async for event in workflow.run(email, stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
print(f"🎉 Final result for '{email.recipient}': {'Delivered' if event.data else 'Blocked'}")
@@ -16,7 +16,6 @@ from agent_framework import (
WorkflowBuilder,
WorkflowContext,
WorkflowEvent,
WorkflowOutputEvent,
executor,
)
from agent_framework.azure import AzureOpenAIChatClient
@@ -279,7 +278,7 @@ async def main() -> None:
async for event in workflow.run(email, stream=True):
if isinstance(event, DatabaseEvent):
print(f"{event}")
elif isinstance(event, WorkflowOutputEvent):
elif event.type == "output":
print(f"Workflow output: {event.data}")
"""
@@ -7,7 +7,6 @@ from agent_framework import (
Executor,
WorkflowBuilder,
WorkflowContext,
WorkflowOutputEvent,
handler,
)
from typing_extensions import Never
@@ -77,7 +76,7 @@ async def main() -> None:
outputs: list[str] = []
async for event in workflow.run("hello world", stream=True):
print(f"Event: {event}")
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
outputs.append(cast(str, event.data))
if outputs:
@@ -2,7 +2,7 @@
import asyncio
from agent_framework import WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, executor
from agent_framework import WorkflowBuilder, WorkflowContext, executor
from typing_extensions import Never
"""
@@ -14,7 +14,8 @@ The second reverses the text and yields the workflow output. Events are printed
Purpose:
Show how to declare executors with the @executor decorator, connect them with WorkflowBuilder,
pass intermediate values using ctx.send_message, and yield final output using ctx.yield_output().
Demonstrate how streaming exposes ExecutorInvokedEvent and ExecutorCompletedEvent for observability.
Demonstrate how streaming exposes executor_invoked events (type='executor_invoked') and
executor_completed events (type='executor_completed') for observability.
Prerequisites:
- No external services required.
@@ -67,17 +68,17 @@ async def main():
async for event in workflow.run("hello world", stream=True):
# You will see executor invoke and completion events as the workflow progresses.
print(f"Event: {event}")
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
print(f"Workflow completed with result: {event.data}")
"""
Sample Output:
Event: ExecutorInvokedEvent(executor_id=upper_case_executor)
Event: ExecutorCompletedEvent(executor_id=upper_case_executor)
Event: ExecutorInvokedEvent(executor_id=reverse_text_executor)
Event: ExecutorCompletedEvent(executor_id=reverse_text_executor)
Event: WorkflowOutputEvent(data='DLROW OLLEH', executor_id=reverse_text_executor)
Event: executor_invoked event (type='executor_invoked', executor_id=upper_case_executor)
Event: executor_completed event (type='executor_completed', executor_id=upper_case_executor)
Event: executor_invoked event (type='executor_invoked', executor_id=reverse_text_executor)
Event: executor_completed event (type='executor_completed', executor_id=reverse_text_executor)
Event: output event (type='output', data='DLROW OLLEH', executor_id=reverse_text_executor)
Workflow completed with result: DLROW OLLEH
"""
@@ -9,7 +9,6 @@ from agent_framework import (
ChatAgent,
ChatMessage,
Executor,
ExecutorCompletedEvent,
WorkflowBuilder,
WorkflowContext,
handler,
@@ -143,7 +142,7 @@ async def main():
# Step 2: Run the workflow and print the events.
iterations = 0
async for event in workflow.run(NumberSignal.INIT, stream=True):
if isinstance(event, ExecutorCompletedEvent) and event.executor_id == "guess_number":
if event.type == "executor_completed" and event.executor_id == "guess_number":
iterations += 1
print(f"Event: {event}")
@@ -26,7 +26,6 @@ import logging
import uuid
from pathlib import Path
from agent_framework import RequestInfoEvent, WorkflowOutputEvent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.declarative import (
AgentExternalInputRequest,
@@ -259,7 +258,7 @@ async def main() -> None:
stream = workflow.run(user_input, stream=True)
async for event in stream:
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
data = event.data
source_id = getattr(event, "source_executor_id", "")
@@ -286,7 +285,7 @@ async def main() -> None:
else:
accumulated_response += str(data)
elif isinstance(event, RequestInfoEvent) and isinstance(event.data, AgentExternalInputRequest):
elif event.type == "request_info" and isinstance(event.data, AgentExternalInputRequest):
request = event.data
# The agent_response from the request contains the structured response
@@ -24,7 +24,6 @@ Usage:
import asyncio
from pathlib import Path
from agent_framework import WorkflowOutputEvent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.declarative import WorkflowFactory
from azure.identity import AzureCliCredential
@@ -193,7 +192,7 @@ async def main() -> None:
task = "What is the weather like in Seattle and how does it compare to the average for this time of year?"
async for event in workflow.run(task, stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
print(f"{event.data}", end="", flush=True)
print("\n" + "=" * 60)
@@ -10,7 +10,7 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Annotated, Any
from agent_framework import FileCheckpointStorage, RequestInfoEvent, WorkflowOutputEvent, tool
from agent_framework import FileCheckpointStorage, tool
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_declarative import ExternalInputRequest, ExternalInputResponse, WorkflowFactory
from azure.identity import AzureCliCredential
@@ -98,12 +98,12 @@ async def main():
first_response = True
async for event in stream:
if isinstance(event, WorkflowOutputEvent) and isinstance(event.data, str):
if event.type == "output" and isinstance(event.data, str):
if first_response:
print("MenuAgent: ", end="")
first_response = False
print(event.data, end="", flush=True)
elif isinstance(event, RequestInfoEvent) and isinstance(event.data, ExternalInputRequest):
elif event.type == "request_info" and isinstance(event.data, ExternalInputRequest):
pending_request_id = event.request_id
print()
@@ -15,7 +15,7 @@ In a production scenario, you would integrate with a real UI or chat interface.
import asyncio
from pathlib import Path
from agent_framework import Workflow, WorkflowOutputEvent
from agent_framework import Workflow
from agent_framework.declarative import ExternalInputRequest, WorkflowFactory
from agent_framework_declarative._workflows._handlers import TextOutputEvent
@@ -27,7 +27,7 @@ async def run_with_streaming(workflow: Workflow) -> None:
async for event in workflow.run({}, stream=True):
# WorkflowOutputEvent wraps the actual output data
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
data = event.data
if isinstance(data, TextOutputEvent):
print(f"[Bot]: {data.text}")
@@ -15,7 +15,6 @@ Demonstrates sequential multi-agent pipeline:
import asyncio
from pathlib import Path
from agent_framework import WorkflowOutputEvent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.declarative import WorkflowFactory
from azure.identity import AzureCliCredential
@@ -85,7 +84,7 @@ async def main() -> None:
product = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours."
async for event in workflow.run(product, stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
print(f"{event.data}", end="", flush=True)
print("\n" + "=" * 60)
@@ -22,7 +22,6 @@ Prerequisites:
import asyncio
from pathlib import Path
from agent_framework import WorkflowOutputEvent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.declarative import WorkflowFactory
from azure.identity import AzureCliCredential
@@ -82,7 +81,7 @@ async def main() -> None:
print("=" * 50)
async for event in workflow.run("How would you compute the value of PI?", stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
print(f"{event.data}", flush=True, end="")
print("\n" + "=" * 50)
@@ -11,12 +11,9 @@ from agent_framework import (
AgentResponseUpdate,
ChatMessage,
Executor,
RequestInfoEvent,
Role,
WorkflowBuilder,
WorkflowContext,
WorkflowEvent,
WorkflowOutputEvent,
handler,
response_handler,
)
@@ -30,13 +27,13 @@ Sample: AzureOpenAI Chat Agents in workflow with human feedback
Pipeline layout:
writer_agent -> Coordinator -> writer_agent -> Coordinator -> final_editor_agent -> Coordinator -> output
The writer agent drafts marketing copy. A custom executor emits a RequestInfoEvent so a human can comment,
then relays the human guidance back into the conversation before the final editor agent produces the polished
output.
The writer agent drafts marketing copy. A custom executor emits a request_info event (type='request_info') so a
human can comment, then relays the human guidance back into the conversation before the final editor agent
produces the polished output.
Demonstrates:
- Capturing agent responses in a custom executor.
- Emitting RequestInfoEvent to request human input.
- Emitting request_info events (type='request_info') to request human input.
- Handling human feedback and routing it to the appropriate agents.
Prerequisites:
@@ -103,8 +100,7 @@ class Coordinator(Executor):
# Human approved the draft as-is; forward it unchanged.
await ctx.send_message(
AgentExecutorRequest(
messages=original_request.conversation
+ [ChatMessage(Role.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_name,
@@ -119,7 +115,7 @@ class Coordinator(Executor):
"Rewrite the draft from the previous assistant message into a polished final version. "
"Keep the response under 120 words and reflect any requested tone adjustments."
)
conversation.append(ChatMessage(Role.USER, text=instruction))
conversation.append(ChatMessage("user", text=instruction))
await ctx.send_message(
AgentExecutorRequest(messages=conversation, should_respond=True), target_id=self.writer_name
)
@@ -132,9 +128,9 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
requests: list[tuple[str, DraftFeedbackRequest]] = []
async for event in stream:
if isinstance(event, RequestInfoEvent) and isinstance(event.data, DraftFeedbackRequest):
if event.type == "request_info" and isinstance(event.data, DraftFeedbackRequest):
requests.append((event.request_id, event.data))
elif isinstance(event, WorkflowOutputEvent) and isinstance(event.data, AgentResponseUpdate):
elif event.type == "output" and isinstance(event.data, AgentResponseUpdate):
# This workflow should only produce AgentResponseUpdate as outputs.
# Streaming updates from an agent will be consecutive, because no two agents run simultaneously
# in this workflow. So we can use last_author to format output nicely.
@@ -47,7 +47,7 @@ Demonstrate:
Prerequisites:
- Azure AI Agent Service configured, along with the required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, edges, events, RequestInfoEvent, and streaming runs.
- Basic familiarity with WorkflowBuilder, edges, events, request_info events (type='request_info'), and streaming runs.
"""
@@ -26,12 +26,10 @@ from collections.abc import AsyncIterable
from typing import Any
from agent_framework import (
AgentExecutorResponse,
ChatMessage,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
)
from agent_framework._workflows._agent_executor import AgentExecutorResponse
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import AgentRequestInfoResponse, ConcurrentBuilder
from azure.identity import AzureCliCredential
@@ -97,11 +95,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
requests: dict[str, AgentExecutorResponse] = {}
async for event in stream:
if isinstance(event, RequestInfoEvent) and isinstance(event.data, AgentExecutorResponse):
# Display agent output for review and potential modification
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
requests[event.request_id] = event.data
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
# The output of the workflow comes from the aggregator and it's a single string
print("\n" + "=" * 60)
print("ANALYSIS COMPLETE")
@@ -29,9 +29,7 @@ from typing import cast
from agent_framework import (
AgentExecutorResponse,
ChatMessage,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import AgentRequestInfoResponse, GroupChatBuilder
@@ -43,10 +41,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
requests: dict[str, AgentExecutorResponse] = {}
async for event in stream:
if isinstance(event, RequestInfoEvent) and isinstance(event.data, AgentExecutorResponse):
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
requests[event.request_id] = event.data
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
# The output of the workflow comes from the orchestrator and it's a list of messages
print("\n" + "=" * 60)
print("DISCUSSION COMPLETE")
@@ -10,11 +10,9 @@ from agent_framework import (
AgentResponseUpdate,
ChatMessage,
Executor,
RequestInfoEvent,
WorkflowBuilder,
WorkflowContext,
WorkflowEvent,
WorkflowOutputEvent,
handler,
response_handler,
)
@@ -46,7 +44,7 @@ Prerequisites:
# How human-in-the-loop is achieved via `request_info` and `send_responses_streaming`:
# - An executor (TurnManager) calls `ctx.request_info` with a payload (HumanFeedbackRequest).
# - The workflow run pauses and emits a RequestInfoEvent with the payload and the request_id.
# - The workflow run pauses and emits a with the payload and the request_id.
# - The application captures the event, prompts the user, and collects replies.
# - The application calls `send_responses_streaming` with a map of request_ids to replies.
# - The workflow resumes, and the response is delivered to the executor method decorated with @response_handler.
@@ -132,11 +130,13 @@ class TurnManager(Executor):
return
# Provide feedback to the agent to try again.
# We keep the agent's output strictly JSON to ensure stable parsing on the next turn.
user_msg = ChatMessage(
"user",
text=(f'Feedback: {reply}. Return ONLY a JSON object matching the schema {{"guess": <int 1..10>}}.'),
# response_format=GuessOutput on the agent ensures JSON output, so we just need to guide the logic.
last_guess = original_request.prompt.split(": ")[1].split(".")[0]
feedback_text = (
f"Feedback: {reply}. Your last guess was {last_guess}. "
f"Use this feedback to adjust and make your next guess (1-10)."
)
user_msg = ChatMessage("user", text=feedback_text)
await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True))
@@ -147,9 +147,9 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
requests: list[tuple[str, HumanFeedbackRequest]] = []
async for event in stream:
if isinstance(event, RequestInfoEvent) and isinstance(event.data, HumanFeedbackRequest):
if event.type == "request_info" and isinstance(event.data, HumanFeedbackRequest):
requests.append((event.request_id, event.data))
elif isinstance(event, WorkflowOutputEvent):
elif event.type == "output":
if isinstance(event.data, AgentResponseUpdate):
update = event.data
response_id = update.response_id
@@ -13,7 +13,7 @@ using the standard request_info pattern for consistency.
Demonstrate:
- Configuring request info with `.with_request_info()`
- Handling RequestInfoEvent with AgentInputRequest data
- Handling with AgentInputRequest data
- Injecting responses back into the workflow via send_responses_streaming
Prerequisites:
@@ -28,9 +28,7 @@ from typing import cast
from agent_framework import (
AgentExecutorResponse,
ChatMessage,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import AgentRequestInfoResponse, SequentialBuilder
@@ -42,10 +40,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
requests: dict[str, AgentExecutorResponse] = {}
async for event in stream:
if isinstance(event, RequestInfoEvent) and isinstance(event.data, AgentExecutorResponse):
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
requests[event.request_id] = event.data
elif isinstance(event, WorkflowOutputEvent):
elif event.type == "output":
# The output of the sequential workflow is a list of ChatMessages
print("\n" + "=" * 60)
print("WORKFLOW COMPLETE")
@@ -5,11 +5,8 @@ from typing import Any, cast
from agent_framework import (
Executor,
ExecutorCompletedEvent,
ExecutorInvokedEvent,
WorkflowBuilder,
WorkflowContext,
WorkflowOutputEvent,
handler,
)
from typing_extensions import Never
@@ -21,8 +18,8 @@ This sample demonstrates how to observe executor input and output data without m
executor code. This is useful for debugging, logging, or building monitoring tools.
What this example shows:
- ExecutorInvokedEvent.data contains the input message received by the executor
- ExecutorCompletedEvent.data contains the messages sent via ctx.send_message()
- executor_invoked events (type='executor_invoked') contain the input message in event.data
- executor_completed events (type='executor_completed') contain the messages sent via ctx.send_message() in event.data
- How to generically observe all executor I/O through workflow streaming events
This approach allows you to enable_instrumentation any workflow for observability without
@@ -92,18 +89,18 @@ async def main() -> None:
print("Running workflow with executor I/O observation...\n")
async for event in workflow.run("hello world", stream=True):
if isinstance(event, ExecutorInvokedEvent):
if event.type == "executor_invoked":
# The input message received by the executor is in event.data
print(f"[INVOKED] {event.executor_id}")
print(f" Input: {format_io_data(event.data)}")
elif isinstance(event, ExecutorCompletedEvent):
elif event.type == "executor_completed":
# Messages sent via ctx.send_message() are in event.data
print(f"[COMPLETED] {event.executor_id}")
if event.data:
print(f" Output: {format_io_data(event.data)}")
elif isinstance(event, WorkflowOutputEvent):
elif event.type == "output":
print(f"[WORKFLOW OUTPUT] {format_io_data(event.data)}")
"""
@@ -1,145 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
from typing import cast
from agent_framework import (
AgentRunUpdateEvent,
ChatAgent,
ChatMessage,
MagenticBuilder,
MagenticPlanReviewRequest,
RequestInfoEvent,
WorkflowOutputEvent,
)
from agent_framework.openai import OpenAIChatClient
"""
Sample: Magentic Orchestration with Human Plan Review
This sample demonstrates how humans can review and provide feedback on plans
generated by the Magentic workflow orchestrator. When plan review is enabled,
the workflow requests human approval or revision before executing each plan.
Key concepts:
- with_plan_review(): Enables human review of generated plans
- MagenticPlanReviewRequest: The event type for plan review requests
- Human can choose to: approve the plan or provide revision feedback
Plan review options:
- approve(): Accept the proposed plan and continue execution
- revise(feedback): Provide textual feedback to modify the plan
Prerequisites:
- OpenAI credentials configured for `OpenAIChatClient`.
"""
async def main() -> None:
researcher_agent = ChatAgent(
name="ResearcherAgent",
description="Specialist in research and information gathering",
instructions="You are a Researcher. You find information and gather facts.",
chat_client=OpenAIChatClient(model_id="gpt-4o"),
)
analyst_agent = ChatAgent(
name="AnalystAgent",
description="Data analyst who processes and summarizes research findings",
instructions="You are an Analyst. You analyze findings and create summaries.",
chat_client=OpenAIChatClient(model_id="gpt-4o"),
)
manager_agent = ChatAgent(
name="MagenticManager",
description="Orchestrator that coordinates the workflow",
instructions="You coordinate a team to complete tasks efficiently.",
chat_client=OpenAIChatClient(model_id="gpt-4o"),
)
print("\nBuilding Magentic Workflow with Human Plan Review...")
workflow = (
MagenticBuilder()
.participants([researcher_agent, analyst_agent])
.with_manager(
agent=manager_agent,
max_round_count=10,
max_stall_count=1,
max_reset_count=2,
)
.with_plan_review() # Request human input for plan review
.build()
)
task = "Research sustainable aviation fuel technology and summarize the findings."
print(f"\nTask: {task}")
print("\nStarting workflow execution...")
print("=" * 60)
pending_request: RequestInfoEvent | None = None
pending_responses: dict[str, object] | None = None
output_event: WorkflowOutputEvent | None = None
while not output_event:
if pending_responses is not None:
stream = workflow.send_responses_streaming(pending_responses)
else:
stream = workflow.run(task, stream=True)
last_message_id: str | None = None
async for event in stream:
if isinstance(event, AgentRunUpdateEvent):
message_id = event.data.message_id
if message_id != last_message_id:
if last_message_id is not None:
print("\n")
print(f"- {event.executor_id}:", end=" ", flush=True)
last_message_id = message_id
print(event.data, end="", flush=True)
elif isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
pending_request = event
elif isinstance(event, WorkflowOutputEvent):
output_event = event
pending_responses = None
# Handle plan review request if any
if pending_request is not None:
event_data = cast(MagenticPlanReviewRequest, pending_request.data)
print("\n\n[Magentic Plan Review Request]")
if event_data.current_progress is not None:
print("Current Progress Ledger:")
print(json.dumps(event_data.current_progress.to_dict(), indent=2))
print()
print(f"Proposed Plan:\n{event_data.plan.text}\n")
print("Please provide your feedback (press Enter to approve):")
reply = await asyncio.get_event_loop().run_in_executor(None, input, "> ")
if reply.strip() == "":
print("Plan approved.\n")
pending_responses = {pending_request.request_id: event_data.approve()}
else:
print("Plan revised by human.\n")
pending_responses = {pending_request.request_id: event_data.revise(reply)}
pending_request = None
print("\n" + "=" * 60)
print("WORKFLOW COMPLETED")
print("=" * 60)
print("Final Output:")
# The output of the Magentic workflow is a list of ChatMessages with only one final message
# generated by the orchestrator.
output_messages = cast(list[ChatMessage], output_event.data)
if output_messages:
output = output_messages[-1].text
print(output)
if __name__ == "__main__":
asyncio.run(main())
@@ -3,7 +3,7 @@
import asyncio
import random
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, handler
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
from typing_extensions import Never
"""
@@ -87,7 +87,7 @@ async def main() -> None:
# 2) Run the workflow
output: list[int | float] | None = None
async for event in workflow.run([random.randint(1, 100) for _ in range(10)], stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
output = event.data
if output is not None:
@@ -3,18 +3,14 @@
import asyncio
from dataclasses import dataclass
from agent_framework import ( # Core chat primitives to build LLM requests
from agent_framework import (
AgentExecutorRequest, # The message bundle sent to an AgentExecutor
AgentExecutorResponse, # The structured result returned by an AgentExecutor
ChatAgent, # Tracing event for agent execution steps
ChatMessage, # Chat message structure
Executor, # Base class for custom Python executors
ExecutorCompletedEvent,
ExecutorInvokedEvent,
Role, # Enum of chat roles (user, assistant, system)
WorkflowBuilder, # Fluent builder for wiring the workflow graph
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 AzureOpenAIChatClient
@@ -45,7 +41,7 @@ class DispatchToExperts(Executor):
@handler
async def dispatch(self, prompt: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None:
# Wrap the incoming prompt as a user message for each expert and request a response.
initial_message = ChatMessage(Role.USER, text=prompt)
initial_message = ChatMessage("user", text=prompt)
await ctx.send_message(AgentExecutorRequest(messages=[initial_message], should_respond=True))
@@ -143,12 +139,12 @@ async def main() -> None:
async for event in workflow.run(
"We are launching a new budget-friendly electric bike for urban commuters.", stream=True
):
if isinstance(event, ExecutorInvokedEvent):
if event.type == "executor_invoked":
# Show when executors are invoked and completed for lightweight observability.
print(f"{event.executor_id} invoked")
elif isinstance(event, ExecutorCompletedEvent):
elif event.type == "executor_completed":
print(f"{event.executor_id} completed")
elif isinstance(event, WorkflowOutputEvent):
elif event.type == "output":
print("===== Final Aggregated Output =====")
print(event.data)
@@ -10,8 +10,7 @@ import aiofiles
from agent_framework import (
Executor, # Base class for custom workflow steps
WorkflowBuilder, # Fluent builder for executors and edges
WorkflowContext, # Per run context with workflow state and messaging
WorkflowOutputEvent, # Event emitted when workflow yields output
WorkflowContext, # Per run context with shared state and messaging
WorkflowViz, # Utility to visualize a workflow graph
handler, # Decorator to expose an Executor method as a step
)
@@ -332,7 +331,7 @@ async def main():
# Step 4: Run the workflow with the raw text as input.
async for event in workflow.run(raw_text, stream=True):
print(f"Event: {event}")
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
print(f"Final Output: {event.data}")
@@ -2,9 +2,9 @@
import asyncio
import json
from typing import Annotated, Any
from typing import Annotated, Any, cast
from agent_framework import ChatMessage, WorkflowOutputEvent, tool
from agent_framework import ChatMessage, tool
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
from pydantic import Field
@@ -27,7 +27,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")],
@@ -118,8 +120,8 @@ async def main() -> None:
additional_function_arguments={"custom_data": custom_data, "user_token": user_token},
stream=True,
):
if isinstance(event, WorkflowOutputEvent):
output_data = event.data
if event.type == "output":
output_data = cast(list[ChatMessage], event.data)
if isinstance(output_data, list):
for item in output_data:
if isinstance(item, ChatMessage) and item.text:
@@ -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:")
@@ -6,7 +6,7 @@ import asyncio
from collections.abc import Sequence
from typing import cast
from agent_framework import ChatMessage, ConcurrentBuilder, WorkflowOutputEvent
from agent_framework import ChatMessage, ConcurrentBuilderWorkflowEvent
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
from semantic_kernel.agents import Agent, ChatCompletionAgent, ConcurrentOrchestration
@@ -91,7 +91,7 @@ async def run_agent_framework_example(prompt: str) -> Sequence[list[ChatMessage]
outputs: list[list[ChatMessage]] = []
async for event in workflow.run(prompt, stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
outputs.append(cast(list[ChatMessage], event.data))
return outputs
@@ -7,7 +7,7 @@ import sys
from collections.abc import Sequence
from typing import Any, cast
from agent_framework import ChatAgent, ChatMessage, GroupChatBuilder, WorkflowOutputEvent
from agent_framework import ChatAgent, ChatMessage, GroupChatBuilderWorkflowEvent
from agent_framework.azure import AzureOpenAIChatClient, AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from semantic_kernel.agents import Agent, ChatCompletionAgent, GroupChatOrchestration
@@ -240,7 +240,7 @@ async def run_agent_framework_example(task: str) -> str:
final_response = ""
async for event in workflow.run(task, stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
data = event.data
if isinstance(data, list) and len(data) > 0:
# Get the final message from the conversation
@@ -8,12 +8,9 @@ from typing import cast
from agent_framework import (
ChatMessage,
HandoffBuilder,
HandoffUserInputRequest,
RequestInfoEvent,
WorkflowEvent,
WorkflowOutputEvent,
)
from agent_framework.orchestrations import HandoffBuilder, HandoffUserInputRequest
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
from semantic_kernel.agents import Agent, ChatCompletionAgent, HandoffOrchestration, OrchestrationHandoffs
@@ -214,17 +211,17 @@ async def _drain_events(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEv
return [event async for event in stream]
def _collect_handoff_requests(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
requests: list[RequestInfoEvent] = []
def _collect_handoff_requests(events: list[WorkflowEvent]) -> list[WorkflowEvent]:
requests: list[WorkflowEvent] = []
for event in events:
if isinstance(event, RequestInfoEvent) and isinstance(event.data, HandoffUserInputRequest):
if event.type == "request_info" and isinstance(event.data, HandoffUserInputRequest):
requests.append(event)
return requests
def _extract_final_conversation(events: list[WorkflowEvent]) -> list[ChatMessage]:
for event in events:
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
data = cast(list[ChatMessage], event.data)
return data
return []
@@ -6,7 +6,7 @@ import asyncio
from collections.abc import Sequence
from typing import cast
from agent_framework import ChatAgent, HostedCodeInterpreterTool, MagenticBuilder, WorkflowOutputEvent
from agent_framework import ChatAgent, HostedCodeInterpreterTool, MagenticBuilderWorkflowEvent
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
from semantic_kernel.agents import (
Agent,
@@ -148,7 +148,7 @@ async def run_agent_framework_example(prompt: str) -> str | None:
final_text: str | None = None
async for event in workflow.run(prompt, stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
final_text = cast(str, event.data)
return final_text
@@ -6,8 +6,9 @@ import asyncio
from collections.abc import Sequence
from typing import cast
from agent_framework import ChatMessage, SequentialBuilder, WorkflowOutputEvent
from agent_framework import ChatMessage
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from semantic_kernel.agents import Agent, ChatCompletionAgent, SequentialOrchestration
from semantic_kernel.agents.runtime import InProcessRuntime
@@ -77,7 +78,7 @@ async def run_agent_framework_example(prompt: str) -> list[ChatMessage]:
conversation_outputs: list[list[ChatMessage]] = []
async for event in workflow.run(prompt, stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
conversation_outputs.append(cast(list[ChatMessage], event.data))
return conversation_outputs[-1] if conversation_outputs else []
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, ClassVar, cast
######################################################################
# region Agent Framework imports
######################################################################
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, WorkflowOutputEvent, handler
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
from pydantic import BaseModel, Field
######################################################################
@@ -232,7 +232,7 @@ async def run_agent_framework_workflow_example() -> str | None:
final_text: str | None = None
async for event in workflow.run(CommonEvents.START_PROCESS, stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
final_text = cast(str, event.data)
return final_text
@@ -17,7 +17,7 @@ from agent_framework import (
WorkflowBuilder,
WorkflowContext,
WorkflowExecutor,
WorkflowOutputEvent,
handler,
)
from pydantic import BaseModel, Field
@@ -257,7 +257,7 @@ async def run_agent_framework_nested_workflow(initial_message: str) -> Sequence[
results: list[str] = []
async for event in outer_workflow.run(initial_message, stream=True):
if isinstance(event, WorkflowOutputEvent):
if event.type == "output":
results.append(cast(str, event.data))
return results