mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: extend HITL support for all orchestration patterns (#2620)
* Support HITL for orchestration patterns * Cleanup around naming * Fix typing issues * Clean up * Naming clean up * Updates to HITL to make it cleaner * Rename human input hook to orchestration request info * Clean up per PR feedback
This commit is contained in:
committed by
GitHub
Unverified
parent
0d9ae1920d
commit
b378ca75d1
+183
@@ -0,0 +1,183 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
ConcurrentBuilder,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionApprovalResponseContent,
|
||||
RequestInfoEvent,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
"""
|
||||
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. One agent has a tool requiring approval (financial transaction).
|
||||
3. The other agent has only non-approval tools (market data lookup).
|
||||
4. Both agents receive the same task and work concurrently.
|
||||
5. When the financial agent tries to execute a trade, it triggers an approval request.
|
||||
6. The sample simulates human approval and the workflow completes.
|
||||
7. Results from both agents are aggregated and output.
|
||||
|
||||
Purpose:
|
||||
Show how tool call approvals work in parallel execution scenarios where only some
|
||||
agents have sensitive tools.
|
||||
|
||||
Demonstrate:
|
||||
- Combining agents with and without approval-required tools in concurrent workflows.
|
||||
- Handling RequestInfoEvent during concurrent agent execution.
|
||||
- Understanding that approval pauses only the agent that triggered it, not all agents.
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI or Azure OpenAI configured with the required environment variables.
|
||||
- Basic familiarity with ConcurrentBuilder and streaming workflow events.
|
||||
"""
|
||||
|
||||
|
||||
# 1. Define tools for the research agent (no approval required)
|
||||
@ai_function
|
||||
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}"
|
||||
|
||||
|
||||
@ai_function
|
||||
def get_market_sentiment(symbol: Annotated[str, "The stock ticker symbol"]) -> str:
|
||||
"""Get market sentiment analysis for a stock."""
|
||||
# Mock sentiment data
|
||||
return f"Market sentiment for {symbol.upper()}: Bullish (72% positive mentions in last 24h)"
|
||||
|
||||
|
||||
# 2. Define tools for the trading agent (approval required for trades)
|
||||
@ai_function(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()}"
|
||||
|
||||
|
||||
@ai_function
|
||||
def get_portfolio_balance() -> str:
|
||||
"""Get current portfolio balance and available funds."""
|
||||
return "Portfolio: $50,000 invested, $10,000 cash available"
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 3. Create two agents with different tool sets
|
||||
chat_client = OpenAIChatClient()
|
||||
|
||||
research_agent = chat_client.create_agent(
|
||||
name="ResearchAgent",
|
||||
instructions=(
|
||||
"You are a market research analyst. Analyze stock data and provide "
|
||||
"recommendations based on price and sentiment. Do not execute trades."
|
||||
),
|
||||
tools=[get_stock_price, get_market_sentiment],
|
||||
)
|
||||
|
||||
trading_agent = chat_client.create_agent(
|
||||
name="TradingAgent",
|
||||
instructions=(
|
||||
"You are a trading assistant. When asked to buy or sell shares, you MUST "
|
||||
"call the execute_trade function to complete the transaction. Check portfolio "
|
||||
"balance first, then execute the requested trade."
|
||||
),
|
||||
tools=[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([research_agent, trading_agent]).build()
|
||||
|
||||
# 5. Start the workflow - both agents will process the same task in parallel
|
||||
print("Starting concurrent workflow with tool approval...")
|
||||
print("Two agents will analyze MSFT - one for research, one for trading.")
|
||||
print("-" * 60)
|
||||
|
||||
# Phase 1: Run workflow and collect all events (stream ends at IDLE or IDLE_WITH_PENDING_REQUESTS)
|
||||
request_info_events: list[RequestInfoEvent] = []
|
||||
workflow_completed_without_approvals = False
|
||||
async for event in workflow.run_stream("Analyze MSFT stock and if sentiment is positive, buy 10 shares."):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
request_info_events.append(event)
|
||||
if isinstance(event.data, FunctionApprovalRequestContent):
|
||||
print(f"\nApproval requested for tool: {event.data.function_call.name}")
|
||||
print(f" Arguments: {event.data.function_call.arguments}")
|
||||
elif isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
workflow_completed_without_approvals = True
|
||||
|
||||
# 6. Handle approval requests (if any)
|
||||
if request_info_events:
|
||||
responses: dict[str, FunctionApprovalResponseContent] = {}
|
||||
for request_event in request_info_events:
|
||||
if isinstance(request_event.data, FunctionApprovalRequestContent):
|
||||
print(f"\nSimulating human approval for: {request_event.data.function_call.name}")
|
||||
# Create approval response
|
||||
responses[request_event.request_id] = request_event.data.create_response(approved=True)
|
||||
|
||||
if responses:
|
||||
# Phase 2: Send all approvals and continue workflow
|
||||
output: list[ChatMessage] | None = None
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
output = event.data
|
||||
|
||||
if output:
|
||||
print("\n" + "-" * 60)
|
||||
print("Workflow completed. Aggregated results from both agents:")
|
||||
for msg in output:
|
||||
if hasattr(msg, "author_name") and msg.author_name:
|
||||
print(f"\n[{msg.author_name}]:")
|
||||
text = msg.text[:300] + "..." if len(msg.text) > 300 else msg.text
|
||||
if text:
|
||||
print(f" {text}")
|
||||
elif workflow_completed_without_approvals:
|
||||
print("\nWorkflow completed without requiring approvals.")
|
||||
print("(The trading agent may have only checked balance without executing a trade)")
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
Starting concurrent workflow with tool approval...
|
||||
Two agents will analyze MSFT - one for research, one for trading.
|
||||
------------------------------------------------------------
|
||||
|
||||
Approval requested for tool: execute_trade
|
||||
Arguments: {"symbol": "MSFT", "action": "buy", "quantity": 10}
|
||||
Simulating human approval for: execute_trade
|
||||
|
||||
------------------------------------------------------------
|
||||
Workflow completed. Aggregated results from both agents:
|
||||
|
||||
[ResearchAgent]:
|
||||
MSFT is currently trading at $175.50 with bullish market sentiment
|
||||
(72% positive mentions). Based on the positive sentiment, this could
|
||||
be a good opportunity to consider buying.
|
||||
|
||||
[TradingAgent]:
|
||||
I've checked your portfolio balance ($10,000 cash available) and
|
||||
executed the trade: BUY 10 shares of MSFT at approximately $175.50
|
||||
per share, totaling ~$1,755.
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import (
|
||||
FunctionApprovalRequestContent,
|
||||
GroupChatBuilder,
|
||||
GroupChatStateSnapshot,
|
||||
RequestInfoEvent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
"""
|
||||
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 RequestInfoEvent in group chat scenarios.
|
||||
- Multi-round group chat with tool approval interruption and resumption.
|
||||
|
||||
Prerequisites:
|
||||
- OpenAI or Azure OpenAI configured with the required environment variables.
|
||||
- Basic familiarity with GroupChatBuilder and streaming workflow events.
|
||||
"""
|
||||
|
||||
|
||||
# 1. Define tools for different agents
|
||||
@ai_function
|
||||
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"
|
||||
|
||||
|
||||
@ai_function
|
||||
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"
|
||||
|
||||
|
||||
@ai_function(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}"
|
||||
|
||||
|
||||
@ai_function
|
||||
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: GroupChatStateSnapshot) -> str | None:
|
||||
"""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)
|
||||
"""
|
||||
round_index: int = state["round_index"]
|
||||
|
||||
# Define the conversation flow
|
||||
speaker_order: list[str] = [
|
||||
"QAEngineer", # Round 0: Run tests
|
||||
"DevOpsEngineer", # Round 1: Check staging, create rollback
|
||||
"DevOpsEngineer", # Round 2: Deploy to production (approval required)
|
||||
]
|
||||
|
||||
if round_index >= len(speaker_order):
|
||||
return None # End the conversation
|
||||
|
||||
return speaker_order[round_index]
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# 3. Create specialized agents
|
||||
chat_client = OpenAIChatClient()
|
||||
|
||||
qa_engineer = chat_client.create_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 = chat_client.create_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
|
||||
workflow = (
|
||||
GroupChatBuilder()
|
||||
# Optionally, use `.set_manager(...)` to customize the group chat manager
|
||||
.set_select_speakers_func(select_next_speaker)
|
||||
.participants([qa_engineer, devops_engineer])
|
||||
.with_max_rounds(5)
|
||||
.build()
|
||||
)
|
||||
|
||||
# 5. Start the workflow
|
||||
print("Starting group chat workflow for software deployment...")
|
||||
print("Agents: QA Engineer, DevOps Engineer")
|
||||
print("-" * 60)
|
||||
|
||||
# Phase 1: Run workflow and collect all events (stream ends at IDLE or IDLE_WITH_PENDING_REQUESTS)
|
||||
request_info_events: list[RequestInfoEvent] = []
|
||||
async for event in workflow.run_stream(
|
||||
"We need to deploy version 2.4.0 to production. Please coordinate the deployment."
|
||||
):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
request_info_events.append(event)
|
||||
if isinstance(event.data, FunctionApprovalRequestContent):
|
||||
print("\n[APPROVAL REQUIRED]")
|
||||
print(f" Tool: {event.data.function_call.name}")
|
||||
print(f" Arguments: {event.data.function_call.arguments}")
|
||||
|
||||
# 6. Handle approval requests
|
||||
if request_info_events:
|
||||
for request_event in request_info_events:
|
||||
if isinstance(request_event.data, FunctionApprovalRequestContent):
|
||||
print("\n" + "=" * 60)
|
||||
print("Human review required for production deployment!")
|
||||
print("In a real scenario, you would review the deployment details here.")
|
||||
print("Simulating approval for demo purposes...")
|
||||
print("=" * 60)
|
||||
|
||||
# Create approval response
|
||||
approval_response = request_event.data.create_response(approved=True)
|
||||
|
||||
# Phase 2: Send approval and continue workflow
|
||||
async for _ in workflow.send_responses_streaming({request_event.request_id: approval_response}):
|
||||
pass # Consume all events
|
||||
|
||||
print("\n" + "-" * 60)
|
||||
print("Deployment workflow completed successfully!")
|
||||
print("All agents have finished their tasks.")
|
||||
else:
|
||||
print("\nWorkflow completed without requiring production deployment approval.")
|
||||
|
||||
"""
|
||||
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())
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
FunctionApprovalRequestContent,
|
||||
RequestInfoEvent,
|
||||
SequentialBuilder,
|
||||
WorkflowOutputEvent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
"""
|
||||
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 @ai_function 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 FunctionApprovalRequestContent, pausing the workflow.
|
||||
4. The sample simulates human approval by responding to the RequestInfoEvent.
|
||||
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 @ai_function(approval_mode="always_require") for sensitive operations.
|
||||
- Handling RequestInfoEvent with FunctionApprovalRequestContent in sequential workflows.
|
||||
- Resuming workflow execution after approval via send_responses_streaming.
|
||||
|
||||
Prerequisites:
|
||||
- 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
|
||||
@ai_function(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}'"
|
||||
|
||||
|
||||
@ai_function
|
||||
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 main() -> None:
|
||||
# 2. Create the agent with tools (approval mode is set per-tool via decorator)
|
||||
chat_client = OpenAIChatClient()
|
||||
database_agent = chat_client.create_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)
|
||||
|
||||
# Phase 1: Run workflow and collect all events (stream ends at IDLE or IDLE_WITH_PENDING_REQUESTS)
|
||||
request_info_events: list[RequestInfoEvent] = []
|
||||
async for event in workflow.run_stream(
|
||||
"Check the schema and then update all orders with status 'pending' to 'processing'"
|
||||
):
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
request_info_events.append(event)
|
||||
if isinstance(event.data, FunctionApprovalRequestContent):
|
||||
print(f"\nApproval requested for tool: {event.data.function_call.name}")
|
||||
print(f" Arguments: {event.data.function_call.arguments}")
|
||||
|
||||
# 5. Handle approval requests
|
||||
if request_info_events:
|
||||
for request_event in request_info_events:
|
||||
if isinstance(request_event.data, FunctionApprovalRequestContent):
|
||||
# In a real application, you would prompt the user here
|
||||
print("\nSimulating human approval (auto-approving for demo)...")
|
||||
|
||||
# Create approval response
|
||||
approval_response = request_event.data.create_response(approved=True)
|
||||
|
||||
# Phase 2: Send approval and continue workflow
|
||||
output: list[ChatMessage] | None = None
|
||||
async for event in workflow.send_responses_streaming({request_event.request_id: approval_response}):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
output = event.data
|
||||
|
||||
if output:
|
||||
print("\n" + "-" * 60)
|
||||
print("Workflow completed. Final conversation:")
|
||||
for msg in output:
|
||||
role = msg.role.value if hasattr(msg.role, "value") else msg.role
|
||||
text = msg.text[:200] + "..." if len(msg.text) > 200 else msg.text
|
||||
print(f" [{role}]: {text}")
|
||||
else:
|
||||
print("No approval requests were generated (schema check may have been sufficient).")
|
||||
|
||||
"""
|
||||
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