chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,145 @@
# Orchestration Getting Started Samples
## Installation
The orchestrations package is included when you install `agent-framework` (which pulls in all optional packages):
```bash
pip install agent-framework
```
Or install the orchestrations package directly:
```bash
pip install agent-framework-orchestrations
```
Orchestration builders are available via the `agent_framework.orchestrations` submodule:
```python
from agent_framework.orchestrations import (
SequentialBuilder,
ConcurrentBuilder,
HandoffBuilder,
GroupChatBuilder,
MagenticBuilder,
)
```
## Samples Overview (by directory)
### concurrent
| 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 |
| Concurrent Orchestration as Agent | [concurrent_workflow_as_agent.py](../agents/concurrent_workflow_as_agent.py) | Build a ConcurrentBuilder workflow and expose it as an agent via `workflow.as_agent(...)` |
| Tool Approval with ConcurrentBuilder | [concurrent_builder_tool_approval.py](../tool-approval/concurrent_builder_tool_approval.py) | Require human approval for sensitive tools across concurrent participants |
| ConcurrentBuilder Request Info | [concurrent_request_info.py](../human-in-the-loop/concurrent_request_info.py) | Review concurrent agent outputs before aggregation using `.with_request_info()` |
### sequential
| Sample | File | Concepts |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| 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 |
| Sequential Orchestration as Agent | [sequential_workflow_as_agent.py](../agents/sequential_workflow_as_agent.py) | Build a SequentialBuilder workflow and expose it as an agent via `workflow.as_agent(...)` |
| Tool Approval with SequentialBuilder | [sequential_builder_tool_approval.py](../tool-approval/sequential_builder_tool_approval.py) | Require human approval for sensitive tools in SequentialBuilder workflows |
| SequentialBuilder Request Info | [sequential_request_info.py](../human-in-the-loop/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_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 Selector | [group_chat_simple_selector.py](./group_chat_simple_selector.py) | Group chat with a simple function selector for next speaker |
| Group Chat Orchestration as Agent | [group_chat_workflow_as_agent.py](../agents/group_chat_workflow_as_agent.py) | Build a GroupChatBuilder workflow and wrap it as an agent for composition |
| Tool Approval with GroupChatBuilder | [group_chat_builder_tool_approval.py](../tool-approval/group_chat_builder_tool_approval.py) | Require human approval for sensitive tools in group chat orchestration |
| GroupChatBuilder Request Info | [group_chat_request_info.py](../human-in-the-loop/group_chat_request_info.py) | Steer group discussions with periodic guidance using `.with_request_info()` |
### handoff
| Sample | File | Concepts |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| 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 |
| Handoff with Tool Approval + Checkpoint | [handoff_with_tool_approval_checkpoint_resume.py](./handoff_with_tool_approval_checkpoint_resume.py) | Capture tool-approval decisions in checkpoints and resume from persisted state |
| Handoff Orchestration as Agent | [handoff_workflow_as_agent.py](../agents/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.py](./magentic.py) | Orchestrate multiple agents with a Magentic manager and streaming |
| Magentic + Human Plan Review | [magentic_human_plan_review.py](./magentic_human_plan_review.py) | Human reviews or updates the plan before execution |
| Magentic + Checkpoint Resume | [magentic_checkpoint.py](./magentic_checkpoint.py) | Resume Magentic orchestration from saved checkpoints |
| Magentic Orchestration as Agent | [magentic_workflow_as_agent.py](../agents/magentic_workflow_as_agent.py) | Build a MagenticBuilder workflow and reuse it as an agent |
## Tips
**Participant output selection**: Orchestration builders use participant-oriented names for Workflow Output selection.
Use `output_from=[...]` when participant responses should be Workflow Output (`type='output'` events), and
`intermediate_output_from=[...]` when participant responses should be Intermediate Output (`type='intermediate'`
events). `output_from` is an allow-list for Workflow Output, not a routing rule for every other participant output.
Unselected participant responses are hidden unless `intermediate_output_from` selects them.
| Selection | Workflow Output | Intermediate Output | Hidden payloads |
| --- | --- | --- | --- |
| Omit both selections | Builder default Workflow Output contract | None | Builder-specific non-output participant payloads |
| `output_from="all"` | Every output-capable participant | None | None |
| `output_from=[writer]` | Only `writer` | None | All other participant payloads |
| `output_from=[writer], intermediate_output_from="all_other"` | Only `writer` | Every output-capable participant not selected by `output_from` | None |
| `intermediate_output_from="all_other"` | None, except builder-internal default output executors where applicable | Every output-capable participant | Builder-internal plumbing payloads |
| `output_from=[], intermediate_output_from="all_other"` | None, except builder-internal default output executors where applicable | Every output-capable participant | Builder-internal plumbing payloads |
| `output_from=[writer], intermediate_output_from=[researcher, reviewer]` | Only `writer` | `researcher` and `reviewer` | Any other participant payloads |
Invalid selections fail at construction or build time:
| Invalid selection | Why it fails |
| --- | --- |
| `output_from="all_other"` | `"all_other"` is only valid for `intermediate_output_from` |
| `intermediate_output_from="all"` | `"all"` is only valid for `output_from` |
| The same participant in both selections | One payload cannot be both Workflow Output and Intermediate Output |
| Duplicate participant selections | Duplicates are treated as configuration errors |
| Unknown participant selections | Typos and missing participants are rejected |
| `output_from=[], intermediate_output_from=[]` | Both explicit selections are empty |
By default, Sequential keeps the last participant as Workflow Output. Concurrent, GroupChat, and Magentic keep their
synthetic aggregator/orchestrator/manager executors as Workflow Output, while participant responses stay hidden unless
selected. Handoff keeps participants as Workflow Output by default.
When an orchestration workflow is exposed via `workflow.as_agent()`, Workflow Output becomes normal text content in
the `AgentResponse`; Intermediate Output becomes `text_reasoning` content. This preserves `.text` while making
selected progress available for callers that inspect message contents.
**Magentic checkpointing tip**: Treat `MagenticBuilder.participants` keys as stable identifiers. When resuming from a checkpoint, the rebuilt workflow must reuse the same participant names; otherwise the checkpoint cannot be applied and the run will fail fast.
**Handoff workflow tip**: Handoff workflows maintain the full conversation history including any `Message.additional_properties` emitted by your agents. This ensures routing metadata remains intact across all agent transitions. For specialist-to-specialist handoffs, use `.add_handoff(source, targets)` to configure which agents can route to which others with a fluent, type-safe API.
**Handoff `require_per_service_call_history_persistence`**: All agents in a handoff workflow **must** set `require_per_service_call_history_persistence=True`. `HandoffBuilder.build()` will raise a `ValueError` if any participant is missing this flag. This is required because handoff middleware short-circuits tool calls via `MiddlewareTermination`, and without per-service-call history persistence, local history would store tool results the service never received, causing mismatches on subsequent turns.
**Sequential orchestration note**: Sequential orchestration uses a few small adapter nodes for plumbing:
- `input-conversation` normalizes input to `list[Message]`
- `to-conversation:<participant>` converts agent responses into the shared conversation
- `complete` publishes the Workflow Output event (`type='output'`)
These may appear in event streams (executor_invoked/executor_completed). They're analogous to concurrent's dispatcher and aggregator and can be ignored if you only care about agent activity.
## Why FoundryChatClient?
Orchestration samples use `FoundryChatClient` because they create agents locally and do not require
server-side lifecycle management. `FoundryChatClient` is a lightweight, project-backed client that fits
patterns like Sequential, Concurrent, Handoff, GroupChat, and Magentic.
## Environment Variables
Orchestration samples that use `FoundryChatClient` expect:
- `FOUNDRY_PROJECT_ENDPOINT` (Azure AI Foundry Agent Service (V2) project endpoint)
- `FOUNDRY_MODEL` (model deployment name)
These values are passed directly into the client constructor via `os.getenv()` in sample code.
@@ -0,0 +1,144 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Any
from agent_framework import Agent, Message
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: Concurrent fan-out/fan-in (agent-only API) with default aggregator
Build a high-level concurrent workflow using ConcurrentBuilder and three domain agents.
The default dispatcher fans out the same user prompt to all agents in parallel.
The default aggregator fans in their results and yields output containing
a list[Message] representing the concatenated conversations from all agents.
Demonstrates:
- Minimal wiring with ConcurrentBuilder(participants=[...]).build()
- Fan-out to multiple agents, fan-in aggregation of final ChatMessages
- Workflow completion when idle with no pending work
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Familiarity with Workflow events (WorkflowEvent)
"""
async def main() -> None:
# 1) Create three domain agents using FoundryChatClient
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
researcher = Agent(
client=client,
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
" opportunities, and risks."
),
name="researcher",
)
marketer = Agent(
client=client,
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
" aligned to the prompt."
),
name="marketer",
)
legal = Agent(
client=client,
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
# Participants are either Agents (type of SupportsAgentRun) or Executors
workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build()
# 3) Run with a single prompt and pretty-print the final combined messages
events = await workflow.run("We are launching a new budget-friendly electric bike for urban commuters.")
outputs = events.get_outputs()
if outputs:
print("===== Final Aggregated Conversation (messages) =====")
for output in outputs:
messages: list[Message] | Any = output
for i, msg in enumerate(messages, start=1):
name = msg.author_name if msg.author_name else "user"
print(f"{'-' * 60}\n\n{i:02d} [{name}]:\n{msg.text}")
"""
Sample Output:
===== Final Aggregated Conversation (messages) =====
------------------------------------------------------------
01 [user]:
We are launching a new budget-friendly electric bike for urban commuters.
------------------------------------------------------------
02 [researcher]:
**Insights:**
- **Target Demographic:** Urban commuters seeking affordable, eco-friendly transport;
likely to include students, young professionals, and price-sensitive urban residents.
- **Market Trends:** E-bike sales are growing globally, with increasing urbanization,
higher fuel costs, and sustainability concerns driving adoption.
- **Competitive Landscape:** Key competitors include brands like Rad Power Bikes, Aventon,
Lectric, and domestic budget-focused manufacturers in North America, Europe, and Asia.
- **Feature Expectations:** Customers expect reliability, ease-of-use, theft protection,
lightweight design, sufficient battery range for daily city commutes (typically 25-40 miles),
and low-maintenance components.
**Opportunities:**
- **First-time Buyers:** Capture newcomers to e-biking by emphasizing affordability, ease of
operation, and cost savings vs. public transit/car ownership.
...
------------------------------------------------------------
03 [marketer]:
**Value Proposition:**
"Empowering your city commute: Our new electric bike combines affordability, reliability, and
sustainable design—helping you conquer urban journeys without breaking the bank."
**Target Messaging:**
*For Young Professionals:*
...
------------------------------------------------------------
04 [legal]:
**Constraints, Disclaimers, & Policy Concerns for Launching a Budget-Friendly Electric Bike for Urban Commuters:**
**1. Regulatory Compliance**
- Verify that the electric bike meets all applicable federal, state, and local regulations
regarding e-bike classification, speed limits, power output, and safety features.
- Ensure necessary certifications (e.g., UL certification for batteries, CE markings if sold internationally) are obtained.
**2. Product Safety**
- Include consumer safety warnings regarding use, battery handling, charging protocols, and age restrictions.
...
""" # noqa: E501
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,188 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Any
from agent_framework import (
Agent,
AgentExecutorRequest,
AgentExecutorResponse,
Executor,
Message,
WorkflowContext,
handler,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: Concurrent Orchestration with Custom Agent Executors
This sample shows a concurrent fan-out/fan-in pattern using child Executor classes
that each own their Agent. The executors accept AgentExecutorRequest inputs
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 FoundryChatClient)
- 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:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
class ResearcherExec(Executor):
agent: Agent
def __init__(self, client: FoundryChatClient, id: str = "researcher"):
self.agent = Agent(
client=client,
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
" opportunities, and risks."
),
name=id,
)
super().__init__(id=id)
@handler
async def run(self, request: AgentExecutorRequest, ctx: WorkflowContext[AgentExecutorResponse]) -> None:
response = await self.agent.run(request.messages)
full_conversation = list(request.messages) + list(response.messages)
await ctx.send_message(AgentExecutorResponse(self.id, response, full_conversation=full_conversation))
class MarketerExec(Executor):
agent: Agent
def __init__(self, client: FoundryChatClient, id: str = "marketer"):
self.agent = Agent(
client=client,
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
" aligned to the prompt."
),
name=id,
)
super().__init__(id=id)
@handler
async def run(self, request: AgentExecutorRequest, ctx: WorkflowContext[AgentExecutorResponse]) -> None:
response = await self.agent.run(request.messages)
full_conversation = list(request.messages) + list(response.messages)
await ctx.send_message(AgentExecutorResponse(self.id, response, full_conversation=full_conversation))
class LegalExec(Executor):
agent: Agent
def __init__(self, client: FoundryChatClient, id: str = "legal"):
self.agent = Agent(
client=client,
instructions=(
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
" based on the prompt."
),
name=id,
)
super().__init__(id=id)
@handler
async def run(self, request: AgentExecutorRequest, ctx: WorkflowContext[AgentExecutorResponse]) -> None:
response = await self.agent.run(request.messages)
full_conversation = list(request.messages) + list(response.messages)
await ctx.send_message(AgentExecutorResponse(self.id, response, full_conversation=full_conversation))
async def main() -> None:
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
researcher = ResearcherExec(client)
marketer = MarketerExec(client)
legal = LegalExec(client)
workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build()
events = await workflow.run("We are launching a new budget-friendly electric bike for urban commuters.")
outputs = events.get_outputs()
if outputs:
print("===== Final Aggregated Conversation (messages) =====")
messages: list[Message] | Any = outputs[0] # Get the first (and typically only) output
for i, msg in enumerate(messages, start=1):
name = msg.author_name if msg.author_name else "user"
print(f"{'-' * 60}\n\n{i:02d} [{name}]:\n{msg.text}")
"""
Sample Output:
===== Final Aggregated Conversation (messages) =====
------------------------------------------------------------
01 [user]:
We are launching a new budget-friendly electric bike for urban commuters.
------------------------------------------------------------
02 [researcher]:
**Insights:**
- **Target Demographic:** Urban commuters seeking affordable, eco-friendly transport;
likely to include students, young professionals, and price-sensitive urban residents.
- **Market Trends:** E-bike sales are growing globally, with increasing urbanization,
higher fuel costs, and sustainability concerns driving adoption.
- **Competitive Landscape:** Key competitors include brands like Rad Power Bikes, Aventon,
Lectric, and domestic budget-focused manufacturers in North America, Europe, and Asia.
- **Feature Expectations:** Customers expect reliability, ease-of-use, theft protection,
lightweight design, sufficient battery range for daily city commutes (typically 25-40 miles),
and low-maintenance components.
**Opportunities:**
- **First-time Buyers:** Capture newcomers to e-biking by emphasizing affordability, ease of
operation, and cost savings vs. public transit/car ownership.
...
------------------------------------------------------------
03 [marketer]:
**Value Proposition:**
"Empowering your city commute: Our new electric bike combines affordability, reliability, and
sustainable design—helping you conquer urban journeys without breaking the bank."
**Target Messaging:**
*For Young Professionals:*
...
------------------------------------------------------------
04 [legal]:
**Constraints, Disclaimers, & Policy Concerns for Launching a Budget-Friendly Electric Bike for Urban Commuters:**
**1. Regulatory Compliance**
- Verify that the electric bike meets all applicable federal, state, and local regulations
regarding e-bike classification, speed limits, power output, and safety features.
- Ensure necessary certifications (e.g., UL certification for batteries, CE markings if sold internationally) are obtained.
**2. Product Safety**
- Include consumer safety warnings regarding use, battery handling, charging protocols, and age restrictions.
...
""" # noqa: E501
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,138 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Any
from agent_framework import Agent, Message
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
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 FoundryChatClient.get_response()
to synthesize a concise, consolidated summary from the experts' outputs.
The workflow completes when all participants become idle.
Demonstrates:
- ConcurrentBuilder(participants=[...]).with_aggregator(callback)
- Fan-out to agents and fan-in at an aggregator
- Aggregation implemented via an LLM call (client.get_response)
- Workflow output yielded with the synthesized summary string
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
async def main() -> None:
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
researcher = Agent(
client=client,
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
" opportunities, and risks."
),
name="researcher",
)
marketer = Agent(
client=client,
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
" aligned to the prompt."
),
name="marketer",
)
legal = Agent(
client=client,
instructions=(
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
" based on the prompt."
),
name="legal",
)
# Define a custom aggregator callback that uses the chat client to summarize
async def summarize_results(results: list[Any]) -> str:
# Extract one final assistant message per agent
expert_sections: list[str] = []
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', 'expert')}:\n{final_text}")
except Exception as e:
expert_sections.append(f"{getattr(r, 'executor_id', 'expert')}: (error: {type(e).__name__}: {e})")
# Ask the model to synthesize a concise summary of the experts' outputs
system_msg = Message(
"system",
contents=[
(
"You are a helpful assistant that consolidates multiple domain expert outputs "
"into one cohesive, concise summary with clear takeaways. Keep it under 200 words."
)
],
)
user_msg = Message("user", contents=["\n\n".join(expert_sections)])
response = await client.get_response([system_msg, user_msg])
# Return the model's final assistant text as the completion result
return response.messages[-1].text if response.messages else ""
# Build with a custom aggregator callback function
# - participants([...]) accepts SupportsAgentRun (agents) or Executor instances.
# Each participant becomes a parallel branch (fan-out) from an internal dispatcher.
# - with_aggregator(...) overrides the default aggregator:
# • 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()
events = await workflow.run("We are launching a new budget-friendly electric bike for urban commuters.")
outputs = events.get_outputs()
if outputs:
print("===== Final Consolidated Output =====")
print(outputs[0]) # Get the first (and typically only) output
"""
Sample Output:
===== Final Consolidated Output =====
Urban e-bike demand is rising rapidly due to eco-awareness, urban congestion, and high fuel costs,
with market growth projected at a ~10% CAGR through 2030. Key customer concerns are affordability,
easy maintenance, convenient charging, compact design, and theft protection. Differentiation opportunities
include integrating smart features (GPS, app connectivity), offering subscription or leasing options, and
developing portable, space-saving designs. Partnering with local governments and bike shops can boost visibility.
Risks include price wars eroding margins, regulatory hurdles, battery quality concerns, and heightened expectations
for after-sales support. Accurate, substantiated product claims and transparent marketing (with range disclaimers)
are essential. All e-bikes must comply with local and federal regulations on speed, wattage, safety certification,
and labeling. Clear warranty, safety instructions (especially regarding batteries), and inclusive, accessible
marketing are required. For connected features, data privacy policies and user consents are mandatory.
Effective messaging should target young professionals, students, eco-conscious commuters, and first-time buyers,
emphasizing affordability, convenience, and sustainability. Slogan suggestion: “Charge Ahead—City Commutes Made
Affordable.” Legal review in each target market, compliance vetting, and robust customer support policies are
critical before launch.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,126 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import cast
from agent_framework import (
Agent,
AgentResponseUpdate,
Message,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import GroupChatBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: Group Chat with Agent-Based Manager
What it does:
- Demonstrates the new set_manager() API for agent-based coordination
- Manager is a full Agent with access to tools, context, and observability
- Coordinates a researcher and writer agent to solve tasks collaboratively
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
ORCHESTRATOR_AGENT_INSTRUCTIONS = """
You coordinate a team conversation to solve the user's task.
Guidelines:
- Start with Researcher to gather information
- Then have Writer synthesize the final answer
- Only finish after both have contributed meaningfully
"""
async def main() -> None:
# Create a Responses client using Azure OpenAI and Azure CLI credentials for all agents
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# Orchestrator agent that manages the conversation
# Note: This agent (and the underlying chat client) must support structured outputs.
# The group chat workflow relies on this to parse the orchestrator's decisions.
# `response_format` is set internally by the GroupChat workflow when the agent is invoked.
orchestrator_agent = Agent(
name="Orchestrator",
description="Coordinates multi-agent collaboration by selecting speakers",
instructions=ORCHESTRATOR_AGENT_INSTRUCTIONS,
client=client,
)
# Participant agents
researcher = Agent(
name="Researcher",
description="Collects relevant background information",
instructions="Gather concise facts that help a teammate answer the question.",
client=client,
)
writer = Agent(
name="Writer",
description="Synthesizes polished answers from gathered information",
instructions="Compose clear and structured answers using any notes provided.",
client=client,
)
# Build the group chat workflow
# termination_condition: stop after 4 assistant messages
# (The agent orchestrator will intelligently decide when to end before this limit but just in case)
# Mark participant responses as intermediate so the stream shows the
# conversation as it unfolds while the orchestrator's transcript remains the
# terminal workflow output.
workflow = (
GroupChatBuilder(
participants=[researcher, writer],
termination_condition=lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 4,
intermediate_output_from=[researcher, writer],
orchestrator_agent=orchestrator_agent,
)
# Set a hard termination condition: stop after 4 assistant messages
# The agent orchestrator will intelligently decide when to end before this limit but just in case
.with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 4)
.build()
)
task = "What are the key benefits of using async/await in Python? Provide a concise summary."
print("\nStarting Group Chat with Agent-Based Manager...\n")
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
async for event in workflow.run(task, stream=True):
if event.type in ("intermediate", "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 __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,378 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
import os
from typing import cast
from agent_framework import (
Agent,
AgentResponseUpdate,
Message,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import GroupChatBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
logging.basicConfig(level=logging.WARNING)
"""
Sample: Philosophical Debate with Agent-Based Manager
What it does:
- Creates a diverse group of agents representing different global perspectives
- Uses an agent-based manager to guide a philosophical discussion
- Demonstrates longer, multi-round discourse with natural conversation flow
- Manager decides when discussion has reached meaningful conclusion
Topic: "What does a good life mean to you personally?"
Participants represent:
- Farmer from Southeast Asia (tradition, sustainability, land connection)
- Software Developer from United States (innovation, technology, work-life balance)
- History Teacher from Eastern Europe (legacy, learning, cultural continuity)
- Activist from South America (social justice, environmental rights)
- Spiritual Leader from Middle East (morality, community service)
- Artist from Africa (creative expression, storytelling)
- Immigrant Entrepreneur from Asia in Canada (tradition + adaptation)
- Doctor from Scandinavia (public health, equity, societal support)
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
# Load environment variables from .env file
load_dotenv()
def _get_chat_client() -> FoundryChatClient:
return FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
async def main() -> None:
# Create debate moderator with structured output for speaker selection
# Note: Participant names and descriptions are automatically injected by the orchestrator
moderator = Agent(
name="Moderator",
description="Guides philosophical discussion by selecting next speaker",
instructions="""
You are a thoughtful moderator guiding a philosophical discussion on the topic handed to you by the user.
Your participants bring diverse global perspectives. Select speakers strategically to:
- Create natural conversation flow and responses to previous points
- Ensure all voices are heard throughout the discussion
- Build on themes and contrasts that emerge
- Allow for respectful challenges and counterpoints
- Guide toward meaningful conclusions
Select speakers who can:
1. Respond directly to points just made
2. Introduce fresh perspectives when needed
3. Bridge or contrast different viewpoints
4. Deepen the philosophical exploration
Finish when:
- Multiple rounds have occurred (at least 6-8 exchanges)
- Key themes have been explored from different angles
- Natural conclusion or synthesis has emerged
- Diminishing returns in new insights
In your final_message, provide a brief synthesis highlighting key themes that emerged.
""",
client=_get_chat_client(),
)
farmer = Agent(
name="Farmer",
description="A rural farmer from Southeast Asia",
instructions="""
You're a farmer from Southeast Asia. Your life is deeply connected to land and family.
You value tradition and sustainability. You are in a philosophical debate.
Share your perspective authentically. Feel free to:
- Challenge other participants respectfully
- Build on points others have made
- Use concrete examples from your experience
- Keep responses thoughtful but concise (2-4 sentences)
""",
client=_get_chat_client(),
)
developer = Agent(
name="Developer",
description="An urban software developer from the United States",
instructions="""
You're a software developer from the United States. Your life is fast-paced and technology-driven.
You value innovation, freedom, and work-life balance. You are in a philosophical debate.
Share your perspective authentically. Feel free to:
- Challenge other participants respectfully
- Build on points others have made
- Use concrete examples from your experience
- Keep responses thoughtful but concise (2-4 sentences)
""",
client=_get_chat_client(),
)
teacher = Agent(
name="Teacher",
description="A retired history teacher from Eastern Europe",
instructions="""
You're a retired history teacher from Eastern Europe. You bring historical and philosophical
perspectives to discussions. You value legacy, learning, and cultural continuity.
You are in a philosophical debate.
Share your perspective authentically. Feel free to:
- Challenge other participants respectfully
- Build on points others have made
- Use concrete examples from history or your teaching experience
- Keep responses thoughtful but concise (2-4 sentences)
""",
client=_get_chat_client(),
)
activist = Agent(
name="Activist",
description="A young activist from South America",
instructions="""
You're a young activist from South America. You focus on social justice, environmental rights,
and generational change. You are in a philosophical debate.
Share your perspective authentically. Feel free to:
- Challenge other participants respectfully
- Build on points others have made
- Use concrete examples from your activism
- Keep responses thoughtful but concise (2-4 sentences)
""",
client=_get_chat_client(),
)
spiritual_leader = Agent(
name="SpiritualLeader",
description="A spiritual leader from the Middle East",
instructions="""
You're a spiritual leader from the Middle East. You provide insights grounded in religion,
morality, and community service. You are in a philosophical debate.
Share your perspective authentically. Feel free to:
- Challenge other participants respectfully
- Build on points others have made
- Use examples from spiritual teachings or community work
- Keep responses thoughtful but concise (2-4 sentences)
""",
client=_get_chat_client(),
)
artist = Agent(
name="Artist",
description="An artist from Africa",
instructions="""
You're an artist from Africa. You view life through creative expression, storytelling,
and collective memory. You are in a philosophical debate.
Share your perspective authentically. Feel free to:
- Challenge other participants respectfully
- Build on points others have made
- Use examples from your art or cultural traditions
- Keep responses thoughtful but concise (2-4 sentences)
""",
client=_get_chat_client(),
)
immigrant = Agent(
name="Immigrant",
description="An immigrant entrepreneur from Asia living in Canada",
instructions="""
You're an immigrant entrepreneur from Asia living in Canada. You balance tradition with adaptation.
You focus on family success, risk, and opportunity. You are in a philosophical debate.
Share your perspective authentically. Feel free to:
- Challenge other participants respectfully
- Build on points others have made
- Use examples from your immigrant and entrepreneurial journey
- Keep responses thoughtful but concise (2-4 sentences)
""",
client=_get_chat_client(),
)
doctor = Agent(
name="Doctor",
description="A doctor from Scandinavia",
instructions="""
You're a doctor from Scandinavia. Your perspective is shaped by public health, equity,
and structured societal support. You are in a philosophical debate.
Share your perspective authentically. Feel free to:
- Challenge other participants respectfully
- Build on points others have made
- Use examples from healthcare and societal systems
- Keep responses thoughtful but concise (2-4 sentences)
""",
client=_get_chat_client(),
)
# termination_condition: stop after 10 assistant messages
# Mark participant responses as intermediate so the stream shows the
# conversation as it unfolds while the orchestrator's transcript remains the
# terminal workflow output.
workflow = (
GroupChatBuilder(
participants=[farmer, developer, teacher, activist, spiritual_leader, artist, immigrant, doctor],
termination_condition=lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 10,
intermediate_output_from=[
"all",
],
orchestrator_agent=moderator,
)
.with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 10)
.build()
)
topic = "What does a good life mean to you personally?"
print("\n" + "=" * 80)
print("PHILOSOPHICAL DEBATE: Perspectives on a Good Life")
print("=" * 80)
print(f"\nTopic: {topic}")
print("\nParticipants:")
print(" - Farmer (Southeast Asia)")
print(" - Developer (United States)")
print(" - Teacher (Eastern Europe)")
print(" - Activist (South America)")
print(" - SpiritualLeader (Middle East)")
print(" - Artist (Africa)")
print(" - Immigrant (Asia → Canada)")
print(" - Doctor (Scandinavia)")
print("\n" + "=" * 80)
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
async for event in workflow.run(f"Please begin the discussion on: {topic}", stream=True):
if event.type in ("intermediate", "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")
"""
Sample Output:
================================================================================
PHILOSOPHICAL DEBATE: Perspectives on a Good Life
================================================================================
Topic: What does a good life mean to you personally?
Participants:
- Farmer (Southeast Asia)
- Developer (United States)
- Teacher (Eastern Europe)
- Activist (South America)
- SpiritualLeader (Middle East)
- Artist (Africa)
- Immigrant (Asia → Canada)
- Doctor (Scandinavia)
================================================================================
DISCUSSION BEGINS
================================================================================
[Farmer]
To me, a good life is deeply intertwined with the rhythm of the land and the nurturing of relationships with my
family and community. It means cultivating crops that respect our environment, ensuring sustainability for future
generations, and sharing meals made from our harvests around the dinner table. The joy found in everyday
tasks—planting rice or tending to our livestock—creates a sense of fulfillment that cannot be measured by material
wealth. It's the simple moments, like sharing stories with my children under the stars, that truly define a good
life. What good is progress if it isolates us from those we love and the land that sustains us?
[Developer]
As a software developer in an urban environment, a good life for me hinges on the intersection of innovation,
creativity, and balance. It's about having the freedom to explore new technologies that can solve real-world
problems while ensuring that my work doesn't encroach on my personal life. For instance, I value remote work
flexibility, which allows me to maintain connections with family and friends, similar to how the Farmer values
community. While our lifestyles may differ markedly, both of us seek fulfillment—whether through meaningful work or
rich personal experiences. The challenge is finding harmony between technological progress and preserving the
intimate human connections that truly enrich our lives.
[SpiritualLeader]
From my spiritual perspective, a good life embodies a balance between personal fulfillment and service to others,
rooted in compassion and community. In our teachings, we emphasize that true happiness comes from helping those in
need and fostering strong connections with our families and neighbors. Whether it's the Farmer nurturing the earth
or the Developer creating tools to enhance lives, both contribute to the greater good. The essence of a good life
lies in our intentions and actions—finding ways to serve our communities, spread kindness, and live harmoniously
with those around us. Ultimately, as we align our personal beliefs with our communal responsibilities, we cultivate
a richness that transcends material wealth.
[Activist]
As a young activist in South America, a good life for me is about advocating for social justice and environmental
sustainability. It means living in a society where everyone's rights are respected and where marginalized voices,
particularly those of Indigenous communities, are amplified. I see a good life as one where we work collectively to
dismantle oppressive systems—such as deforestation and inequality—while nurturing our planet. For instance, through
my activism, I've witnessed the transformative power of community organizing, where collective efforts lead to real
change, like resisting destructive mining practices that threaten our rivers and lands. A good life, therefore, is
not just lived for oneself but is deeply tied to the well-being of our communities and the health of our
environment. How can we, regardless of our backgrounds, collaborate to foster these essential changes?
[Teacher]
As a retired history teacher from Eastern Europe, my understanding of a good life is deeply rooted in the lessons
drawn from history and the struggle for freedom and dignity. Historical events, such as the fall of the Iron
Curtain, remind us of the profound importance of liberty and collective resilience. A good life, therefore, is about
cherishing our freedoms and working towards a society where everyone has a voice, much as my students and I
discussed the impacts of totalitarian regimes. Additionally, I believe it involves fostering cultural continuity,
where we honor our heritage while embracing progressive values. We must learn from the past—especially the
consequences of neglecting empathy and solidarity—so that we can cultivate a future that values every individual's
contributions to the rich tapestry of our shared humanity. How can we ensure that the lessons of history inform a
more compassionate and just society moving forward?
[Artist]
As an artist from Africa, I define a good life as one steeped in cultural expression, storytelling, and the
celebration of our collective memories. Art is a powerful medium through which we capture our histories, struggles,
and triumphs, creating a tapestry that connects generations. For instance, in my work, I often draw from folktales
and traditional music, weaving narratives that reflect the human experience, much like how the retired teacher
emphasizes learning from history. A good life involves not only personal fulfillment but also the responsibility to
share our narratives and use our creativity to inspire change, whether addressing social injustices or environmental
issues. It's in this interplay of art and activism that we can transcend individual existence and contribute to a
collective good, fostering empathy and understanding among diverse communities. How can we harness art to bridge
differences and amplify marginalized voices in our pursuit of a good life?
================================================================================
DISCUSSION SUMMARY
================================================================================
As our discussion unfolds, several key themes have gracefully emerged, reflecting the richness of diverse
perspectives on what constitutes a good life. From the rural farmer's integration with the land to the developer's
search for balance between technology and personal connection, each viewpoint validates that fulfillment, at its
core, transcends material wealth. The spiritual leader and the activist highlight the importance of community and
social justice, while the history teacher and the artist remind us of the lessons and narratives that shape our
cultural and personal identities.
Ultimately, the good life seems to revolve around meaningful relationships, honoring our legacies while striving for
progress, and nurturing both our inner selves and external communities. This dialogue demonstrates that despite our
varied backgrounds and experiences, the quest for a good life binds us together, urging cooperation and empathy in
our shared human journey.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,147 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import cast
from agent_framework import (
Agent,
AgentResponseUpdate,
Message,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import GroupChatBuilder, GroupChatState
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: Group Chat with a round-robin speaker selector
What it does:
- Demonstrates the selection_func parameter for GroupChat orchestration
- Uses a pure Python function to control speaker selection based on conversation state
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
def round_robin_selector(state: GroupChatState) -> str:
"""A round-robin selector function that picks the next speaker based on the current round index."""
participant_names = list(state.participants.keys())
return participant_names[state.current_round % len(participant_names)]
async def main() -> None:
# Create a Responses client using Azure OpenAI and Azure CLI credentials for all agents
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# Participant agents
expert = Agent(
name="PythonExpert",
instructions=(
"You are an expert in Python in a workgroup. "
"Your job is to answer Python related questions and refine your answer "
"based on feedback from all the other participants."
),
client=client,
)
verifier = Agent(
name="AnswerVerifier",
instructions=(
"You are a programming expert in a workgroup. "
f"Your job is to review the answer provided by {expert.name} and point "
"out statements that are technically true but practically dangerous."
"If there is nothing woth pointing out, respond with 'The answer looks good to me.'"
),
client=client,
)
clarifier = Agent(
name="AnswerClarifier",
instructions=(
"You are an accessibility expert in a workgroup. "
f"Your job is to review the answer provided by {expert.name} and point "
"out jargons or complex terms that may be difficult for a beginner to understand."
"If there is nothing worth pointing out, respond with 'The answer looks clear to me.'"
),
client=client,
)
skeptic = Agent(
name="Skeptic",
instructions=(
"You are a devil's advocate in a workgroup. "
f"Your job is to review the answer provided by {expert.name} and point "
"out caveats, exceptions, and alternative perspectives."
"If there is nothing worth pointing out, respond with 'I have no further questions.'"
),
client=client,
)
# Build the group chat workflow
# termination_condition: stop after 6 messages (user task + one full rounds + 1)
# One round is expert -> verifier -> clarifier -> skeptic, after which the expert gets to respond again.
# This will end the conversation after the expert has spoken 2 times (one iteration loop)
# Note: it's possible that the expert gets it right the first time and the other participants
# have nothing to add, but for demo purposes we want to see at least one full round of interaction.
# Mark participant responses as intermediate so the stream shows the
# conversation as it unfolds while the orchestrator's transcript remains the
# terminal workflow output.
workflow = (
GroupChatBuilder(
participants=[expert, verifier, clarifier, skeptic],
termination_condition=lambda conversation: len(conversation) >= 6,
intermediate_output_from=[expert, verifier, clarifier, skeptic],
selection_func=round_robin_selector,
)
# Set a hard termination condition: stop after 6 messages (user task + one full rounds + 1)
# One round is expert -> verifier -> clarifier -> skeptic, after which the expert gets to respond again.
# This will end the conversation after the expert has spoken 2 times (one iteration loop)
# Note: it's possible that the expert gets it right the first time and the other participants
# have nothing to add, but for demo purposes we want to see at least one full round of interaction.
.with_termination_condition(lambda conversation: len(conversation) >= 6)
.build()
)
task = "How does Pythons Protocol differ from abstract base classes?"
print("\nStarting Group Chat with round-robin speaker selector...\n")
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
async for event in workflow.run(task, stream=True):
if event.type in ("intermediate", "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 __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,167 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
import os
from typing import cast
from agent_framework import (
Agent,
AgentResponseUpdate,
Message,
resolve_agent_id,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import HandoffBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
logging.basicConfig(level=logging.ERROR)
"""Sample: Autonomous handoff workflow with agent iteration.
This sample demonstrates `.with_autonomous_mode()`, where agents continue
iterating on their task until they explicitly invoke a handoff tool. This allows
specialists to perform long-running autonomous work (research, coding, analysis)
without prematurely returning control to the coordinator or user.
Routing Pattern:
User -> Coordinator -> Specialist (iterates N times) -> Handoff -> Final Output
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for FoundryChatClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run `az login` before executing the sample.
Key Concepts:
- Autonomous interaction mode: agents iterate until they handoff
- Turn limits: use `.with_autonomous_mode(turn_limits={agent_name: N})` to cap iterations per agent
"""
# Load environment variables from .env file
load_dotenv()
def create_agents(
client: FoundryChatClient,
) -> tuple[Agent, Agent, Agent]:
"""Create coordinator and specialists for autonomous iteration."""
coordinator = Agent(
client=client,
instructions=(
"You are a coordinator. You break down a user query into a research task and a summary task. "
"Assign the two tasks to the appropriate specialists, one after the other."
),
name="coordinator",
require_per_service_call_history_persistence=True,
)
research_agent = Agent(
client=client,
instructions=(
"You are a research specialist that explores topics thoroughly using web search. "
"When given a research task, break it down into multiple aspects and explore each one. "
"Continue your research across multiple responses - don't try to finish everything in one "
"response. After each response, think about what else needs to be explored. When you have "
"covered the topic comprehensively (at least 3-4 different aspects), return control to the "
"coordinator. Keep each individual response focused on one aspect."
),
name="research_agent",
require_per_service_call_history_persistence=True,
)
summary_agent = Agent(
client=client,
instructions=(
"You summarize research findings. Provide a concise, well-organized summary. When done, return "
"control to the coordinator."
),
name="summary_agent",
require_per_service_call_history_persistence=True,
)
return coordinator, research_agent, summary_agent
async def main() -> None:
"""Run an autonomous handoff workflow with specialist iteration enabled."""
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
coordinator, research_agent, summary_agent = create_agents(client)
# Build the workflow with autonomous mode
# In autonomous mode, agents continue iterating until they invoke a handoff tool
# termination_condition: Terminate after coordinator provides 5 assistant responses
workflow = (
HandoffBuilder(
name="autonomous_iteration_handoff",
participants=[coordinator, research_agent, summary_agent],
termination_condition=lambda conv: (
sum(1 for msg in conv if msg.author_name == "coordinator" and msg.role == "assistant") >= 5
),
)
.with_start_agent(coordinator)
.add_handoff(coordinator, [research_agent, summary_agent])
.add_handoff(research_agent, [coordinator]) # Research can hand back to coordinator
.add_handoff(summary_agent, [coordinator])
.with_autonomous_mode(
# You can set turn limits per agent to allow some agents to go longer.
# If a limit is not set, the agent will get an default limit: 50.
# Internally, handoff prefers agent names as the agent identifiers if set.
# Otherwise, it falls back to agent IDs.
turn_limits={
resolve_agent_id(coordinator): 5,
resolve_agent_id(research_agent): 10,
resolve_agent_id(summary_agent): 5,
}
)
.build()
)
request = "Perform a comprehensive research on Microsoft Agent Framework."
print("Request:", request)
last_response_id: str | None = None
async for event in workflow.run(request, stream=True):
if event.type == "handoff_sent":
print(f"\nHandoff Event: from {event.data.source} to {event.data.target}\n")
elif event.type == "output":
data = event.data
if isinstance(data, AgentResponseUpdate):
if not data.text:
# Skip updates that don't have text content
# These can be tool calls or other non-text events
continue
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 handoff 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")
"""
Expected behavior:
- Coordinator routes to research_agent.
- Research agent iterates multiple times, exploring different aspects of Microsoft Agent Framework.
- Each iteration adds to the conversation without returning to coordinator.
- After thorough research, research_agent calls handoff to coordinator.
- Coordinator routes to summary_agent for final summary.
In autonomous mode, agents continue working until they invoke a handoff tool,
allowing the research_agent to perform 3-4+ responses before handing off.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,314 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Annotated, cast
from agent_framework import (
Agent,
AgentResponse,
Message,
WorkflowEvent,
WorkflowRunState,
tool,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""Sample: Simple handoff workflow.
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:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run `az login` before executing the sample.
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/02-agents/tools/function_tool_with_approval.py
# samples/02-agents/tools/function_tool_with_approval_and_sessions.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: FoundryChatClient) -> tuple[Agent, Agent, Agent, Agent]:
"""Create and configure the triage and specialist agents.
Args:
client: The FoundryChatClient 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 = Agent(
client=client,
instructions=(
"You are frontline support triage. Route customer issues to the appropriate specialist agents "
"based on the problem described."
),
name="triage_agent",
require_per_service_call_history_persistence=True,
)
# Refund specialist: Handles refund requests
refund_agent = Agent(
client=client,
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],
require_per_service_call_history_persistence=True,
)
# Order/shipping specialist: Resolves delivery issues
order_agent = Agent(
client=client,
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],
require_per_service_call_history_persistence=True,
)
# Return specialist: Handles return requests
return_agent = Agent(
client=client,
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],
require_per_service_call_history_persistence=True,
)
return triage_agent, refund_agent, order_agent, return_agent
def _handle_events(events: list[WorkflowEvent]) -> list[WorkflowEvent[HandoffAgentUserRequest]]:
"""Process workflow events and extract any pending user input requests.
This function inspects each event type and:
- Prints workflow status changes (IDLE, IDLE_WITH_PENDING_REQUESTS, etc.)
- Displays final conversation snapshots when workflow completes
- Prints user input request prompts
- Collects all request_info events for response handling
Args:
events: List of WorkflowEvent to process
Returns:
List of WorkflowEvent[HandoffAgentUserRequest] representing pending user input requests
"""
requests: list[WorkflowEvent[HandoffAgentUserRequest]] = []
for event in events:
if event.type == "handoff_sent":
# handoff_sent event: Indicates a handoff has been initiated
print(f"\n[Handoff from {event.data.source} to {event.data.target} initiated.]")
elif event.type == "status" and event.state in {
WorkflowRunState.IDLE,
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
}:
# Status event: Indicates workflow state changes
print(f"\n[Workflow Status] {event.state}")
elif event.type == "output":
# Output event: Contains contents generated by the workflow
data = event.data
if isinstance(data, AgentResponse):
for message in data.messages:
if not message.text:
# Skip messages without text (e.g., tool calls)
continue
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text}")
elif event.type == "output":
# The output of the handoff workflow is a collection of chat messages from all participants
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("===================================")
elif event.type == "request_info" and isinstance(event.data, HandoffAgentUserRequest):
_print_handoff_agent_user_request(event.data.agent_response)
requests.append(cast(WorkflowEvent[HandoffAgentUserRequest], event))
return requests
def _print_handoff_agent_user_request(response: AgentResponse) -> None:
"""Display the agent's response messages when requesting user input.
This will happen when an agent generates a response that doesn't trigger
a handoff, i.e., the agent is asking the user for more information.
Args:
response: The AgentResponse from the agent requesting user input
"""
if not response.messages:
raise RuntimeError("Cannot print agent responses: response has no messages.")
print("\n[Agent is requesting your input...]")
# Print agent responses
for message in response.messages:
if not message.text:
# Skip messages without text (e.g., tool calls)
continue
speaker = message.author_name or message.role
print(f"- {speaker}: {message.text}")
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 Responses client
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
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").
workflow = (
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()
)
# 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
# run(..., stream=True) returns an async iterator of WorkflowEvent
print("[Starting workflow with initial user message...]\n")
initial_message = "Hello, I need assistance with my recent purchase."
print(f"- User: {initial_message}")
workflow_result = workflow.run(initial_message, stream=True)
pending_requests = _handle_events([event async for event in workflow_result])
# 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.request_id: HandoffAgentUserRequest.terminate() for req 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.request_id: HandoffAgentUserRequest.create_response(user_response) for req in pending_requests
}
# Send responses and get new events
# We use run(responses=...) to get events from the workflow, allowing us to
# display agent responses and handle new requests as they arrive
events = await workflow.run(responses=responses)
pending_requests = _handle_events(events)
"""
Sample Output:
[Starting workflow with initial user message...]
- User: Hello, I need assistance with my recent purchase.
- triage_agent: Could you please provide more details about the issue you're experiencing with your recent purchase? This will help me route you to the appropriate specialist.
[Workflow Status] IDLE_WITH_PENDING_REQUESTS
- User: My order 1234 arrived damaged and the packaging was destroyed. I'd like to return it.
- triage_agent: I've directed your request to our return agent, who will assist you with returning the damaged order. Thank you for your patience!
- return_agent: The return for your order 1234 has been successfully initiated. You will receive return instructions via email shortly. If you have any other questions or need further assistance, feel free to ask!
[Workflow Status] IDLE_WITH_PENDING_REQUESTS
- User: Thanks for resolving this.
=== Final Conversation Snapshot ===
- user: Hello, I need assistance with my recent purchase.
- triage_agent: Could you please provide more details about the issue you're experiencing with your recent purchase? This will help me route you to the appropriate specialist.
- user: My order 1234 arrived damaged and the packaging was destroyed. I'd like to return it.
- triage_agent: I've directed your request to our return agent, who will assist you with returning the damaged order. Thank you for your patience!
- return_agent: The return for your order 1234 has been successfully initiated. You will receive return instructions via email shortly. If you have any other questions or need further assistance, feel free to ask!
- user: Thanks for resolving this.
- triage_agent: You're welcome! If you have any more questions or need assistance in the future, feel free to reach out. Have a great day!
===================================
[Workflow Status] IDLE
""" # noqa: E501
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,195 @@
# 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:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- `az login` (Azure CLI authentication)
"""
import asyncio
import os
from collections.abc import AsyncIterable
from typing import cast
from agent_framework import (
Agent,
AgentResponseUpdate,
Message,
WorkflowEvent,
WorkflowRunState,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
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 = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
triage = Agent(
client=client,
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."
),
require_per_service_call_history_persistence=True,
)
code_interpreter_tool = client.get_code_interpreter_tool()
code_specialist = Agent(
client=client,
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],
require_per_service_call_history_persistence=True,
)
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())
@@ -0,0 +1,260 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
import os
from pathlib import Path
from typing import Any
from agent_framework import (
Agent,
Content,
FileCheckpointStorage,
Workflow,
WorkflowEvent,
tool,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: Handoff Workflow with Tool Approvals + Checkpoint Resume
Demonstrates resuming a handoff workflow from a checkpoint while handling both
HandoffAgentUserRequest prompts and function approval request Content for tool calls
(e.g., submit_refund).
Scenario:
1. User starts a conversation with the workflow.
2. Agents may emit user input requests or tool approval requests.
3. Workflow writes a checkpoint capturing pending requests and pauses.
4. Process can exit/restart.
5. On resume: Restore checkpoint, inspect pending requests, then provide responses.
6. Workflow continues from the saved state.
Pattern:
- workflow.run(checkpoint_id=..., stream=True) to restore checkpoint and discover pending requests.
- workflow.run(stream=True, responses=responses) to supply human replies and approvals.
(Two steps are needed here because the sample must inspect request types before building responses.
When response payloads are already known, use the single-call form:
workflow.run(stream=True, checkpoint_id=..., responses=responses).)
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Azure CLI authentication (az login).
"""
CHECKPOINT_DIR = Path(__file__).parent / "tmp" / "handoff_checkpoints"
CHECKPOINT_DIR.mkdir(parents=True, exist_ok=True)
@tool(approval_mode="always_require")
def submit_refund(refund_description: str, amount: str, order_id: str) -> str:
"""Capture a refund request for manual review before processing."""
return f"refund recorded for order {order_id} (amount: {amount}) with details: {refund_description}"
def create_agents(client: FoundryChatClient) -> tuple[Agent, Agent, Agent]:
"""Create a simple handoff scenario: triage, refund, and order specialists."""
triage = Agent(
client=client,
name="triage_agent",
instructions=(
"You are a customer service triage agent. Listen to customer issues and determine "
"if they need refund help or order tracking. Use handoff_to_refund_agent or "
"handoff_to_order_agent to transfer them."
),
require_per_service_call_history_persistence=True,
)
refund = Agent(
client=client,
name="refund_agent",
instructions=(
"You are a refund specialist. Help customers with refund requests. "
"Be empathetic and ask for order numbers if not provided. "
"When the user confirms they want a refund and supplies order details, call submit_refund "
"to record the request before continuing."
),
tools=[submit_refund],
require_per_service_call_history_persistence=True,
)
order = Agent(
client=client,
name="order_agent",
instructions=(
"You are an order tracking specialist. Help customers track their orders. "
"Ask for order numbers and provide shipping updates."
),
require_per_service_call_history_persistence=True,
)
return triage, refund, order
def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow:
"""Build the handoff workflow with checkpointing enabled."""
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
triage, refund, order = create_agents(client)
# checkpoint_storage: Enable checkpointing for resume
# termination_condition: Terminate after 5 user messages for this demo
return (
HandoffBuilder(
name="checkpoint_handoff_demo",
participants=[triage, refund, order],
checkpoint_storage=checkpoint_storage,
termination_condition=lambda conv: sum(1 for msg in conv if msg.role == "user") >= 5,
)
.with_start_agent(triage)
.build()
)
def print_handoff_agent_user_request(request: HandoffAgentUserRequest, request_id: str) -> None:
"""Log pending handoff request details for debugging."""
print(f"\n{'=' * 60}")
print("User input needed")
print(f"Request ID: {request_id}")
response = request.agent_response
if not response.messages:
print("(No agent messages)")
return
for message in response.messages:
if not message.text:
continue
speaker = message.author_name or message.role
print(f"{speaker}: {message.text}")
print(f"{'=' * 60}\n")
def print_function_approval_request(request: Content, request_id: str) -> None:
"""Log pending tool approval details for debugging."""
args = request.function_call.parse_arguments() or {} # type: ignore
print(f"\n{'=' * 60}")
print("Tool approval required")
print(f"Request ID: {request_id}")
print(f"Function: {request.function_call.name}") # type: ignore
print(f"Arguments:\n{json.dumps(args, indent=2)}")
print(f"{'=' * 60}\n")
async def main() -> None:
"""
Demonstrate the checkpoint-based pause/resume pattern for handoff workflows.
This sample shows:
1. Starting a workflow and getting a HandoffAgentUserRequest
2. Pausing (checkpoint is saved automatically)
3. Resuming from checkpoint with a user response or tool approval
4. Continuing the conversation until completion
"""
# Clean up old checkpoints
for file in CHECKPOINT_DIR.glob("*.json"):
file.unlink()
for file in CHECKPOINT_DIR.glob("*.json.tmp"):
file.unlink()
storage = FileCheckpointStorage(storage_path=CHECKPOINT_DIR)
workflow = create_workflow(checkpoint_storage=storage)
# Scripted human input for demo purposes
handoff_responses = [
(
"The headphones in order 12345 arrived cracked. "
"Please submit the refund for $89.99 and send a replacement to my original address."
),
"Yes, that covers the damage and refund request.",
"That's everything I needed for the refund.",
"Thanks for handling the refund.",
]
print("=" * 60)
print("HANDOFF WORKFLOW CHECKPOINT DEMO")
print("=" * 60)
# Scenario: User needs help with a damaged order
initial_request = "Hi, my order 12345 arrived damaged. I need a refund."
# Phase 1: Initial run - workflow will pause when it needs user input
print("Running initial workflow...")
results = await workflow.run(message=initial_request, stream=True)
# Iterate through streamed events and collect request_info events
request_events: list[WorkflowEvent] = []
async for event in results:
event: WorkflowEvent
if event.type == "request_info":
request_events.append(event)
if not request_events:
print("Workflow completed without needing user input")
return
print("=" * 60)
print("WORKFLOW PAUSED with pending requests")
print("=" * 60)
# Phase 2: Running until no more user input is needed
# This creates a new workflow instance to simulate a fresh process start,
# but points it to the same checkpoint storage
while request_events:
print("\n" + "=" * 60)
print("Simulating process restart...")
print("=" * 60)
workflow = create_workflow(checkpoint_storage=storage)
responses: dict[str, Any] = {}
for request_event in request_events:
print(f"Pending request ID: {request_event.request_id}, Type: {type(request_event.data)}")
if isinstance(request_event.data, HandoffAgentUserRequest):
print_handoff_agent_user_request(request_event.data, request_event.request_id)
response = handoff_responses.pop(0)
print(f"Responding with: {response}")
responses[request_event.request_id] = HandoffAgentUserRequest.create_response(response)
elif isinstance(request_event.data, Content) and request_event.data.type == "function_approval_request":
print_function_approval_request(request_event.data, request_event.request_id)
print("Approving tool call...")
responses[request_event.request_id] = request_event.data.to_function_approval_response(approved=True)
else:
# This sample only expects HandoffAgentUserRequest and function approval requests
raise ValueError(f"Unsupported request type: {type(request_event.data)}")
checkpoint = await storage.get_latest(workflow_name=workflow.name)
if not checkpoint:
raise RuntimeError("No checkpoints found.")
checkpoint_id = checkpoint.checkpoint_id
print("Resuming workflow from checkpoint...")
results = await workflow.run(responses=responses, checkpoint_id=checkpoint_id, stream=True)
# Iterate through streamed events and collect request_info events
request_events: list[WorkflowEvent] = []
async for event in results:
if event.type == "request_info":
request_events.append(event)
print("\n" + "=" * 60)
print("DEMO COMPLETE")
print("=" * 60)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,158 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
import logging
import os
from typing import cast
from agent_framework import (
Agent,
AgentResponseUpdate,
Message,
WorkflowEvent,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import GroupChatRequestSentEvent, MagenticBuilder, MagenticProgressLedger
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)
"""
Sample: Magentic Orchestration (multi-agent)
What it does:
- Orchestrates multiple agents using `MagenticBuilder` with streaming callbacks.
- ResearcherAgent (Agent backed by an OpenAI chat client) for
finding information.
- CoderAgent (Agent backed by OpenAI Assistants with the hosted
code interpreter tool) for analysis and computation.
The workflow is configured with:
- A Standard Magentic manager (uses a chat client for planning and progress).
- Callbacks for final results, per-message agent responses, and streaming
token updates.
When run, the script builds the workflow, submits a task about estimating the
energy efficiency and CO2 emissions of several ML models, streams intermediate
events, and prints the final answer. The workflow completes when idle.
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
# Load environment variables from .env file
load_dotenv()
async def main() -> None:
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
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."
),
client=client,
)
# Create code interpreter tool using instance method
code_interpreter_tool = 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=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=client,
)
print("\nBuilding Magentic Workflow...")
# Mark participant responses as intermediate so the stream shows the
# conversation as it unfolds while the manager's final answer remains the
# terminal workflow output.
workflow = MagenticBuilder(
participants=[researcher_agent, coder_agent],
intermediate_output_from=[researcher_agent, coder_agent],
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...")
# Keep track of the last executor to format output nicely in streaming mode
last_response_id: str | None = None
output_event: WorkflowEvent | None = None
async for event in workflow.run(task, stream=True):
if event.type in ("intermediate", "output") and isinstance(event.data, AgentResponseUpdate):
response_id = event.data.response_id
if response_id != last_response_id:
if last_response_id is not None:
print("\n")
print(f"- {event.executor_id}:", end=" ", flush=True)
last_response_id = response_id
print(event.data, end="", flush=True)
elif event.type == "magentic_orchestrator":
print(f"\n[Magentic Orchestrator Event] Type: {event.data.event_type.name}")
if isinstance(event.data.content, Message):
print(f"Please review the plan:\n{event.data.content.text}")
elif isinstance(event.data.content, MagenticProgressLedger):
print(f"Please review progress ledger:\n{json.dumps(event.data.content.to_dict(), indent=2)}")
else:
print(f"Unknown data type in MagenticOrchestratorEvent: {type(event.data.content)}")
# Block to allow user to read the plan/progress before continuing
# Note: this is for demonstration only and is not the recommended way to handle human interaction.
# Please refer to `with_plan_review` for proper human interaction during planning phases.
await asyncio.get_event_loop().run_in_executor(None, input, "Press Enter to continue...")
elif event.type == "group_chat" and isinstance(event.data, GroupChatRequestSentEvent):
print(f"\n[REQUEST SENT ({event.data.round_index})] to agent: {event.data.participant_name}")
elif event.type == "output":
output_event = event
if output_event:
# The output of the magentic workflow is a collection of chat messages from all participants
outputs = cast(list[Message], output_event.data)
print("\n" + "=" * 80)
print("\nFinal Conversation Transcript:\n")
for message in outputs:
print(f"{message.author_name or message.role}: {message.text}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,319 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
import os
from datetime import datetime
from pathlib import Path
from typing import cast
from agent_framework import (
Agent,
FileCheckpointStorage,
Message,
WorkflowCheckpoint,
WorkflowEvent,
WorkflowRunState,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import MagenticBuilder, MagenticPlanReviewRequest
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: Magentic Orchestration + Checkpointing
The goal of this sample is to show the exact mechanics needed to pause a Magentic
workflow that requires human plan review, persist the outstanding request via a
checkpoint, and later resume the workflow by feeding in the saved response.
Concepts highlighted here:
1. **Deterministic executor IDs** - the orchestrator and plan-review request executor
must keep stable IDs so the checkpoint state aligns when we rebuild the graph.
2. **Executor snapshotting** - checkpoints capture the pending plan-review request
map, at superstep boundaries.
3. **Resume with responses** - `Workflow.run(responses=...)` accepts a
`responses` mapping so we can inject the stored human reply during restoration.
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
TASK = (
"Draft a concise internal brief describing how our research and implementation teams should collaborate "
"to launch a beta feature for data-driven email summarization. Highlight the key milestones, "
"risks, and communication cadence."
)
# Dedicated folder for captured checkpoints. Keeping it under the sample directory
# makes it easy to inspect the JSON blobs produced by each run.
CHECKPOINT_DIR = Path(__file__).parent / "tmp" / "magentic_checkpoints"
def build_workflow(checkpoint_storage: FileCheckpointStorage):
"""Construct the Magentic workflow graph with checkpointing enabled."""
# Two vanilla ChatAgents act as participants in the orchestration. They do not need
# extra state handling because their inputs/outputs are fully described by chat messages.
researcher = Agent(
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=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
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=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
)
# Create a manager agent for orchestration
manager_agent = Agent(
name="MagenticManager",
description="Orchestrator that coordinates the research and writing workflow",
instructions="You coordinate a team to complete complex tasks efficiently.",
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
)
# The builder wires in the Magentic orchestrator, sets the plan review path, and
# stores the checkpoint backend so the runtime knows where to persist snapshots.
return MagenticBuilder(
participants=[researcher, writer],
enable_plan_review=True,
checkpoint_storage=checkpoint_storage,
manager_agent=manager_agent,
max_round_count=10,
max_stall_count=3,
).build()
async def main() -> None:
# Stage 0: make sure the checkpoint folder is empty so we inspect only checkpoints
# written by this invocation. This prevents stale files from previous runs from
# confusing the analysis.
CHECKPOINT_DIR.mkdir(parents=True, exist_ok=True)
for file in CHECKPOINT_DIR.glob("*.json"):
file.unlink()
checkpoint_storage = FileCheckpointStorage(CHECKPOINT_DIR)
print("\n=== Stage 1: run until plan review request (checkpointing active) ===")
workflow = build_workflow(checkpoint_storage)
# Run the workflow until the first is surfaced. The event carries the
# request_id we must reuse on resume. In a real system this is where the UI would present
# the plan for human review.
plan_review_request: MagenticPlanReviewRequest | None = None
async for event in workflow.run(TASK, stream=True):
if event.type == "request_info" and event.request_type is MagenticPlanReviewRequest:
plan_review_request = event.data
print(f"Captured plan review request: {event.request_id}")
if event.type == "status" and event.state is WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
break
if plan_review_request is None:
print("No plan review request emitted; nothing to resume.")
return
resume_checkpoint = await checkpoint_storage.get_latest(workflow_name=workflow.name)
if not resume_checkpoint:
print("No checkpoints persisted.")
return
print(f"Using checkpoint {resume_checkpoint.checkpoint_id} at iteration {resume_checkpoint.iteration_count}")
# Show that the checkpoint JSON indeed contains the pending plan-review request record.
checkpoint_path = checkpoint_storage.storage_path / f"{resume_checkpoint.checkpoint_id}.json"
if checkpoint_path.exists():
with checkpoint_path.open() as f:
snapshot = json.load(f)
request_map = snapshot.get("pending_request_info_events", {})
print(f"Pending plan-review requests persisted in checkpoint: {list(request_map.keys())}")
print("\n=== Stage 2: resume from checkpoint and approve plan ===")
resumed_workflow = build_workflow(checkpoint_storage)
# Construct an approval reply to supply when the plan review request is re-emitted.
approval = plan_review_request.approve()
# Resume execution and capture the re-emitted plan review request.
request_info_event: WorkflowEvent | None = None
async for event in resumed_workflow.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
if event.type == "request_info" and isinstance(event.data, MagenticPlanReviewRequest):
request_info_event = event
if request_info_event is None:
print("No plan review request re-emitted on resume; cannot approve.")
return
print(f"Resumed plan review request: {request_info_event.request_id}")
# Supply the approval and continue to run to completion.
final_event: WorkflowEvent | None = None
async for event in resumed_workflow.run(stream=True, responses={request_info_event.request_id: approval}):
if event.type == "output":
final_event = event
if final_event is None:
print("Workflow did not complete after resume.")
return
# Final sanity check: display the assistant's answer as proof the orchestration reached
# a natural completion after resuming from the checkpoint.
result = final_event.data
if not result:
print("No result data from workflow.")
return
output_messages = cast(list[Message], result)
print("\n=== Final Answer ===")
# The output of the Magentic workflow is a list of ChatMessages with only one final message
# generated by the orchestrator.
print(output_messages[-1].text)
# ------------------------------------------------------------------
# Stage 3: demonstrate resuming from a later checkpoint (post-plan)
# ------------------------------------------------------------------
def _pending_message_count(cp: WorkflowCheckpoint) -> int:
return sum(len(msg_list) for msg_list in cp.messages.values() if isinstance(msg_list, list))
all_checkpoints = await checkpoint_storage.list_checkpoints(workflow_name=resume_checkpoint.workflow_name)
later_checkpoints_with_messages = [
cp
for cp in all_checkpoints
if cp.iteration_count > resume_checkpoint.iteration_count and _pending_message_count(cp) > 0
]
if later_checkpoints_with_messages:
post_plan_checkpoint = max(later_checkpoints_with_messages, key=lambda cp: datetime.fromisoformat(cp.timestamp))
else:
later_checkpoints = [cp for cp in all_checkpoints if cp.iteration_count > resume_checkpoint.iteration_count]
if not later_checkpoints:
print("\nNo additional checkpoints recorded beyond plan approval; sample complete.")
return
post_plan_checkpoint = max(later_checkpoints, key=lambda cp: datetime.fromisoformat(cp.timestamp))
print("\n=== Stage 3: resume from post-plan checkpoint ===")
pending_messages = _pending_message_count(post_plan_checkpoint)
print(
f"Resuming from checkpoint {post_plan_checkpoint.checkpoint_id} at iteration "
f"{post_plan_checkpoint.iteration_count} (pending messages: {pending_messages})"
)
if pending_messages == 0:
print("Checkpoint has no pending messages; no additional work expected on resume.")
final_event_post: WorkflowEvent | None = None
post_emitted_events = False
post_plan_workflow = build_workflow(checkpoint_storage)
async for event in post_plan_workflow.run(checkpoint_id=post_plan_checkpoint.checkpoint_id, stream=True):
post_emitted_events = True
if event.type == "output":
final_event_post = event
if final_event_post is None:
if not post_emitted_events:
print("No new events were emitted; checkpoint already captured a completed run.")
print("\n=== Final Answer (post-plan resume) ===")
print(output_messages[-1].text)
return
print("Workflow did not complete after post-plan resume.")
return
post_result = final_event_post.data
if not post_result:
print("No result data from post-plan resume.")
return
output_messages = cast(list[Message], post_result)
print("\n=== Final Answer (post-plan resume) ===")
# The output of the Magentic workflow is a list of ChatMessages with only one final message
# generated by the orchestrator.
print(output_messages[-1].text)
"""
Sample Output:
=== Stage 1: run until plan review request (checkpointing active) ===
Captured plan review request: 3a1a4a09-4ed1-4c90-9cf6-9ac488d452c0
Using checkpoint 4c76d77a-6ff8-4d2b-84f6-824771ffac7e at iteration 1
Pending plan-review requests persisted in checkpoint: ['3a1a4a09-4ed1-4c90-9cf6-9ac488d452c0']
=== Stage 2: resume from checkpoint and approve plan ===
=== Final Answer ===
Certainly! Here's your concise internal brief on how the research and implementation teams should collaborate for
the beta launch of the data-driven email summarization feature:
---
**Internal Brief: Collaboration Plan for Data-driven Email Summarization Beta Launch**
**Collaboration Approach**
- **Joint Kickoff:** Research and Implementation teams hold a project kickoff to align on objectives, requirements,
and success metrics.
- **Ongoing Coordination:** Teams collaborate closely; researchers share model developments and insights, while
implementation ensures smooth integration and user experience.
- **Real-time Feedback Loop:** Implementation provides early feedback on technical integration and UX, while
Research evaluates initial performance and user engagement signals post-integration.
**Key Milestones**
1. **Requirement Finalization & Scoping** - Define MVP feature set and success criteria.
2. **Model Prototyping & Evaluation** - Researchers develop and validate summarization models with agreed metrics.
3. **Integration & Internal Testing** - Implementation team integrates the model; internal alpha testing and
compliance checks.
4. **Beta User Onboarding** - Recruit a select cohort of beta users and guide them through onboarding.
5. **Beta Launch & Monitoring** - Soft-launch for beta group, with active monitoring of usage, feedback,
and performance.
6. **Iterative Improvements** - Address issues, refine features, and prepare for possible broader rollout.
**Top Risks**
- **Data Privacy & Compliance:** Strict protocols and compliance reviews to prevent data leakage.
- **Model Quality (Bias, Hallucination):** Careful monitoring of summary accuracy; rapid iterations if critical
errors occur.
- **User Adoption:** Ensuring the beta solves genuine user needs, collecting actionable feedback early.
- **Feedback Quality & Quantity:** Proactively schedule user outreach to ensure substantive beta feedback.
**Communication Cadence**
- **Weekly Team Syncs:** Short all-hands progress and blockers meeting.
- **Bi-Weekly Stakeholder Check-ins:** Leadership and project leads address escalations and strategic decisions.
- **Dedicated Slack Channel:** For real-time queries and updates.
- **Documentation Hub:** Up-to-date project docs and FAQs on a shared internal wiki.
- **Post-Milestone Retrospectives:** After critical phases (e.g., alpha, beta), reviewing what worked and what needs
improvement.
**Summary**
Clear alignment, consistent communication, and iterative feedback are key to a successful beta. All team members are
expected to surface issues quickly and keep documentation current as we drive toward launch.
---
=== Stage 3: resume from post-plan checkpoint ===
Resuming from checkpoint 9a3b... at iteration 3 (pending messages: 0)
No new events were emitted; checkpoint already captured a completed run.
=== Final Answer (post-plan resume) ===
(same brief as above)
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,165 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
import os
from collections.abc import AsyncIterable
from typing import cast
from agent_framework import (
Agent,
AgentResponseUpdate,
Message,
WorkflowEvent,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import MagenticBuilder, MagenticPlanReviewRequest, MagenticPlanReviewResponse
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: Magentic Orchestration with Human Plan Review
This sample demonstrates how humans can review and provide feedback on plans
generated by the Magentic workflow orchestrator. When plan review is enabled,
the workflow requests human approval or revision before executing each plan.
Key concepts:
- with_plan_review(): Enables human review of generated plans
- MagenticPlanReviewRequest: The event type for plan review requests
- Human can choose to: approve the plan or provide revision feedback
Plan review options:
- approve(): Accept the proposed plan and continue execution
- revise(feedback): Provide textual feedback to modify the plan
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
# Keep track of the last response to format output nicely in streaming mode
last_response_id: str | None = None
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, MagenticPlanReviewResponse] | None:
"""Process events from the workflow stream to capture human feedback requests."""
global last_response_id
requests: dict[str, MagenticPlanReviewRequest] = {}
async for event in stream:
if event.type == "request_info" and event.request_type is MagenticPlanReviewRequest:
requests[event.request_id] = cast(MagenticPlanReviewRequest, event.data)
if event.type in ("intermediate", "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)
else:
# 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, MagenticPlanReviewResponse] = {}
if requests:
for request_id, request in requests.items():
print("\n\n[Magentic Plan Review Request]")
if request.current_progress is not None:
print("Current Progress Ledger:")
print(json.dumps(request.current_progress.to_dict(), indent=2))
print()
print(f"Proposed Plan:\n{request.plan.text}\n")
print("Please provide your feedback (press Enter to approve):")
reply = input("> ") # noqa: ASYNC250
if reply.strip() == "":
print("Plan approved.\n")
responses[request_id] = request.approve()
else:
print("Plan revised by human.\n")
responses[request_id] = request.revise(reply)
return responses if responses else None
async def main() -> None:
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
researcher_agent = Agent(
name="ResearcherAgent",
description="Specialist in research and information gathering",
instructions="You are a Researcher. You find information and gather facts.",
client=client,
)
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=client,
)
manager_agent = Agent(
name="MagenticManager",
description="Orchestrator that coordinates the workflow",
instructions="You coordinate a team to complete tasks efficiently.",
client=client,
)
print("\nBuilding Magentic Workflow with Human Plan Review...")
# enable_plan_review=True: Request human input for plan review.
# Mark participant responses as intermediate so the stream shows the
# conversation as it unfolds while the manager's final answer remains the
# terminal workflow output.
workflow = MagenticBuilder(
participants=[researcher_agent, analyst_agent],
enable_plan_review=True,
intermediate_output_from=[researcher_agent, analyst_agent],
manager_agent=manager_agent,
max_round_count=10,
max_stall_count=1,
max_reset_count=2,
).build()
task = "Research sustainable aviation fuel technology and summarize the findings."
print(f"\nTask: {task}")
print("\nStarting workflow execution...")
print("=" * 60)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run(task, 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,94 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import cast
from agent_framework import Agent, AgentResponse, Message
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: Sequential workflow (agent-focused API) with shared conversation context
Build a high-level sequential workflow using SequentialBuilder and two domain agents.
The shared conversation flows through each participant. Each agent appends its
assistant message to the context. The sample prints the original user message plus
the visible outputs from both agents.
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:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
async def main() -> None:
# 1) Create agents
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
writer = Agent(
client=client,
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
name="writer",
)
reviewer = Agent(
client=client,
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], output_from="all").build()
# 3) Run and collect outputs
prompt = "Write a tagline for a budget-friendly eBike."
result = await workflow.run(prompt)
conversation = [Message(role="user", contents=[prompt])]
for output in result.get_outputs():
response = cast(AgentResponse, output)
conversation.extend(response.messages)
if conversation:
print("===== Final Conversation =====")
for i, msg in enumerate(conversation, start=1):
name = msg.author_name or ("assistant" if msg.role == "assistant" else "user")
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!
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,84 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent, AgentResponseUpdate
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""
Sample: Sequential workflow with chain_only_agent_responses=True
Demonstrates SequentialBuilder with `chain_only_agent_responses=True`, which passes
only the previous agent's response (not the full conversation history) to the next
agent. This is useful when each agent should focus solely on refining or transforming
the prior agent's output without being influenced by earlier turns.
In this sample, a writer agent produces a draft tagline, a translator agent translates
it into French (seeing only the writer's output, not the original user prompt), and a
reviewer agent evaluates the translation (seeing only the translator's output).
Compare with `sequential_agents.py`, which uses the default behavior where the full
conversation context is passed to each agent.
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be the deployment name of a model in your Foundry project.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
# Load environment variables from .env file
load_dotenv()
async def main() -> None:
# 1) Create agents
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
writer = Agent(
client=client,
instructions="You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt.",
name="writer",
)
translator = Agent(
client=client,
instructions="You are a translator. Translate the given text into French. Output only the translation.",
name="translator",
)
reviewer = Agent(
client=client,
instructions="You are a reviewer. Evaluate the quality of the marketing tagline.",
name="reviewer",
)
# 2) Build sequential workflow: writer -> translator -> reviewer
# chain_only_agent_responses=True means each agent sees only the previous agent's reply,
# not the full conversation history.
workflow = SequentialBuilder(
participants=[writer, translator, reviewer],
chain_only_agent_responses=True,
intermediate_output_from=[writer, translator],
).build()
# 3) Run and collect outputs
last_agent: str | None = None
async for event in workflow.run("Write a tagline for a budget-friendly eBike.", stream=True):
if event.type in ("intermediate", "output") and isinstance(event.data, AgentResponseUpdate):
if event.data.author_name != last_agent:
last_agent = event.data.author_name
print()
print(f"{last_agent}: ", end="", flush=True)
print(event.data.text, end="", flush=True)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,107 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import (
Agent,
AgentExecutorResponse,
AgentResponse,
Executor,
Message,
WorkflowContext,
handler,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from typing_extensions import Never
# Load environment variables from .env file
load_dotenv()
"""
Sample: Sequential workflow mixing agents and a custom summarizer executor
This demonstrates how SequentialBuilder chains participants with a shared
conversation context (list[Message]). An agent produces content; a custom
executor synthesizes a compact summary and yields it as the workflow's terminal
output.
Custom executor contract:
- Intermediate custom executors: handle the message type from the prior participant
and forward `list[Message]` via `ctx.send_message(...)` for the next participant.
- Terminator custom executors: handle the message type from the prior participant and
yield the workflow's final answer as an `AgentResponse` via `ctx.yield_output(...)`.
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
class Summarizer(Executor):
"""Terminator custom executor: synthesizes a one-line summary as the workflow's final answer."""
@handler
async def summarize(
self,
agent_response: AgentExecutorResponse,
ctx: WorkflowContext[Never, AgentResponse],
) -> None:
"""Yield a terminal AgentResponse containing the summary.
The prior participant is an agent, which is wrapped in an `AgentExecutor` that
produces `AgentExecutorResponse`. As the last participant in the sequential workflow,
this executor calls `ctx.yield_output(AgentResponse(...))` so its output becomes the
workflow's terminal output (rather than being forwarded to a downstream participant).
"""
if not agent_response.full_conversation:
await ctx.yield_output(AgentResponse(messages=[Message("assistant", ["No conversation to summarize."])]))
return
users = sum(1 for m in agent_response.full_conversation if m.role == "user")
assistants = sum(1 for m in agent_response.full_conversation if m.role == "assistant")
summary = Message("assistant", [f"Summary -> users:{users} assistants:{assistants}"])
await ctx.yield_output(AgentResponse(messages=[summary]))
async def main() -> None:
# 1) Create a content agent
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
content = Agent(
client=client,
instructions="Produce a concise paragraph answering the user's request.",
name="content",
)
# 2) Build sequential workflow: content -> summarizer
summarizer = Summarizer(id="summarizer")
workflow = SequentialBuilder(participants=[content, summarizer]).build()
# 3) Run workflow and extract the final summary
events = await workflow.run("Explain the benefits of budget eBikes for commuters.")
outputs = events.get_outputs()
if outputs:
print("===== Final Summary =====")
final: AgentResponse = outputs[0]
for msg in final.messages:
print(msg.text)
"""
Sample Output:
===== Final Summary =====
Summary -> users:1 assistants:1
"""
if __name__ == "__main__":
asyncio.run(main())