[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
@@ -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:")