Python: (samples): adopt AzureOpenAIResponsesClient, reorganize orchestration examples, and fix workflow/orchestration bugs (#3873)

* adopt AzureOpenAIResponsesClient, reorganize orchestration examples, and fix workflow/orchestration bugs

* Updates

* add comment
This commit is contained in:
Evan Mattson
2026-02-12 19:46:58 +09:00
committed by GitHub
Unverified
parent 8457533c69
commit 1b10b051fd
73 changed files with 1612 additions and 686 deletions
@@ -26,24 +26,58 @@ from agent_framework.orchestrations import (
)
```
## Samples Overview
## Samples Overview (by directory)
| Sample | File | Concepts |
| ------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| Concurrent Orchestration (Default Aggregator) | [concurrent_agents.py](./concurrent_agents.py) | Fan-out to multiple agents; fan-in with default aggregator returning combined Messages |
| Concurrent Orchestration (Custom Aggregator) | [concurrent_custom_aggregator.py](./concurrent_custom_aggregator.py) | Override aggregator via callback; summarize results with an LLM |
| Concurrent Orchestration (Custom Agent Executors) | [concurrent_custom_agent_executors.py](./concurrent_custom_agent_executors.py) | Child executors own Agents; concurrent fan-out/fan-in via ConcurrentBuilder |
| Group Chat with Agent Manager | [group_chat_agent_manager.py](./group_chat_agent_manager.py) | Agent-based manager using `with_orchestrator(agent=)` to select next speaker |
| Group Chat Philosophical Debate | [group_chat_philosophical_debate.py](./group_chat_philosophical_debate.py) | Agent manager moderates long-form, multi-round debate across diverse participants |
| Group Chat with Simple Function Selector | [group_chat_simple_selector.py](./group_chat_simple_selector.py) | Group chat with a simple function selector for next speaker |
| Handoff (Simple) | [handoff_simple.py](./handoff_simple.py) | Single-tier routing: triage agent routes to specialists, control returns to user after each specialist response |
| Handoff (Autonomous) | [handoff_autonomous.py](./handoff_autonomous.py) | Autonomous mode: specialists iterate independently until invoking a handoff tool using `.with_autonomous_mode()` |
| Handoff with Code Interpreter | [handoff_with_code_interpreter_file.py](./handoff_with_code_interpreter_file.py) | Retrieve file IDs from code interpreter output in handoff workflow |
| Magentic Workflow (Multi-Agent) | [magentic.py](./magentic.py) | Orchestrate multiple agents with Magentic manager and streaming |
| Magentic + Human Plan Review | [magentic_human_plan_review.py](./magentic_human_plan_review.py) | Human reviews/updates the plan before execution |
| Magentic + Checkpoint Resume | [magentic_checkpoint.py](./magentic_checkpoint.py) | Resume Magentic orchestration from saved checkpoints |
| Sequential Orchestration (Agents) | [sequential_agents.py](./sequential_agents.py) | Chain agents sequentially with shared conversation context |
| Sequential Orchestration (Custom Executor) | [sequential_custom_executors.py](./sequential_custom_executors.py) | Mix agents with a summarizer that appends a compact summary |
### concurrent
| Sample | File | Concepts |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Concurrent Orchestration (Default Aggregator) | [concurrent/concurrent_agents.py](./concurrent/concurrent_agents.py) | Fan-out to multiple agents; fan-in with default aggregator returning combined Messages |
| Concurrent Orchestration (Custom Aggregator) | [concurrent/concurrent_custom_aggregator.py](./concurrent/concurrent_custom_aggregator.py) | Override aggregator via callback; summarize results with an LLM |
| Concurrent Orchestration (Custom Agent Executors) | [concurrent/concurrent_custom_agent_executors.py](./concurrent/concurrent_custom_agent_executors.py) | Child executors own Agents; concurrent fan-out/fan-in via ConcurrentBuilder |
| Concurrent Orchestration as Agent | [concurrent/concurrent_workflow_as_agent.py](./concurrent/concurrent_workflow_as_agent.py) | Build a ConcurrentBuilder workflow and expose it as an agent via `workflow.as_agent(...)` |
| Tool Approval with ConcurrentBuilder | [concurrent/concurrent_builder_tool_approval.py](./concurrent/concurrent_builder_tool_approval.py) | Require human approval for sensitive tools across concurrent participants |
| ConcurrentBuilder Request Info | [concurrent/concurrent_request_info.py](./concurrent/concurrent_request_info.py) | Review concurrent agent outputs before aggregation using `.with_request_info()` |
### sequential
| Sample | File | Concepts |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| Sequential Orchestration (Agents) | [sequential/sequential_agents.py](./sequential/sequential_agents.py) | Chain agents sequentially with shared conversation context |
| Sequential Orchestration (Custom Executor) | [sequential/sequential_custom_executors.py](./sequential/sequential_custom_executors.py) | Mix agents with a summarizer that appends a compact summary |
| Sequential Orchestration as Agent | [sequential/sequential_workflow_as_agent.py](./sequential/sequential_workflow_as_agent.py) | Build a SequentialBuilder workflow and expose it as an agent via `workflow.as_agent(...)` |
| Tool Approval with SequentialBuilder | [sequential/sequential_builder_tool_approval.py](./sequential/sequential_builder_tool_approval.py) | Require human approval for sensitive tools in SequentialBuilder workflows |
| SequentialBuilder Request Info | [sequential/sequential_request_info.py](./sequential/sequential_request_info.py) | Request info for agent responses mid-orchestration using `.with_request_info()` |
### group-chat
| Sample | File | Concepts |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| Group Chat with Agent Manager | [group-chat/group_chat_agent_manager.py](./group-chat/group_chat_agent_manager.py) | Agent-based manager using `with_orchestrator(agent=)` to select next speaker |
| Group Chat Philosophical Debate | [group-chat/group_chat_philosophical_debate.py](./group-chat/group_chat_philosophical_debate.py) | Agent manager moderates long-form, multi-round debate across diverse participants |
| Group Chat with Simple Selector | [group-chat/group_chat_simple_selector.py](./group-chat/group_chat_simple_selector.py) | Group chat with a simple function selector for next speaker |
| Group Chat Orchestration as Agent | [group-chat/group_chat_workflow_as_agent.py](./group-chat/group_chat_workflow_as_agent.py) | Build a GroupChatBuilder workflow and wrap it as an agent for composition |
| Tool Approval with GroupChatBuilder | [group-chat/group_chat_builder_tool_approval.py](./group-chat/group_chat_builder_tool_approval.py) | Require human approval for sensitive tools in group chat orchestration |
| GroupChatBuilder Request Info | [group-chat/group_chat_request_info.py](./group-chat/group_chat_request_info.py) | Steer group discussions with periodic guidance using `.with_request_info()` |
### handoff
| Sample | File | Concepts |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Handoff (Simple) | [handoff/handoff_simple.py](./handoff/handoff_simple.py) | Single-tier routing: triage agent routes to specialists, control returns to user after each specialist response |
| Handoff (Autonomous) | [handoff/handoff_autonomous.py](./handoff/handoff_autonomous.py) | Autonomous mode: specialists iterate independently until invoking a handoff tool using `.with_autonomous_mode()` |
| Handoff with Code Interpreter | [handoff/handoff_with_code_interpreter_file.py](./handoff/handoff_with_code_interpreter_file.py) | Retrieve file IDs from code interpreter output in handoff workflow |
| Handoff with Tool Approval + Checkpoint | [handoff/handoff_with_tool_approval_checkpoint_resume.py](./handoff/handoff_with_tool_approval_checkpoint_resume.py) | Capture tool-approval decisions in checkpoints and resume from persisted state |
| Handoff Orchestration as Agent | [handoff/handoff_workflow_as_agent.py](./handoff/handoff_workflow_as_agent.py) | Build a HandoffBuilder workflow and expose it as an agent, including HITL request/response flow |
### magentic
| Sample | File | Concepts |
| ---------------------------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
| Magentic Workflow | [magentic/magentic.py](./magentic/magentic.py) | Orchestrate multiple agents with a Magentic manager and streaming |
| Magentic + Human Plan Review | [magentic/magentic_human_plan_review.py](./magentic/magentic_human_plan_review.py) | Human reviews or updates the plan before execution |
| Magentic + Checkpoint Resume | [magentic/magentic_checkpoint.py](./magentic/magentic_checkpoint.py) | Resume Magentic orchestration from saved checkpoints |
| Magentic Orchestration as Agent | [magentic/magentic_workflow_as_agent.py](./magentic/magentic_workflow_as_agent.py) | Build a MagenticBuilder workflow and reuse it as an agent |
## Tips
@@ -60,8 +94,9 @@ These may appear in event streams (executor_invoked/executor_completed). They're
## Environment Variables
- **AzureOpenAIChatClient**: Set Azure OpenAI environment variables as documented [here](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/chat_client/README.md#environment-variables).
Orchestration samples that use `AzureOpenAIResponsesClient` expect:
- **OpenAI** (used in some orchestration samples):
- [OpenAIChatClient env vars](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/agents/openai_chat_client/README.md)
- [OpenAIResponsesClient env vars](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/agents/openai_responses_client/README.md)
- `AZURE_AI_PROJECT_ENDPOINT` (Azure AI Foundry Agent Service (V2) project endpoint)
- `AZURE_AI_MODEL_DEPLOYMENT_NAME` (model deployment name)
These values are passed directly into the client constructor via `os.getenv()` in sample code.
@@ -1,10 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Any
from agent_framework import Message
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
@@ -22,14 +23,19 @@ Demonstrates:
- Workflow completion when idle with no pending work
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI access configured for AzureOpenAIResponsesClient (use az login + env vars)
- Familiarity with Workflow events (WorkflowEvent)
"""
async def main() -> None:
# 1) Create three domain agents using AzureOpenAIChatClient
client = AzureOpenAIChatClient(credential=AzureCliCredential())
# 1) Create three domain agents using AzureOpenAIResponsesClient
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
researcher = client.as_agent(
instructions=(
@@ -0,0 +1,207 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from collections.abc import AsyncIterable
from typing import Annotated
from agent_framework import (
Content,
Message,
WorkflowEvent,
tool,
)
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
"""
Sample: Concurrent Workflow with Tool Approval Requests
This sample demonstrates how to use ConcurrentBuilder with tools that require human
approval before execution. Multiple agents run in parallel, and any tool requiring
approval will pause the workflow until the human responds.
This sample works as follows:
1. A ConcurrentBuilder workflow is created with two agents running in parallel.
2. Both agents have the same tools, including one requiring approval (execute_trade).
3. Both agents receive the same task and work concurrently on their respective stocks.
4. When either agent tries to execute a trade, it triggers an approval request.
5. The sample simulates human approval and the workflow completes.
6. Results from both agents are aggregated and output.
Purpose:
Show how tool call approvals work in parallel execution scenarios where multiple
agents may independently trigger approval requests.
Demonstrate:
- Handling multiple approval requests from different agents in concurrent workflows.
- Handling during concurrent agent execution.
- Understanding that approval pauses only the agent that triggered it, not all agents.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- OpenAI or Azure OpenAI configured with the required environment variables.
- Basic familiarity with ConcurrentBuilder and streaming workflow events.
"""
# 1. Define market data tools (no approval required)
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# See:
# samples/getting_started/tools/function_tool_with_approval.py
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_stock_price(symbol: Annotated[str, "The stock ticker symbol"]) -> str:
"""Get the current stock price for a given symbol."""
# Mock data for demonstration
prices = {"AAPL": 175.50, "GOOGL": 140.25, "MSFT": 378.90, "AMZN": 178.75}
price = prices.get(symbol.upper(), 100.00)
return f"{symbol.upper()}: ${price:.2f}"
@tool(approval_mode="never_require")
def get_market_sentiment(symbol: Annotated[str, "The stock ticker symbol"]) -> str:
"""Get market sentiment analysis for a stock."""
# Mock sentiment data
mock_data = {
"AAPL": "Market sentiment for AAPL: Bullish (68% positive mentions in last 24h)",
"GOOGL": "Market sentiment for GOOGL: Neutral (50% positive mentions in last 24h)",
"MSFT": "Market sentiment for MSFT: Bullish (72% positive mentions in last 24h)",
"AMZN": "Market sentiment for AMZN: Bearish (40% positive mentions in last 24h)",
}
return mock_data.get(symbol.upper(), f"Market sentiment for {symbol.upper()}: Unknown")
# 2. Define trading tools (approval required)
@tool(approval_mode="always_require")
def execute_trade(
symbol: Annotated[str, "The stock ticker symbol"],
action: Annotated[str, "Either 'buy' or 'sell'"],
quantity: Annotated[int, "Number of shares to trade"],
) -> str:
"""Execute a stock trade. Requires human approval due to financial impact."""
return f"Trade executed: {action.upper()} {quantity} shares of {symbol.upper()}"
@tool(approval_mode="never_require")
def get_portfolio_balance() -> str:
"""Get current portfolio balance and available funds."""
return "Portfolio: $50,000 invested, $10,000 cash available. Holdings: AAPL, GOOGL, MSFT."
def _print_output(event: WorkflowEvent) -> None:
if not event.data:
raise ValueError("WorkflowEvent has no data")
if not isinstance(event.data, list) and not all(isinstance(msg, Message) for msg in event.data):
raise ValueError("WorkflowEvent data is not a list of Message")
messages: list[Message] = event.data # type: ignore
print("\n" + "-" * 60)
print("Workflow completed. Aggregated results from both agents:")
for msg in messages:
if msg.text:
print(f"- {msg.author_name or msg.role}: {msg.text}")
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, Content] | None:
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, Content] = {}
async for event in stream:
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 event.type == "output":
_print_output(event)
responses: dict[str, Content] = {}
if requests:
for request_id, request in requests.items():
if request.type == "function_approval_request":
print(f"\nSimulating human approval for: {request.function_call.name}") # type: ignore
# Create approval response
responses[request_id] = request.to_function_approval_response(approved=True)
return responses if responses else None
async def main() -> None:
# 3. Create two agents focused on different stocks but with the same tool sets
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
microsoft_agent = client.as_agent(
name="MicrosoftAgent",
instructions=(
"You are a personal trading assistant focused on Microsoft (MSFT). "
"You manage my portfolio and take actions based on market data."
),
tools=[get_stock_price, get_market_sentiment, get_portfolio_balance, execute_trade],
)
google_agent = client.as_agent(
name="GoogleAgent",
instructions=(
"You are a personal trading assistant focused on Google (GOOGL). "
"You manage my trades and portfolio based on market conditions."
),
tools=[get_stock_price, get_market_sentiment, get_portfolio_balance, execute_trade],
)
# 4. Build a concurrent workflow with both agents
# ConcurrentBuilder requires at least 2 participants for fan-out
workflow = ConcurrentBuilder(participants=[microsoft_agent, google_agent]).build()
# 5. Start the workflow - both agents will process the same task in parallel
print("Starting concurrent workflow with tool approval...")
print("-" * 60)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run(
"Manage my portfolio. Use a max of 5000 dollars to adjust my position using "
"your best judgment based on market sentiment. No need to confirm trades with me.",
stream=True,
)
pending_responses = await process_event_stream(stream)
while pending_responses is not None:
# Run the workflow until there is no more human feedback to provide,
# in which case this workflow completes.
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await process_event_stream(stream)
"""
Sample Output:
Starting concurrent workflow with tool approval...
------------------------------------------------------------
Approval requested for tool: execute_trade
Arguments: {"symbol":"MSFT","action":"buy","quantity":13}
Approval requested for tool: execute_trade
Arguments: {"symbol":"GOOGL","action":"buy","quantity":35}
Simulating human approval for: execute_trade
Simulating human approval for: execute_trade
------------------------------------------------------------
Workflow completed. Aggregated results from both agents:
- user: Manage my portfolio. Use a max of 5000 dollars to adjust my position using your best judgment based on
market sentiment. No need to confirm trades with me.
- MicrosoftAgent: I have successfully executed the trade, purchasing 13 shares of Microsoft (MSFT). This action
was based on the positive market sentiment and available funds within the specified limit.
Your portfolio has been adjusted accordingly.
- GoogleAgent: I have successfully executed the trade, purchasing 35 shares of GOOGL. If you need further
assistance or any adjustments, feel free to ask!
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Any
from agent_framework import (
@@ -12,7 +13,7 @@ from agent_framework import (
WorkflowContext,
handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
@@ -25,21 +26,22 @@ and emit AgentExecutorResponse outputs, which allows reuse of the high-level
ConcurrentBuilder API and the default aggregator.
Demonstrates:
- Executors that create their Agent in __init__ (via AzureOpenAIChatClient)
- Executors that create their Agent in __init__ (via AzureOpenAIResponsesClient)
- A @handler that converts AgentExecutorRequest -> AgentExecutorResponse
- ConcurrentBuilder(participants=[...]) to build fan-out/fan-in
- Default aggregator returning list[Message] (one user + one assistant per agent)
- Workflow completion when all participants become idle
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient (az login + required env vars)
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient (az login + required env vars)
"""
class ResearcherExec(Executor):
agent: Agent
def __init__(self, client: AzureOpenAIChatClient, id: str = "researcher"):
def __init__(self, client: AzureOpenAIResponsesClient, id: str = "researcher"):
self.agent = client.as_agent(
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
@@ -59,7 +61,7 @@ class ResearcherExec(Executor):
class MarketerExec(Executor):
agent: Agent
def __init__(self, client: AzureOpenAIChatClient, id: str = "marketer"):
def __init__(self, client: AzureOpenAIResponsesClient, id: str = "marketer"):
self.agent = client.as_agent(
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
@@ -79,7 +81,7 @@ class MarketerExec(Executor):
class LegalExec(Executor):
agent: Agent
def __init__(self, client: AzureOpenAIChatClient, id: str = "legal"):
def __init__(self, client: AzureOpenAIResponsesClient, id: str = "legal"):
self.agent = client.as_agent(
instructions=(
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
@@ -97,7 +99,11 @@ class LegalExec(Executor):
async def main() -> None:
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
researcher = ResearcherExec(client)
marketer = MarketerExec(client)
@@ -1,10 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Any
from agent_framework import Message
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
@@ -13,7 +14,7 @@ Sample: Concurrent Orchestration with Custom Aggregator
Build a concurrent workflow with ConcurrentBuilder that fans out one prompt to
multiple domain agents and fans in their responses. Override the default
aggregator with a custom async callback that uses AzureOpenAIChatClient.get_response()
aggregator with a custom async callback that uses AzureOpenAIResponsesClient.get_response()
to synthesize a concise, consolidated summary from the experts' outputs.
The workflow completes when all participants become idle.
@@ -24,12 +25,17 @@ Demonstrates:
- Workflow output yielded with the synthesized summary string
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient (az login + required env vars)
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient (az login + required env vars)
"""
async def main() -> None:
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
researcher = client.as_agent(
instructions=(
@@ -86,9 +92,7 @@ async def main() -> None:
# • Default aggregator -> returns list[Message] (one user + one assistant per agent)
# • Custom callback -> return value becomes workflow output (string here)
# The callback can be sync or async; it receives list[AgentExecutorResponse].
workflow = (
ConcurrentBuilder(participants=[researcher, marketer, legal]).with_aggregator(summarize_results).build()
)
workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).with_aggregator(summarize_results).build()
events = await workflow.run("We are launching a new budget-friendly electric bike for urban commuters.")
outputs = events.get_outputs()
@@ -0,0 +1,203 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Sample: Request Info with ConcurrentBuilder
This sample demonstrates using the `.with_request_info()` method to pause a
ConcurrentBuilder workflow for specific agents, allowing human review and
modification of individual agent outputs before aggregation.
Purpose:
Show how to use the request info API that pauses for selected concurrent agents,
allowing review and steering of their results.
Demonstrate:
- Configuring request info with `.with_request_info()` for specific agents
- Reviewing output from individual agents during concurrent execution
- Injecting human guidance for specific agents before aggregation
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables
- Authentication via azure-identity (run az login before executing)
"""
import asyncio
import os
from collections.abc import AsyncIterable
from typing import Any
from agent_framework import (
AgentExecutorResponse,
Message,
WorkflowEvent,
)
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import AgentRequestInfoResponse, ConcurrentBuilder
from azure.identity import AzureCliCredential
# Store chat client at module level for aggregator access
_chat_client: AzureOpenAIResponsesClient | None = None
async def aggregate_with_synthesis(results: list[AgentExecutorResponse]) -> Any:
"""Custom aggregator that synthesizes concurrent agent outputs using an LLM.
This aggregator extracts the outputs from each parallel agent and uses the
chat client to create a unified summary, incorporating any human feedback
that was injected into the conversation.
Args:
results: List of responses from all concurrent agents
Returns:
The synthesized summary text
"""
if not _chat_client:
return "Error: Chat client not initialized"
# Extract each agent's final output
expert_sections: list[str] = []
human_guidance = ""
for r in results:
try:
messages = getattr(r.agent_response, "messages", [])
final_text = messages[-1].text if messages and hasattr(messages[-1], "text") else "(no content)"
expert_sections.append(f"{getattr(r, 'executor_id', 'analyst')}:\n{final_text}")
# Check for human feedback in the conversation (will be last user message if present)
if r.full_conversation:
for msg in reversed(r.full_conversation):
if msg.role == "user" and msg.text and "perspectives" not in msg.text.lower():
human_guidance = msg.text
break
except Exception:
expert_sections.append(f"{getattr(r, 'executor_id', 'analyst')}: (error extracting output)")
# Build prompt with human guidance if provided
guidance_text = f"\n\nHuman guidance: {human_guidance}" if human_guidance else ""
system_msg = Message(
"system",
text=(
"You are a synthesis expert. Consolidate the following analyst perspectives "
"into one cohesive, balanced summary (3-4 sentences). If human guidance is provided, "
"prioritize aspects as directed."
),
)
user_msg = Message("user", text="\n\n".join(expert_sections) + guidance_text)
response = await _chat_client.get_response([system_msg, user_msg])
return response.messages[-1].text if response.messages else ""
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, AgentRequestInfoResponse] | None:
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, AgentExecutorResponse] = {}
async for event in stream:
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
requests[event.request_id] = event.data
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")
print("=" * 60)
print("Final synthesized analysis:")
print(event.data)
# Process any requests for human feedback
responses: dict[str, AgentRequestInfoResponse] = {}
if requests:
for request_id, request in requests.items():
print("\n" + "-" * 40)
print("INPUT REQUESTED")
print(
f"Agent {request.executor_id} just responded with: '{request.agent_response.text}'. "
"Please provide your feedback."
)
print("-" * 40)
if request.full_conversation:
print("Conversation context:")
recent = (
request.full_conversation[-2:] if len(request.full_conversation) > 2 else request.full_conversation
)
for msg in recent:
name = msg.author_name or msg.role
text = (msg.text or "")[:150]
print(f" [{name}]: {text}...")
print("-" * 40)
# Get human input to steer this agent's contribution
user_input = input("Your guidance for the analysts (or 'skip' to approve): ") # noqa: ASYNC250
if user_input.lower() == "skip":
user_input = AgentRequestInfoResponse.approve()
else:
user_input = AgentRequestInfoResponse.from_strings([user_input])
responses[request_id] = user_input
return responses if responses else None
async def main() -> None:
global _chat_client
_chat_client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create agents that analyze from different perspectives
technical_analyst = _chat_client.as_agent(
name="technical_analyst",
instructions=(
"You are a technical analyst. When given a topic, provide a technical "
"perspective focusing on implementation details, performance, and architecture. "
"Keep your analysis to 2-3 sentences."
),
)
business_analyst = _chat_client.as_agent(
name="business_analyst",
instructions=(
"You are a business analyst. When given a topic, provide a business "
"perspective focusing on ROI, market impact, and strategic value. "
"Keep your analysis to 2-3 sentences."
),
)
user_experience_analyst = _chat_client.as_agent(
name="ux_analyst",
instructions=(
"You are a UX analyst. When given a topic, provide a user experience "
"perspective focusing on usability, accessibility, and user satisfaction. "
"Keep your analysis to 2-3 sentences."
),
)
# Build workflow with request info enabled and custom aggregator
workflow = (
ConcurrentBuilder(participants=[technical_analyst, business_analyst, user_experience_analyst])
.with_aggregator(aggregate_with_synthesis)
# Only enable request info for the technical analyst agent
.with_request_info(agents=["technical_analyst"])
.build()
)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run("Analyze the impact of large language models on software development.", stream=True)
pending_responses = await process_event_stream(stream)
while pending_responses is not None:
# Run the workflow until there is no more human feedback to provide,
# in which case this workflow completes.
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await process_event_stream(stream)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,89 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
"""
Sample: Build a concurrent workflow orchestration and wrap it as an agent.
This script wires up a fan-out/fan-in workflow using `ConcurrentBuilder`, and then
invokes the entire orchestration through the `workflow.as_agent(...)` interface so
downstream coordinators can reuse the orchestration as a single agent.
Demonstrates:
- Fan-out to multiple agents, fan-in aggregation of final ChatMessages.
- Reusing the orchestrated workflow as an agent entry point with `workflow.as_agent(...)`.
- Workflow completion when idle with no pending work
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI access configured for AzureOpenAIResponsesClient (use az login + env vars)
- Familiarity with Workflow events (WorkflowEvent with type "output")
"""
def clear_and_redraw(buffers: dict[str, str], agent_order: list[str]) -> None:
"""Clear terminal and redraw all agent outputs grouped together."""
# ANSI escape: clear screen and move cursor to top-left
print("\033[2J\033[H", end="")
print("===== Concurrent Agent Streaming (Live) =====\n")
for name in agent_order:
print(f"--- {name} ---")
print(buffers.get(name, ""))
print()
print("", end="", flush=True)
async def main() -> None:
# 1) Create three domain agents using AzureOpenAIResponsesClient
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
researcher = client.as_agent(
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
" opportunities, and risks."
),
name="researcher",
)
marketer = client.as_agent(
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
" aligned to the prompt."
),
name="marketer",
)
legal = client.as_agent(
instructions=(
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
" based on the prompt."
),
name="legal",
)
# 2) Build a concurrent workflow
workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build()
# 3) Expose the concurrent workflow as an agent for easy reuse
agent = workflow.as_agent(name="ConcurrentWorkflowAgent")
prompt = "We are launching a new budget-friendly electric bike for urban commuters."
agent_response = await agent.run(prompt)
print("===== Final Aggregated Response =====\n")
for message in agent_response.messages:
# The agent_response contains messages from all participants concatenated
# into a single message.
print(f"{message.author_name}: {message.text}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import cast
from agent_framework import (
@@ -8,7 +9,7 @@ from agent_framework import (
AgentResponseUpdate,
Message,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import GroupChatBuilder
from azure.identity import AzureCliCredential
@@ -21,7 +22,8 @@ What it does:
- Coordinates a researcher and writer agent to solve tasks collaboratively
Prerequisites:
- OpenAI environment variables configured for OpenAIChatClient
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured for AzureOpenAIResponsesClient
"""
ORCHESTRATOR_AGENT_INSTRUCTIONS = """
@@ -36,7 +38,11 @@ Guidelines:
async def main() -> None:
# Create a chat client using Azure OpenAI and Azure CLI credentials for all agents
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Orchestrator agent that manages the conversation
# Note: This agent (and the underlying chat client) must support structured outputs.
@@ -88,26 +94,35 @@ async def main() -> None:
print(f"TASK: {task}\n")
print("=" * 80)
# Keep track of the last response to format output nicely in streaming mode
last_response_id: str | None = None
# Track current speaker for readable streaming output.
pending_speaker: str | None = None
current_speaker: str | None = None
async for event in workflow.run(task, stream=True):
if event.type == "output":
data = event.data
if isinstance(data, AgentResponseUpdate):
rid = data.response_id
if rid != last_response_id:
if last_response_id is not None:
print("\n")
print(f"{data.author_name}:", end=" ", flush=True)
last_response_id = rid
print(data.text, end="", flush=True)
elif event.type == "output":
# The output of the group chat workflow is a collection of chat messages from all participants
outputs = cast(list[Message], 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 event.type != "output":
continue
data = event.data
if isinstance(data, AgentResponseUpdate):
if data.author_name:
pending_speaker = data.author_name
if not data.text:
continue
speaker = data.author_name or pending_speaker or "assistant"
if speaker != current_speaker:
if current_speaker is not None:
print("\n")
print(f"{speaker}:", end=" ", flush=True)
current_speaker = speaker
print(data.text, end="", flush=True)
continue
# The output of the group chat workflow is a collection of chat messages from all participants
outputs = cast(list[Message], 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__":
@@ -0,0 +1,221 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from collections.abc import AsyncIterable
from typing import Annotated, cast
from agent_framework import (
Content,
Message,
WorkflowEvent,
tool,
)
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import GroupChatBuilder, GroupChatState
from azure.identity import AzureCliCredential
"""
Sample: Group Chat Workflow with Tool Approval Requests
This sample demonstrates how to use GroupChatBuilder with tools that require human
approval before execution. A group of specialized agents collaborate on a task, and
sensitive tool calls trigger human-in-the-loop approval.
This sample works as follows:
1. A GroupChatBuilder workflow is created with multiple specialized agents.
2. A selector function determines which agent speaks next based on conversation state.
3. Agents collaborate on a software deployment task.
4. When the deployment agent tries to deploy to production, it triggers an approval request.
5. The sample simulates human approval and the workflow completes.
Purpose:
Show how tool call approvals integrate with multi-agent group chat workflows where
different agents have different levels of tool access.
Demonstrate:
- Using set_select_speakers_func with agents that have approval-required tools.
- Handling request_info events (type='request_info') in group chat scenarios.
- Multi-round group chat with tool approval interruption and resumption.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- OpenAI or Azure OpenAI configured with the required environment variables.
- Basic familiarity with GroupChatBuilder and streaming workflow events.
"""
# 1. Define tools for different agents
# 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 run_tests(test_suite: Annotated[str, "Name of the test suite to run"]) -> str:
"""Run automated tests for the application."""
return f"Test suite '{test_suite}' completed: 47 passed, 0 failed, 0 skipped"
@tool(approval_mode="never_require")
def check_staging_status() -> str:
"""Check the current status of the staging environment."""
return "Staging environment: Healthy, Version 2.3.0 deployed, All services running"
@tool(approval_mode="always_require")
def deploy_to_production(
version: Annotated[str, "The version to deploy"],
components: Annotated[str, "Comma-separated list of components to deploy"],
) -> str:
"""Deploy specified components to production. Requires human approval."""
return f"Production deployment complete: Version {version}, Components: {components}"
@tool(approval_mode="never_require")
def create_rollback_plan(version: Annotated[str, "The version being deployed"]) -> str:
"""Create a rollback plan for the deployment."""
return (
f"Rollback plan created for version {version}: "
"Automated rollback to v2.2.0 if health checks fail within 5 minutes"
)
# 2. Define the speaker selector function
def select_next_speaker(state: GroupChatState) -> str:
"""Select the next speaker based on the conversation flow.
This simple selector follows a predefined flow:
1. QA Engineer runs tests
2. DevOps Engineer checks staging and creates rollback plan
3. DevOps Engineer deploys to production (triggers approval)
"""
if not state.conversation:
raise RuntimeError("Conversation is empty; cannot select next speaker.")
if len(state.conversation) == 1:
return "QAEngineer" # First speaker
return "DevOpsEngineer" # Subsequent speakers
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, Content] | None:
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, Content] = {}
async for event in stream:
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 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[Message], event.data)
for msg in outputs:
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
responses: dict[str, Content] = {}
if requests:
for request_id, request in requests.items():
if request.type == "function_approval_request":
print("\n[APPROVAL REQUIRED]")
print(f" Tool: {request.function_call.name}") # type: ignore
print(f" Arguments: {request.function_call.arguments}") # type: ignore
print(f"Simulating human approval for: {request.function_call.name}") # type: ignore
# Create approval response
responses[request_id] = request.to_function_approval_response(approved=True)
return responses if responses else None
async def main() -> None:
# 3. Create specialized agents
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
qa_engineer = client.as_agent(
name="QAEngineer",
instructions=(
"You are a QA engineer responsible for running tests before deployment. "
"Run the appropriate test suites and report results clearly."
),
tools=[run_tests],
)
devops_engineer = client.as_agent(
name="DevOpsEngineer",
instructions=(
"You are a DevOps engineer responsible for deployments. First check staging "
"status and create a rollback plan, then proceed with production deployment. "
"Always ensure safety measures are in place before deploying."
),
tools=[check_staging_status, create_rollback_plan, deploy_to_production],
)
# 4. Build a group chat workflow with the selector function
# max_rounds=4: Set a hard limit to 4 rounds
# First round: QAEngineer speaks
# Second round: DevOpsEngineer speaks (check staging + create rollback)
# Third round: DevOpsEngineer speaks with an approval request (deploy to production)
# Fourth round: DevOpsEngineer speaks again after approval
workflow = GroupChatBuilder(
participants=[qa_engineer, devops_engineer],
max_rounds=4,
selection_func=select_next_speaker,
).build()
# 5. Start the workflow
print("Starting group chat workflow for software deployment...")
print(f"Agents: {[qa_engineer.name, devops_engineer.name]}")
print("-" * 60)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run(
"We need to deploy version 2.4.0 to production. Please coordinate the deployment.", stream=True
)
pending_responses = await process_event_stream(stream)
while pending_responses is not None:
# Run the workflow until there is no more human feedback to provide,
# in which case this workflow completes.
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await process_event_stream(stream)
"""
Sample Output:
Starting group chat workflow for software deployment...
Agents: QA Engineer, DevOps Engineer
------------------------------------------------------------
[QAEngineer]: Running the integration test suite to verify the application
before deployment... Test suite 'integration' completed: 47 passed, 0 failed.
All tests passing - ready for deployment.
[DevOpsEngineer]: Checking staging environment status... Staging is healthy
with version 2.3.0. Creating rollback plan for version 2.4.0... Rollback plan
created with automated rollback to v2.2.0 if health checks fail.
[APPROVAL REQUIRED]
Tool: deploy_to_production
Arguments: {"version": "2.4.0", "components": "api,web,worker"}
============================================================
Human review required for production deployment!
In a real scenario, you would review the deployment details here.
Simulating approval for demo purposes...
============================================================
[DevOpsEngineer]: Production deployment complete! Version 2.4.0 has been
successfully deployed with components: api, web, worker.
------------------------------------------------------------
Deployment workflow completed successfully!
All agents have finished their tasks.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -2,6 +2,7 @@
import asyncio
import logging
import os
from typing import cast
from agent_framework import (
@@ -9,7 +10,7 @@ from agent_framework import (
AgentResponseUpdate,
Message,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import GroupChatBuilder
from azure.identity import AzureCliCredential
@@ -37,12 +38,17 @@ Participants represent:
- Doctor from Scandinavia (public health, equity, societal support)
Prerequisites:
- OpenAI environment variables configured for OpenAIChatClient
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured for AzureOpenAIResponsesClient
"""
def _get_chat_client() -> AzureOpenAIChatClient:
return AzureOpenAIChatClient(credential=AzureCliCredential())
def _get_chat_client() -> AzureOpenAIResponsesClient:
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
async def main() -> None:
@@ -240,26 +246,35 @@ Share your perspective authentically. Feel free to:
print("DISCUSSION BEGINS")
print("=" * 80 + "\n")
# Keep track of the last response to format output nicely in streaming mode
last_response_id: str | None = None
# Track current speaker for readable streaming output.
pending_speaker: str | None = None
current_speaker: str | None = None
async for event in workflow.run(f"Please begin the discussion on: {topic}", stream=True):
if event.type == "output":
data = event.data
if isinstance(data, AgentResponseUpdate):
rid = data.response_id
if rid != last_response_id:
if last_response_id is not None:
print("\n")
print(f"{data.author_name}:", end=" ", flush=True)
last_response_id = rid
print(data.text, end="", flush=True)
elif event.type == "output":
# The output of the group chat workflow is a collection of chat messages from all participants
outputs = cast(list[Message], 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 event.type != "output":
continue
data = event.data
if isinstance(data, AgentResponseUpdate):
if data.author_name:
pending_speaker = data.author_name
if not data.text:
continue
speaker = data.author_name or pending_speaker or "assistant"
if speaker != current_speaker:
if current_speaker is not None:
print("\n")
print(f"{speaker}:", end=" ", flush=True)
current_speaker = speaker
print(data.text, end="", flush=True)
continue
# The output of the group chat workflow is a collection of chat messages from all participants
outputs = cast(list[Message], data)
print("\n" + "=" * 80)
print("\nFinal Conversation Transcript:\n")
for message in outputs:
print(f"{message.author_name or message.role}: {message.text}\n")
"""
Sample Output:
@@ -0,0 +1,174 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Sample: Request Info with GroupChatBuilder
This sample demonstrates using the `.with_request_info()` method to pause a
GroupChatBuilder workflow BEFORE specific participants speak. By using the
`agents=` filter parameter, you can target only certain participants rather
than pausing before every turn.
Purpose:
Show how to use the request info API with selective filtering to pause before
specific participants speak, allowing human input to steer their response.
Demonstrate:
- Configuring request info with `.with_request_info(agents=[...])`
- Using agent filtering to reduce interruptions
- Steering agent behavior with pre-agent human input
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables
- Authentication via azure-identity (run az login before executing)
"""
import asyncio
import os
from collections.abc import AsyncIterable
from typing import cast
from agent_framework import (
AgentExecutorResponse,
Message,
WorkflowEvent,
)
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import AgentRequestInfoResponse, GroupChatBuilder
from azure.identity import AzureCliCredential
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, AgentRequestInfoResponse] | None:
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, AgentExecutorResponse] = {}
async for event in stream:
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
requests[event.request_id] = event.data
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")
print("=" * 60)
print("Final discussion summary:")
# To make the type checker happy, we cast event.data to the expected type
outputs = cast(list[Message], event.data)
for msg in outputs:
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
responses: dict[str, AgentRequestInfoResponse] = {}
if requests:
for request_id, request in requests.items():
# Display pre-agent context for human input
print("\n" + "-" * 40)
print("INPUT REQUESTED")
print(
f"Agent {request.executor_id} just responded with: '{request.agent_response.text}'. "
"Please provide your feedback."
)
print("-" * 40)
if request.full_conversation:
print("Conversation context:")
recent = (
request.full_conversation[-2:] if len(request.full_conversation) > 2 else request.full_conversation
)
for msg in recent:
name = msg.author_name or msg.role
text = (msg.text or "")[:150]
print(f" [{name}]: {text}...")
print("-" * 40)
# Get human input to steer the agent
user_input = input(f"Feedback for {request.executor_id} (or 'skip' to approve): ") # noqa: ASYNC250
if user_input.lower() == "skip":
user_input = AgentRequestInfoResponse.approve()
else:
user_input = AgentRequestInfoResponse.from_strings([user_input])
responses[request_id] = user_input
return responses if responses else None
async def main() -> None:
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create agents for a group discussion
optimist = client.as_agent(
name="optimist",
instructions=(
"You are an optimistic team member. You see opportunities and potential "
"in ideas. Engage constructively with the discussion, building on others' "
"points while maintaining a positive outlook. Keep responses to 2-3 sentences."
),
)
pragmatist = client.as_agent(
name="pragmatist",
instructions=(
"You are a pragmatic team member. You focus on practical implementation "
"and realistic timelines. Sometimes you disagree with overly optimistic views. "
"Keep responses to 2-3 sentences."
),
)
creative = client.as_agent(
name="creative",
instructions=(
"You are a creative team member. You propose innovative solutions and "
"think outside the box. You may suggest alternatives to conventional approaches. "
"Keep responses to 2-3 sentences."
),
)
# Orchestrator coordinates the discussion
orchestrator = client.as_agent(
name="orchestrator",
instructions=(
"You are a discussion manager coordinating a team conversation between participants. "
"Your job is to select who speaks next.\n\n"
"RULES:\n"
"1. Rotate through ALL participants - do not favor any single participant\n"
"2. Each participant should speak at least once before any participant speaks twice\n"
"3. Continue for at least 5 rounds before ending the discussion\n"
"4. Do NOT select the same participant twice in a row"
),
)
# Build workflow with request info enabled
# Using agents= filter to only pause before pragmatist speaks (not every turn)
# max_rounds=6: Limit to 6 rounds
workflow = (
GroupChatBuilder(
participants=[optimist, pragmatist, creative],
max_rounds=6,
orchestrator_agent=orchestrator,
)
.with_request_info(agents=[pragmatist]) # Only pause before pragmatist speaks
.build()
)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run(
"Discuss how our team should approach adopting AI tools for productivity. "
"Consider benefits, risks, and implementation strategies.",
stream=True,
)
pending_responses = await process_event_stream(stream)
while pending_responses is not None:
# Run the workflow until there is no more human feedback to provide,
# in which case this workflow completes.
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await process_event_stream(stream)
if __name__ == "__main__":
asyncio.run(main())
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import cast
from agent_framework import (
@@ -8,7 +9,7 @@ from agent_framework import (
AgentResponseUpdate,
Message,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import GroupChatBuilder, GroupChatState
from azure.identity import AzureCliCredential
@@ -20,7 +21,8 @@ What it does:
- Uses a pure Python function to control speaker selection based on conversation state
Prerequisites:
- OpenAI environment variables configured for OpenAIChatClient
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured for AzureOpenAIResponsesClient
"""
@@ -33,7 +35,11 @@ def round_robin_selector(state: GroupChatState) -> str:
async def main() -> None:
# Create a chat client using Azure OpenAI and Azure CLI credentials for all agents
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Participant agents
expert = Agent(
@@ -0,0 +1,85 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import GroupChatBuilder
from azure.identity import AzureCliCredential
"""
Sample: Group Chat Orchestration
What it does:
- Demonstrates the generic GroupChatBuilder with a agent orchestrator directing two agents.
- The orchestrator coordinates a researcher (chat completions) and a writer (responses API) to solve a task.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured for `AzureOpenAIResponsesClient`.
"""
async def main() -> None:
researcher = Agent(
name="Researcher",
description="Collects relevant background information.",
instructions="Gather concise facts that help a teammate answer the question.",
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
writer = Agent(
name="Writer",
description="Synthesizes a polished answer using the gathered notes.",
instructions="Compose clear and structured answers using any notes provided.",
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
# intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds
# (Intermediate outputs will be emitted as WorkflowOutputEvent events)
workflow = GroupChatBuilder(
participants=[researcher, writer],
intermediate_outputs=True,
orchestrator_agent=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
name="Orchestrator",
instructions="You coordinate a team conversation to solve the user's task.",
),
).build()
task = "Outline the core considerations for planning a community hackathon, and finish with a concise action plan."
print("\nStarting Group Chat Workflow...\n")
print(f"Input: {task}\n")
try:
workflow_agent = workflow.as_agent(name="GroupChatWorkflowAgent")
agent_result = await workflow_agent.run(task)
if agent_result.messages:
# The output should contain a message from the researcher, a message from the writer,
# and a final synthesized answer from the orchestrator.
print("\n===== as_agent() Transcript =====")
for i, msg in enumerate(agent_result.messages, start=1):
role_value = getattr(msg.role, "value", msg.role)
speaker = msg.author_name or role_value
print(f"{'-' * 50}\n{i:02d} [{speaker}]\n{msg.text}")
except Exception as e:
print(f"Workflow execution failed: {e}")
if __name__ == "__main__":
asyncio.run(main())
@@ -2,6 +2,7 @@
import asyncio
import logging
import os
from typing import cast
from agent_framework import (
@@ -10,7 +11,7 @@ from agent_framework import (
Message,
resolve_agent_id,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import HandoffBuilder
from azure.identity import AzureCliCredential
@@ -27,8 +28,9 @@ Routing Pattern:
User -> Coordinator -> Specialist (iterates N times) -> Handoff -> Final Output
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- `az login` (Azure CLI authentication)
- Environment variables for AzureOpenAIChatClient (AZURE_OPENAI_ENDPOINT, etc.)
- Environment variables for AzureOpenAIResponsesClient (AZURE_AI_MODEL_DEPLOYMENT_NAME)
Key Concepts:
- Autonomous interaction mode: agents iterate until they handoff
@@ -37,7 +39,7 @@ Key Concepts:
def create_agents(
client: AzureOpenAIChatClient,
client: AzureOpenAIResponsesClient,
) -> tuple[Agent, Agent, Agent]:
"""Create coordinator and specialists for autonomous iteration."""
coordinator = client.as_agent(
@@ -73,7 +75,11 @@ def create_agents(
async def main() -> None:
"""Run an autonomous handoff workflow with specialist iteration enabled."""
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
coordinator, research_agent, summary_agent = create_agents(client)
# Build the workflow with autonomous mode
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Annotated, cast
from agent_framework import (
@@ -11,7 +12,7 @@ from agent_framework import (
WorkflowRunState,
tool,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
@@ -21,8 +22,9 @@ A handoff workflow defines a pattern that assembles agents in a mesh topology, a
them to transfer control to each other based on the conversation context.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- `az login` (Azure CLI authentication)
- Environment variables configured for AzureOpenAIChatClient (AZURE_OPENAI_ENDPOINT, etc.)
- Environment variables configured for AzureOpenAIResponsesClient (AZURE_AI_MODEL_DEPLOYMENT_NAME)
Key Concepts:
- Auto-registered handoff tools: HandoffBuilder automatically creates handoff tools
@@ -54,11 +56,11 @@ def process_return(order_number: Annotated[str, "Order number to process return
return f"Return initiated successfully for order {order_number}. You will receive return instructions via email."
def create_agents(client: AzureOpenAIChatClient) -> tuple[Agent, Agent, Agent, Agent]:
def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Agent, Agent]:
"""Create and configure the triage and specialist agents.
Args:
client: The AzureOpenAIChatClient to use for creating agents.
client: The AzureOpenAIResponsesClient to use for creating agents.
Returns:
Tuple of (triage_agent, refund_agent, order_agent, return_agent)
@@ -189,7 +191,11 @@ async def main() -> None:
replace the scripted_responses with actual user input collection.
"""
# Initialize the Azure OpenAI chat client
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create all agents: triage + specialists
triage, refund, order, support = create_agents(client)
@@ -0,0 +1,186 @@
# Copyright (c) Microsoft. All rights reserved.
"""
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 workflow events.
Verifies GitHub issue #2718: files generated by code interpreter in
HandoffBuilder workflows can be properly retrieved.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- `az login` (Azure CLI authentication)
- AZURE_AI_MODEL_DEPLOYMENT_NAME
"""
import asyncio
import os
from collections.abc import AsyncIterable
from typing import cast
from agent_framework import (
AgentResponseUpdate,
Message,
WorkflowEvent,
WorkflowRunState,
)
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
async def _drain(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:
"""Collect all events from an async stream."""
return [event async for event in stream]
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[WorkflowEvent[HandoffAgentUserRequest]] = []
file_ids: list[str] = []
for event in events:
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,
}:
print(f"[status] {event.state}")
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):
for content in data.contents:
if content.type == "hosted_file":
file_ids.append(content.file_id) # type: ignore
print(f"[Found HostedFileContent: file_id={content.file_id}]")
elif content.type == "text" and content.annotations:
for annotation in content.annotations:
file_id = annotation["file_id"] # type: ignore
file_ids.append(file_id)
print(f"[Found file annotation: file_id={file_id}]")
elif isinstance(data, list):
conversation = cast(list[Message], data)
if isinstance(conversation, list):
print("\n=== Final Conversation Snapshot ===")
for message in conversation:
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text or [content.type for content in message.contents]}")
print("===================================")
return requests, file_ids
async def main() -> None:
"""Run a simple handoff workflow with code interpreter file generation."""
print("=== Handoff Workflow with Code Interpreter File Generation ===\n")
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
triage = client.as_agent(
name="triage_agent",
instructions=(
"You are a triage agent. Route code-related requests to the code_specialist. "
"When the user asks to create or generate files, hand off to code_specialist "
"by calling handoff_to_code_specialist."
),
)
code_interpreter_tool = client.get_code_interpreter_tool()
code_specialist = client.as_agent(
name="code_specialist",
instructions=(
"You are a Python code specialist. Use the code interpreter to execute Python code "
"and create files when requested. Always save files to /mnt/data/ directory."
),
tools=[code_interpreter_tool],
)
workflow = (
HandoffBuilder(
termination_condition=lambda conv: sum(1 for msg in conv if msg.role == "user") >= 2,
)
.participants([triage, code_specialist])
.with_start_agent(triage)
.build()
)
user_inputs = [
"Please create a text file called hello.txt with 'Hello from handoff workflow!' inside it.",
"exit",
]
input_index = 0
all_file_ids: list[str] = []
print(f"User: {user_inputs[0]}")
events = await _drain(workflow.run(user_inputs[0], stream=True))
requests, file_ids = _handle_events(events)
all_file_ids.extend(file_ids)
input_index += 1
while requests:
request = requests[0]
if input_index >= len(user_inputs):
break
user_input = user_inputs[input_index]
print(f"\nUser: {user_input}")
responses = {request.request_id: HandoffAgentUserRequest.create_response(user_input)}
events = await _drain(workflow.run(stream=True, responses=responses))
requests, file_ids = _handle_events(events)
all_file_ids.extend(file_ids)
input_index += 1
print("\n" + "=" * 50)
if all_file_ids:
print(f"SUCCESS: Found {len(all_file_ids)} file ID(s) in handoff workflow:")
for fid in all_file_ids:
print(f" - {fid}")
else:
print("WARNING: No file IDs captured from the handoff workflow.")
print("=" * 50)
"""
Sample Output:
User: Please create a text file called hello.txt with 'Hello from handoff workflow!' inside it.
[Found HostedFileContent: file_id=assistant-JT1sA...]
=== Conversation So Far ===
- user: Please create a text file called hello.txt with 'Hello from handoff workflow!' inside it.
- triage_agent: I am handing off your request to create the text file "hello.txt" with the specified content to the code specialist. They will assist you shortly.
- code_specialist: The file "hello.txt" has been created with the content "Hello from handoff workflow!". You can download it using the link below:
[hello.txt](sandbox:/mnt/data/hello.txt)
===========================
[status] IDLE_WITH_PENDING_REQUESTS
User: exit
[status] IDLE
==================================================
SUCCESS: Found 1 file ID(s) in handoff workflow:
- assistant-JT1sA...
==================================================
""" # noqa: E501
if __name__ == "__main__":
asyncio.run(main())
@@ -2,6 +2,7 @@
import asyncio
import json
import os
from pathlib import Path
from typing import Any
@@ -12,7 +13,7 @@ from agent_framework import (
Workflow,
tool,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
@@ -39,8 +40,9 @@ Pattern:
workflow.run(stream=True, checkpoint_id=..., responses=responses).)
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure CLI authentication (az login).
- Environment variables configured for AzureOpenAIChatClient.
- Environment variables configured for AzureOpenAIResponsesClient.
"""
CHECKPOINT_DIR = Path(__file__).parent / "tmp" / "handoff_checkpoints"
@@ -53,7 +55,7 @@ def submit_refund(refund_description: str, amount: str, order_id: str) -> str:
return f"refund recorded for order {order_id} (amount: {amount}) with details: {refund_description}"
def create_agents(client: AzureOpenAIChatClient) -> tuple[Agent, Agent, Agent]:
def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Agent]:
"""Create a simple handoff scenario: triage, refund, and order specialists."""
triage = client.as_agent(
@@ -90,7 +92,11 @@ def create_agents(client: AzureOpenAIChatClient) -> tuple[Agent, Agent, Agent]:
def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow:
"""Build the handoff workflow with checkpointing enabled."""
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
triage, refund, order = create_agents(client)
# checkpoint_storage: Enable checkpointing for resume
@@ -0,0 +1,227 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Annotated
from agent_framework import (
Agent,
AgentResponse,
Content,
Message,
WorkflowAgent,
tool,
)
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
"""Sample: Handoff Workflow as Agent with Human-in-the-Loop.
This sample demonstrates how to use a handoff workflow as an agent, enabling
human-in-the-loop interactions through the agent interface.
A handoff workflow defines a pattern that assembles agents in a mesh topology, allowing
them to transfer control to each other based on the conversation context.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- `az login` (Azure CLI authentication)
- Environment variables configured for AzureOpenAIResponsesClient (AZURE_AI_MODEL_DEPLOYMENT_NAME)
Key Concepts:
- Auto-registered handoff tools: HandoffBuilder automatically creates handoff tools
for each participant, allowing the coordinator to transfer control to specialists
- Termination condition: Controls when the workflow stops requesting user input
- Request/response cycle: Workflow requests input, user responds, cycle continues
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# See:
# samples/getting_started/tools/function_tool_with_approval.py
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str:
"""Simulated function to process a refund for a given order number."""
return f"Refund processed successfully for order {order_number}."
@tool(approval_mode="never_require")
def check_order_status(order_number: Annotated[str, "Order number to check status for"]) -> str:
"""Simulated function to check the status of a given order number."""
return f"Order {order_number} is currently being processed and will ship in 2 business days."
@tool(approval_mode="never_require")
def process_return(order_number: Annotated[str, "Order number to process return for"]) -> str:
"""Simulated function to process a return for a given order number."""
return f"Return initiated successfully for order {order_number}. You will receive return instructions via email."
def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Agent, Agent]:
"""Create and configure the triage and specialist agents.
Args:
client: The AzureOpenAIResponsesClient to use for creating agents.
Returns:
Tuple of (triage_agent, refund_agent, order_agent, return_agent)
"""
# Triage agent: Acts as the frontline dispatcher
triage_agent = client.as_agent(
instructions=(
"You are frontline support triage. Route customer issues to the appropriate specialist agents "
"based on the problem described."
),
name="triage_agent",
)
# Refund specialist: Handles refund requests
refund_agent = client.as_agent(
instructions="You process refund requests.",
name="refund_agent",
# In a real application, an agent can have multiple tools; here we keep it simple
tools=[process_refund],
)
# Order/shipping specialist: Resolves delivery issues
order_agent = client.as_agent(
instructions="You handle order and shipping inquiries.",
name="order_agent",
# In a real application, an agent can have multiple tools; here we keep it simple
tools=[check_order_status],
)
# Return specialist: Handles return requests
return_agent = client.as_agent(
instructions="You manage product return requests.",
name="return_agent",
# In a real application, an agent can have multiple tools; here we keep it simple
tools=[process_return],
)
return triage_agent, refund_agent, order_agent, return_agent
def handle_response_and_requests(response: AgentResponse) -> dict[str, HandoffAgentUserRequest]:
"""Process agent response messages and extract any user requests.
This function inspects the agent response and:
- Displays agent messages to the console
- Collects HandoffAgentUserRequest instances for response handling
Args:
response: The AgentResponse from the agent run call.
Returns:
A dictionary mapping request IDs to HandoffAgentUserRequest instances.
"""
pending_requests: dict[str, HandoffAgentUserRequest] = {}
for message in response.messages:
if message.text:
print(f"- {message.author_name or message.role}: {message.text}")
for content in message.contents:
if content.type == "function_call":
if isinstance(content.arguments, dict):
request = WorkflowAgent.RequestInfoFunctionArgs.from_dict(content.arguments)
elif isinstance(content.arguments, str):
request = WorkflowAgent.RequestInfoFunctionArgs.from_json(content.arguments)
else:
raise ValueError("Invalid arguments type. Expecting a request info structure for this sample.")
if isinstance(request.data, HandoffAgentUserRequest):
pending_requests[request.request_id] = request.data
return pending_requests
async def main() -> None:
"""Main entry point for the handoff workflow demo.
This function demonstrates:
1. Creating triage and specialist agents
2. Building a handoff workflow with custom termination condition
3. Running the workflow with scripted user responses
4. Processing events and handling user input requests
The workflow uses scripted responses instead of interactive input to make
the demo reproducible and testable. In a production application, you would
replace the scripted_responses with actual user input collection.
"""
# Initialize the Azure OpenAI chat client
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create all agents: triage + specialists
triage, refund, order, support = create_agents(client)
# Build the handoff workflow
# - participants: All agents that can participate in the workflow
# - with_start_agent: The triage agent is designated as the start agent, which means
# it receives all user input first and orchestrates handoffs to specialists
# - termination_condition: Custom logic to stop the request/response loop.
# Without this, the default behavior continues requesting user input until max_turns
# is reached. Here we use a custom condition that checks if the conversation has ended
# naturally (when one of the agents says something like "you're welcome").
agent = (
HandoffBuilder(
name="customer_support_handoff",
participants=[triage, refund, order, support],
# Custom termination: Check if one of the agents has provided a closing message.
# This looks for the last message containing "welcome", which indicates the
# conversation has concluded naturally.
termination_condition=lambda conversation: (
len(conversation) > 0 and "welcome" in conversation[-1].text.lower()
),
)
.with_start_agent(triage)
.build()
.as_agent() # Convert workflow to agent interface
)
# Scripted user responses for reproducible demo
# In a console application, replace this with:
# user_input = input("Your response: ")
# or integrate with a UI/chat interface
scripted_responses = [
"My order 1234 arrived damaged and the packaging was destroyed. I'd like to return it.",
"Please also process a refund for order 1234.",
"Thanks for resolving this.",
]
# Start the workflow with the initial user message
print("[Starting workflow with initial user message...]\n")
initial_message = "Hello, I need assistance with my recent purchase."
print(f"- User: {initial_message}")
response = await agent.run(initial_message)
pending_requests = handle_response_and_requests(response)
# Process the request/response cycle
# The workflow will continue requesting input until:
# 1. The termination condition is met, OR
# 2. We run out of scripted responses
while pending_requests:
if not scripted_responses:
# No more scripted responses; terminate the workflow
responses = {req_id: HandoffAgentUserRequest.terminate() for req_id in pending_requests}
else:
# Get the next scripted response
user_response = scripted_responses.pop(0)
print(f"\n- User: {user_response}")
# Send response(s) to all pending requests
# In this demo, there's typically one request per cycle, but the API supports multiple
responses = {req_id: HandoffAgentUserRequest.create_response(user_response) for req_id in pending_requests}
function_results = [
Content.from_function_result(call_id=req_id, result=response) for req_id, response in responses.items()
]
response = await agent.run(Message("tool", function_results))
pending_requests = handle_response_and_requests(response)
if __name__ == "__main__":
asyncio.run(main())
@@ -1,241 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
"""
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 workflow events.
Verifies GitHub issue #2718: files generated by code interpreter in
HandoffBuilder workflows can be properly retrieved.
Toggle USE_V2_CLIENT to switch between:
- V1: AzureAIAgentClient (azure-ai-agents SDK)
- V2: AzureAIClient (azure-ai-projects 2.x with Responses API)
IMPORTANT: When using V2 AzureAIClient with HandoffBuilder, each agent must
have its own client instance. The V2 client binds to a single server-side
agent name, so sharing a client between agents causes routing issues.
Prerequisites:
- `az login` (Azure CLI authentication)
- V1: AZURE_AI_AGENT_PROJECT_CONNECTION_STRING
- V2: AZURE_AI_PROJECT_ENDPOINT, AZURE_AI_MODEL_DEPLOYMENT_NAME
"""
import asyncio
from collections.abc import AsyncIterable, AsyncIterator
from contextlib import asynccontextmanager
from typing import cast
from agent_framework import (
Agent,
AgentResponseUpdate,
Message,
WorkflowEvent,
WorkflowRunState,
)
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity.aio import AzureCliCredential
# Toggle between V1 (AzureAIAgentClient) and V2 (AzureAIClient)
USE_V2_CLIENT = False
async def _drain(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:
"""Collect all events from an async stream."""
return [event async for event in stream]
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[WorkflowEvent[HandoffAgentUserRequest]] = []
file_ids: list[str] = []
for event in events:
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,
}:
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):
for content in data.contents:
if content.type == "hosted_file":
file_ids.append(content.file_id) # type: ignore
print(f"[Found HostedFileContent: file_id={content.file_id}]")
elif content.type == "text" and content.annotations:
for annotation in content.annotations:
file_id = annotation["file_id"] # type: ignore
file_ids.append(file_id)
print(f"[Found file annotation: file_id={file_id}]")
elif event.type == "output":
conversation = cast(list[Message], event.data)
if isinstance(conversation, list):
print("\n=== Final Conversation Snapshot ===")
for message in conversation:
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text or [content.type for content in message.contents]}")
print("===================================")
return requests, file_ids
@asynccontextmanager
async def create_agents_v1(credential: AzureCliCredential) -> AsyncIterator[tuple[Agent, Agent]]:
"""Create agents using V1 AzureAIAgentClient."""
from agent_framework.azure import AzureAIAgentClient
async with AzureAIAgentClient(credential=credential) as client:
triage = client.as_agent(
name="triage_agent",
instructions=(
"You are a triage agent. Route code-related requests to the code_specialist. "
"When the user asks to create or generate files, hand off to code_specialist "
"by calling handoff_to_code_specialist."
),
)
# Create code interpreter tool using instance method
code_interpreter_tool = client.get_code_interpreter_tool()
code_specialist = client.as_agent(
name="code_specialist",
instructions=(
"You are a Python code specialist. Use the code interpreter to execute Python code "
"and create files when requested. Always save files to /mnt/data/ directory."
),
tools=[code_interpreter_tool],
)
yield triage, code_specialist # type: ignore
@asynccontextmanager
async def create_agents_v2(credential: AzureCliCredential) -> AsyncIterator[tuple[Agent, Agent]]:
"""Create agents using V2 AzureAIClient.
Each agent needs its own client instance because the V2 client binds
to a single server-side agent name.
"""
from agent_framework.azure import AzureAIClient
async with (
AzureAIClient(credential=credential) as triage_client,
AzureAIClient(credential=credential) as code_client,
):
triage = triage_client.as_agent(
name="TriageAgent",
instructions="You are a triage agent. Your ONLY job is to route requests to the appropriate specialist.",
)
# Create code interpreter tool using instance method
code_interpreter_tool = code_client.get_code_interpreter_tool()
code_specialist = code_client.as_agent(
name="CodeSpecialist",
instructions=(
"You are a Python code specialist. You have access to a code interpreter tool. "
"Use the code interpreter to execute Python code and create files. "
"Always save files to /mnt/data/ directory. "
"Do NOT discuss handoffs or routing - just complete the coding task directly."
),
tools=[code_interpreter_tool],
)
yield triage, code_specialist
async def main() -> None:
"""Run a simple handoff workflow with code interpreter file generation."""
client_version = "V2 (AzureAIClient)" if USE_V2_CLIENT else "V1 (AzureAIAgentClient)"
print(f"=== Handoff Workflow with Code Interpreter File Generation [{client_version}] ===\n")
async with AzureCliCredential() as credential:
create_agents = create_agents_v2 if USE_V2_CLIENT else create_agents_v1
async with create_agents(credential) as (triage, code_specialist):
workflow = (
HandoffBuilder(
termination_condition=lambda conv: sum(1 for msg in conv if msg.role == "user") >= 2,
)
.participants([triage, code_specialist])
.with_start_agent(triage)
.build()
)
user_inputs = [
"Please create a text file called hello.txt with 'Hello from handoff workflow!' inside it.",
"exit",
]
input_index = 0
all_file_ids: list[str] = []
print(f"User: {user_inputs[0]}")
events = await _drain(workflow.run(user_inputs[0], stream=True))
requests, file_ids = _handle_events(events)
all_file_ids.extend(file_ids)
input_index += 1
while requests:
request = requests[0]
if input_index >= len(user_inputs):
break
user_input = user_inputs[input_index]
print(f"\nUser: {user_input}")
responses = {request.request_id: HandoffAgentUserRequest.create_response(user_input)}
events = await _drain(workflow.run(stream=True, responses=responses))
requests, file_ids = _handle_events(events)
all_file_ids.extend(file_ids)
input_index += 1
print("\n" + "=" * 50)
if all_file_ids:
print(f"SUCCESS: Found {len(all_file_ids)} file ID(s) in handoff workflow:")
for fid in all_file_ids:
print(f" - {fid}")
else:
print("WARNING: No file IDs captured from the handoff workflow.")
print("=" * 50)
"""
Sample Output:
User: Please create a text file called hello.txt with 'Hello from handoff workflow!' inside it.
[Found HostedFileContent: file_id=assistant-JT1sA...]
=== Conversation So Far ===
- user: Please create a text file called hello.txt with 'Hello from handoff workflow!' inside it.
- triage_agent: I am handing off your request to create the text file "hello.txt" with the specified content to the code specialist. They will assist you shortly.
- code_specialist: The file "hello.txt" has been created with the content "Hello from handoff workflow!". You can download it using the link below:
[hello.txt](sandbox:/mnt/data/hello.txt)
===========================
[status] IDLE_WITH_PENDING_REQUESTS
User: exit
[status] IDLE
==================================================
SUCCESS: Found 1 file ID(s) in handoff workflow:
- assistant-JT1sA...
==================================================
""" # noqa: E501
if __name__ == "__main__":
asyncio.run(main())
@@ -3,6 +3,7 @@
import asyncio
import json
import logging
import os
from typing import cast
from agent_framework import (
@@ -11,8 +12,9 @@ from agent_framework import (
Message,
WorkflowEvent,
)
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import GroupChatRequestSentEvent, MagenticBuilder, MagenticProgressLedger
from azure.identity import AzureCliCredential
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)
@@ -38,7 +40,8 @@ energy efficiency and CO2 emissions of several ML models, streams intermediate
events, and prints the final answer. The workflow completes when idle.
Prerequisites:
- OpenAI credentials configured for `OpenAIChatClient` and `OpenAIResponsesClient`.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- OpenAI credentials configured for `AzureOpenAIResponsesClient` and `AzureOpenAIResponsesClient`.
"""
@@ -50,11 +53,19 @@ async def main() -> None:
"You are a Researcher. You find information without additional computation or quantitative analysis."
),
# This agent requires the gpt-4o-search-preview model to perform web searches.
client=OpenAIChatClient(model_id="gpt-4o-search-preview"),
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
# Create code interpreter tool using instance method
coder_client = OpenAIResponsesClient()
coder_client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
code_interpreter_tool = coder_client.get_code_interpreter_tool()
coder_agent = Agent(
@@ -70,7 +81,11 @@ async def main() -> None:
name="MagenticManager",
description="Orchestrator that coordinates the research and coding workflow",
instructions="You coordinate a team to complete complex tasks efficiently.",
client=OpenAIChatClient(),
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
print("\nBuilding Magentic Workflow...")
@@ -2,6 +2,7 @@
import asyncio
import json
import os
from datetime import datetime
from pathlib import Path
from typing import cast
@@ -14,9 +15,9 @@ from agent_framework import (
WorkflowEvent,
WorkflowRunState,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import MagenticBuilder, MagenticPlanReviewRequest
from azure.identity._credentials import AzureCliCredential
from azure.identity import AzureCliCredential
"""
Sample: Magentic Orchestration + Checkpointing
@@ -34,7 +35,8 @@ Concepts highlighted here:
`responses` mapping so we can inject the stored human reply during restoration.
Prerequisites:
- OpenAI environment variables configured for `OpenAIChatClient`.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured for `AzureOpenAIResponsesClient`.
"""
TASK = (
@@ -57,14 +59,22 @@ def build_workflow(checkpoint_storage: FileCheckpointStorage):
name="ResearcherAgent",
description="Collects background facts and references for the project.",
instructions=("You are the research lead. Gather crisp bullet points the team should know."),
client=AzureOpenAIChatClient(credential=AzureCliCredential()),
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
writer = Agent(
name="WriterAgent",
description="Synthesizes the final brief for stakeholders.",
instructions=("You convert the research notes into a structured brief with milestones and risks."),
client=AzureOpenAIChatClient(credential=AzureCliCredential()),
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
# Create a manager agent for orchestration
@@ -72,7 +82,11 @@ def build_workflow(checkpoint_storage: FileCheckpointStorage):
name="MagenticManager",
description="Orchestrator that coordinates the research and writing workflow",
instructions="You coordinate a team to complete complex tasks efficiently.",
client=AzureOpenAIChatClient(credential=AzureCliCredential()),
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
# The builder wires in the Magentic orchestrator, sets the plan review path, and
@@ -2,6 +2,7 @@
import asyncio
import json
import os
from collections.abc import AsyncIterable
from typing import cast
@@ -11,8 +12,9 @@ from agent_framework import (
Message,
WorkflowEvent,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import MagenticBuilder, MagenticPlanReviewRequest, MagenticPlanReviewResponse
from azure.identity import AzureCliCredential
"""
Sample: Magentic Orchestration with Human Plan Review
@@ -31,7 +33,8 @@ Plan review options:
- revise(feedback): Provide textual feedback to modify the plan
Prerequisites:
- OpenAI credentials configured for `OpenAIChatClient`.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- OpenAI credentials configured for `AzureOpenAIResponsesClient`.
"""
# Keep track of the last response to format output nicely in streaming mode
@@ -96,21 +99,33 @@ async def main() -> None:
name="ResearcherAgent",
description="Specialist in research and information gathering",
instructions="You are a Researcher. You find information and gather facts.",
client=OpenAIChatClient(model_id="gpt-4o"),
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
analyst_agent = Agent(
name="AnalystAgent",
description="Data analyst who processes and summarizes research findings",
instructions="You are an Analyst. You analyze findings and create summaries.",
client=OpenAIChatClient(model_id="gpt-4o"),
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
manager_agent = Agent(
name="MagenticManager",
description="Orchestrator that coordinates the workflow",
instructions="You coordinate a team to complete tasks efficiently.",
client=OpenAIChatClient(model_id="gpt-4o"),
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
print("\nBuilding Magentic Workflow with Human Plan Review...")
@@ -0,0 +1,115 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import (
Agent,
)
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import MagenticBuilder
from azure.identity import AzureCliCredential
"""
Sample: Build a Magentic orchestration and wrap it as an agent.
The script configures a Magentic workflow with streaming callbacks, then invokes the
orchestration through `workflow.as_agent(...)` so the entire Magentic loop can be reused
like any other agent while still emitting callback telemetry.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- OpenAI credentials configured for `AzureOpenAIResponsesClient` and `AzureOpenAIResponsesClient`.
"""
async def main() -> None:
researcher_agent = Agent(
name="ResearcherAgent",
description="Specialist in research and information gathering",
instructions=(
"You are a Researcher. You find information without additional computation or quantitative analysis."
),
# This agent requires the gpt-4o-search-preview model to perform web searches.
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
# Create code interpreter tool using instance method
coder_client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
code_interpreter_tool = coder_client.get_code_interpreter_tool()
coder_agent = Agent(
name="CoderAgent",
description="A helpful assistant that writes and executes code to process and analyze data.",
instructions="You solve questions using code. Please provide detailed analysis and computation process.",
client=coder_client,
tools=code_interpreter_tool,
)
# Create a manager agent for orchestration
manager_agent = Agent(
name="MagenticManager",
description="Orchestrator that coordinates the research and coding workflow",
instructions="You coordinate a team to complete complex tasks efficiently.",
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
print("\nBuilding Magentic Workflow...")
# intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds
# (Intermediate outputs will be emitted as WorkflowOutputEvent events)
workflow = MagenticBuilder(
participants=[researcher_agent, coder_agent],
intermediate_outputs=True,
manager_agent=manager_agent,
max_round_count=10,
max_stall_count=3,
max_reset_count=2,
).build()
task = (
"I am preparing a report on the energy efficiency of different machine learning model architectures. "
"Compare the estimated training and inference energy consumption of ResNet-50, BERT-base, and GPT-2 "
"on standard datasets (e.g., ImageNet for ResNet, GLUE for BERT, WebText for GPT-2). "
"Then, estimate the CO2 emissions associated with each, assuming training on an Azure Standard_NC6s_v3 "
"VM for 24 hours. Provide tables for clarity, and recommend the most energy-efficient model "
"per task type (image classification, text classification, and text generation)."
)
print(f"\nTask: {task}")
print("\nStarting workflow execution...")
try:
# Wrap the workflow as an agent for composition scenarios
print("\nWrapping workflow as an agent and running...")
workflow_agent = workflow.as_agent(name="MagenticWorkflowAgent")
last_response_id: str | None = None
async for update in workflow_agent.run(task, stream=True):
# Fallback for any other events with text
if last_response_id != update.response_id:
if last_response_id is not None:
print() # Newline between different responses
print(f"{update.author_name}: ", end="", flush=True)
last_response_id = update.response_id
else:
print(update.text, end="", flush=True)
except Exception as e:
print(f"Workflow execution failed: {e}")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,10 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import cast
from agent_framework import Message
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
@@ -24,13 +25,18 @@ Note on internal adapters:
You can safely ignore them when focusing on agent progress.
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI access configured for AzureOpenAIResponsesClient (use az login + env vars)
"""
async def main() -> None:
# 1) Create agents
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
writer = client.as_agent(
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
@@ -0,0 +1,160 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from collections.abc import AsyncIterable
from typing import Annotated, cast
from agent_framework import (
Content,
Message,
WorkflowEvent,
tool,
)
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
"""
Sample: Sequential Workflow with Tool Approval Requests
This sample demonstrates how to use SequentialBuilder with tools that require human
approval before execution. The approval flow uses the existing @tool decorator
with approval_mode="always_require" to trigger human-in-the-loop interactions.
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 .
5. Once approved, the tool executes and the agent completes its response.
6. The workflow outputs the final conversation with all messages.
Purpose:
Show how tool call approvals integrate seamlessly with SequentialBuilder without
requiring any additional builder configuration.
Demonstrate:
- Using @tool(approval_mode="always_require") for sensitive operations.
- Handling request_info events with function_approval_request Content in sequential workflows.
- Resuming workflow execution after approval via run(responses=..., stream=True).
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- OpenAI or Azure OpenAI configured with the required environment variables.
- Basic familiarity with SequentialBuilder and streaming workflow events.
"""
# 1. Define tools - one requiring approval, one that doesn't
@tool(approval_mode="always_require")
def execute_database_query(
query: Annotated[str, "The SQL query to execute against the production database"],
) -> str:
"""Execute a SQL query against the production database. Requires human approval."""
# In a real implementation, this would execute the 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.
@tool(approval_mode="never_require")
def get_database_schema() -> str:
"""Get the current database schema. Does not require approval."""
return """
Tables:
- users (id, name, email, created_at)
- orders (id, user_id, total, status, created_at)
- products (id, name, price, stock)
"""
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, Content] | None:
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, Content] = {}
async for event in stream:
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 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[Message], event.data)
for msg in outputs:
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
responses: dict[str, Content] = {}
if requests:
for request_id, request in requests.items():
if request.type == "function_approval_request":
print("\n[APPROVAL REQUIRED]")
print(f" Tool: {request.function_call.name}") # type: ignore
print(f" Arguments: {request.function_call.arguments}") # type: ignore
print(f"Simulating human approval for: {request.function_call.name}") # type: ignore
# Create approval response
responses[request_id] = request.to_function_approval_response(approved=True)
return responses if responses else None
async def main() -> None:
# 2. Create the agent with tools (approval mode is set per-tool via decorator)
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
database_agent = client.as_agent(
name="DatabaseAgent",
instructions=(
"You are a database assistant. You can view the database schema and execute "
"queries. Always check the schema before running queries. Be careful with "
"queries that modify data."
),
tools=[get_database_schema, execute_database_query],
)
# 3. Build a sequential workflow with the agent
workflow = SequentialBuilder(participants=[database_agent]).build()
# 4. Start the workflow with a user task
print("Starting sequential workflow with tool approval...")
print("-" * 60)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run(
"Check the schema and then update all orders with status 'pending' to 'processing'", stream=True
)
pending_responses = await process_event_stream(stream)
while pending_responses is not None:
# Run the workflow until there is no more human feedback to provide,
# in which case this workflow completes.
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await process_event_stream(stream)
"""
Sample Output:
Starting sequential workflow with tool approval...
------------------------------------------------------------
Approval requested for tool: execute_database_query
Arguments: {"query": "UPDATE orders SET status = 'processing' WHERE status = 'pending'"}
Simulating human approval (auto-approving for demo)...
------------------------------------------------------------
Workflow completed. Final conversation:
[user]: Check the schema and then update all orders with status 'pending' to 'processing'
[assistant]: I've checked the schema and executed the update query. The query
"UPDATE orders SET status = 'processing' WHERE status = 'pending'"
was executed successfully, affecting 3 rows.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Any
from agent_framework import (
@@ -10,7 +11,7 @@ from agent_framework import (
WorkflowContext,
handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
@@ -28,7 +29,8 @@ Custom executor contract:
- Emit the updated conversation via ctx.send_message([...])
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI access configured for AzureOpenAIResponsesClient (use az login + env vars)
"""
@@ -58,7 +60,11 @@ class Summarizer(Executor):
async def main() -> None:
# 1) Create a content agent
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
content = client.as_agent(
instructions="Produce a concise paragraph answering the user's request.",
name="content",
@@ -0,0 +1,142 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Sample: Request Info with SequentialBuilder
This sample demonstrates using the `.with_request_info()` method to pause a
SequentialBuilder workflow AFTER each agent runs, allowing external input
(e.g., human feedback) for review and optional iteration.
Purpose:
Show how to use the request info API that pauses after every agent response,
using the standard request_info pattern for consistency.
Demonstrate:
- Configuring request info with `.with_request_info()`
- Handling request_info events with AgentInputRequest data
- Injecting responses back into the workflow via run(responses=..., stream=True)
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables
- Authentication via azure-identity (run az login before executing)
"""
import asyncio
import os
from collections.abc import AsyncIterable
from typing import cast
from agent_framework import (
AgentExecutorResponse,
Message,
WorkflowEvent,
)
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import AgentRequestInfoResponse, SequentialBuilder
from azure.identity import AzureCliCredential
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, AgentRequestInfoResponse] | None:
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, AgentExecutorResponse] = {}
async for event in stream:
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
requests[event.request_id] = event.data
elif event.type == "output":
# The output of the sequential workflow is a list of ChatMessages
print("\n" + "=" * 60)
print("WORKFLOW COMPLETE")
print("=" * 60)
print("Final output:")
outputs = cast(list[Message], event.data)
for message in outputs:
print(f"[{message.author_name or message.role}]: {message.text}")
responses: dict[str, AgentRequestInfoResponse] = {}
if requests:
for request_id, request in requests.items():
# Display agent response and conversation context for review
print("\n" + "-" * 40)
print("REQUEST INFO: INPUT REQUESTED")
print(
f"Agent {request.executor_id} just responded with: '{request.agent_response.text}'. "
"Please provide your feedback."
)
print("-" * 40)
if request.full_conversation:
print("Conversation context:")
recent = (
request.full_conversation[-2:] if len(request.full_conversation) > 2 else request.full_conversation
)
for msg in recent:
name = msg.author_name or msg.role
text = (msg.text or "")[:150]
print(f" [{name}]: {text}...")
print("-" * 40)
# Get feedback on the agent's response (approve or request iteration)
user_input = input("Your guidance (or 'skip' to approve): ") # noqa: ASYNC250
if user_input.lower() == "skip":
user_input = AgentRequestInfoResponse.approve()
else:
user_input = AgentRequestInfoResponse.from_strings([user_input])
responses[request_id] = user_input
return responses if responses else None
async def main() -> None:
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create agents for a sequential document review workflow
drafter = client.as_agent(
name="drafter",
instructions=("You are a document drafter. When given a topic, create a brief draft (2-3 sentences)."),
)
editor = client.as_agent(
name="editor",
instructions=(
"You are an editor. Review the draft and make improvements. "
"Incorporate any human feedback that was provided."
),
)
finalizer = client.as_agent(
name="finalizer",
instructions=(
"You are a finalizer. Take the edited content and create a polished final version. "
"Incorporate any additional feedback provided."
),
)
# Build workflow with request info enabled (pauses after each agent responds)
workflow = (
SequentialBuilder(participants=[drafter, editor, finalizer])
# Only enable request info for the editor agent
.with_request_info(agents=["editor"])
.build()
)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run("Write a brief introduction to artificial intelligence.", stream=True)
pending_responses = await process_event_stream(stream)
while pending_responses is not None:
# Run the workflow until there is no more human feedback to provide,
# in which case this workflow completes.
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await process_event_stream(stream)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,91 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
"""
Sample: Build a sequential workflow orchestration and wrap it as an agent.
The script assembles a sequential conversation flow with `SequentialBuilder`, then
invokes the entire orchestration through the `workflow.as_agent(...)` interface so
other coordinators can reuse the chain as a single participant.
Note on internal adapters:
- Sequential orchestration includes small adapter nodes for input normalization
("input-conversation"), agent-response conversion ("to-conversation:<participant>"),
and completion ("complete"). These may appear as ExecutorInvoke/Completed events in
the stream—similar to how concurrent orchestration includes a dispatcher/aggregator.
You can safely ignore them when focusing on agent progress.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI access configured for AzureOpenAIResponsesClient (use az login + env vars)
"""
async def main() -> None:
# 1) Create agents
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
writer = client.as_agent(
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
name="writer",
)
reviewer = client.as_agent(
instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."),
name="reviewer",
)
# 2) Build sequential workflow: writer -> reviewer
workflow = SequentialBuilder(participants=[writer, reviewer]).build()
# 3) Treat the workflow itself as an agent for follow-up invocations
agent = workflow.as_agent(name="SequentialWorkflowAgent")
prompt = "Write a tagline for a budget-friendly eBike."
agent_response = await agent.run(prompt)
if agent_response.messages:
print("\n===== Conversation =====")
for i, msg in enumerate(agent_response.messages, start=1):
name = msg.author_name or msg.role
print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}")
"""
Sample Output:
===== Final Conversation =====
------------------------------------------------------------
01 [user]
Write a tagline for a budget-friendly eBike.
------------------------------------------------------------
02 [writer]
Ride farther, spend less—your affordable eBike adventure starts here.
------------------------------------------------------------
03 [reviewer]
This tagline clearly communicates affordability and the benefit of extended travel, making it
appealing to budget-conscious consumers. It has a friendly and motivating tone, though it could
be slightly shorter for more punch. Overall, a strong and effective suggestion!
===== as_agent() Conversation =====
------------------------------------------------------------
01 [writer]
Go electric, save big—your affordable ride awaits!
------------------------------------------------------------
02 [reviewer]
Catchy and straightforward! The tagline clearly emphasizes both the electric aspect and the affordability of the
eBike. It's inviting and actionable. For even more impact, consider making it slightly shorter:
"Go electric, save big." Overall, this is an effective and appealing suggestion for a budget-friendly eBike.
"""
if __name__ == "__main__":
asyncio.run(main())