mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: restructure: Python samples into progressive 01-05 layout (#3862)
* restructure: Python samples into progressive 01-05 layout - 01-get-started/: 6 numbered steps (hello agent → hosting) - 02-agents/: all agent concept samples (tools, middleware, providers, etc.) - 03-workflows/: ALL existing workflow samples preserved as-is - 04-hosting/: azure-functions, durabletask, a2a - 05-end-to-end/: demos, evaluation, hosted agents - Old files moved to _to_delete/ for review - Added AGENTS.md with structure documentation - autogen-migration/ and semantic-kernel-migration/ preserved at root * fix: switch to AzureOpenAI Foundry, fix CI failures - Switch all 01-get-started samples to AzureOpenAIResponsesClient with Azure AI Foundry project endpoint (AZURE_AI_PROJECT_ENDPOINT + AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME + AzureCliCredential) - Add _to_delete/ and 05-end-to-end/ to pyrightconfig.samples.json excludes - Fix test paths in packages/ that referenced old getting_started/ dirs: durabletask conftest + streaming test, azurefunctions conftest, devui conftest + capture_messages + openai_sdk_integration - Fix workflow_as_agent_human_in_the_loop.py import (sibling import) - Update hosting READMEs and tool comment paths - Replace root README.md with new structure overview - Update AGENTS.md to document Azure OpenAI Foundry as default provider * cleanup: remove _to_delete folder, copy resource files to active dirs All files in _to_delete/ were either: - Exact duplicates of files in the new structure (240 files) - Same file with only comment path updates (100 files) - One import-fix diff (workflow_as_agent_human_in_the_loop.py) - One superseded minimal_sample.py Resource files (sample.pdf, countries.json, employees.pdf, weather.json) copied to 02-agents/sample_assets/ and 02-agents/resources/ since active samples reference them. * fix: address PR review comments, centralize resources, remove root duplicates - Fix type annotation in 04_memory.py (string union -> proper types) - Fix old sample paths in observability files - Fix grammar/spelling in observability samples - Move sample_assets/ and resources/ to shared/ folder - Remove 8 duplicate observability files from 02-agents root - Update resource path references in multimodal_input and provider samples * fix: update broken links from old getting_started paths to new structure - Update relative paths in READMEs: getting_started/ → 01-get-started/, 02-agents/, 03-workflows/, 04-hosting/, 05-end-to-end/ - Fix absolute GitHub URLs in package READMEs - Fix broken link in ollama package README * fix: convert absolute GitHub URLs to relative paths for link checker Absolute URLs to python/samples/ on main branch 404 until PR merges. Converted to relative paths that linkspector can verify locally. * fix: update link for handoff sample moved to orchestrations/ * fix: update chatkit-integration README path from demos/ to 05-end-to-end/ * fix: update broken links in orchestrations README to match flat directory structure
This commit is contained in:
@@ -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/02-agents/tools/function_tool_with_approval.py
|
||||
# samples/02-agents/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())
|
||||
@@ -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/02-agents/tools/function_tool_with_approval.py
|
||||
# and samples/02-agents/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())
|
||||
@@ -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/02-agents/tools/function_tool_with_approval.py and
|
||||
# samples/02-agents/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())
|
||||
Reference in New Issue
Block a user