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