mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add Handoff orchestration pattern support (#1469)
* Add Handoff orchestration pattern support * PR feedback * Use AOAI client in samples * Adjust to tool * Handoff to sub-agent via ai function * PR feedback * More cleanup * Improvements * PR feedback cleanup * Add handoff migration sample. * Remove type ignore * fix markdown link formatting * Remove readme link for non-existent sample
This commit is contained in:
committed by
GitHub
Unverified
parent
4554de00ab
commit
b66619a544
@@ -77,7 +77,7 @@ async def run_sequential_workflow() -> None:
|
||||
print(f"Starting workflow with input: '{input_text}'")
|
||||
|
||||
output_event = None
|
||||
async for event in workflow.run_stream(input_text):
|
||||
async for event in workflow.run_stream("Hello world"):
|
||||
if isinstance(event, WorkflowOutputEvent):
|
||||
# The WorkflowOutputEvent contains the final result.
|
||||
output_event = event
|
||||
|
||||
@@ -89,6 +89,8 @@ Once comfortable with these, explore the rest of the samples below.
|
||||
| Concurrent Orchestration (Default Aggregator) | [orchestration/concurrent_agents.py](./orchestration/concurrent_agents.py) | Fan-out to multiple agents; fan-in with default aggregator returning combined ChatMessages |
|
||||
| Concurrent Orchestration (Custom Aggregator) | [orchestration/concurrent_custom_aggregator.py](./orchestration/concurrent_custom_aggregator.py) | Override aggregator via callback; summarize results with an LLM |
|
||||
| Concurrent Orchestration (Custom Agent Executors) | [orchestration/concurrent_custom_agent_executors.py](./orchestration/concurrent_custom_agent_executors.py) | Child executors own ChatAgents; concurrent fan-out/fan-in via ConcurrentBuilder |
|
||||
| Handoff (Simple) | [orchestration/handoff_simple.py](./orchestration/handoff_simple.py) | Single-tier routing: triage agent routes to specialists, control returns to user after each specialist response |
|
||||
| Handoff (Specialist-to-Specialist) | [orchestration/handoff_specialist_to_specialist.py](./orchestration/handoff_specialist_to_specialist.py) | Multi-tier routing: specialists can hand off to other specialists using `.add_handoff()` fluent API |
|
||||
| Magentic Workflow (Multi-Agent) | [orchestration/magentic.py](./orchestration/magentic.py) | Orchestrate multiple agents with Magentic manager and streaming |
|
||||
| Magentic + Human Plan Review | [orchestration/magentic_human_plan_update.py](./orchestration/magentic_human_plan_update.py) | Human reviews/updates the plan before execution |
|
||||
| Magentic + Checkpoint Resume | [orchestration/magentic_checkpoint.py](./orchestration/magentic_checkpoint.py) | Resume Magentic orchestration from saved checkpoints |
|
||||
@@ -97,6 +99,11 @@ Once comfortable with these, explore the rest of the samples below.
|
||||
|
||||
**Magentic checkpointing tip**: Treat `MagenticBuilder.participants` keys as stable identifiers. When resuming from a checkpoint, the rebuilt workflow must reuse the same participant names; otherwise the checkpoint cannot be applied and the run will fail fast.
|
||||
|
||||
**Handoff workflow tip**: Handoff workflows maintain the full conversation history including any
|
||||
`ChatMessage.additional_properties` emitted by your agents. This ensures routing metadata remains
|
||||
intact across all agent transitions. For specialist-to-specialist handoffs, use `.add_handoff(source, targets)`
|
||||
to configure which agents can route to which others with a fluent, type-safe API.
|
||||
|
||||
### parallelism
|
||||
|
||||
| Sample | File | Concepts |
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import (
|
||||
ChatAgent,
|
||||
ChatMessage,
|
||||
HandoffBuilder,
|
||||
HandoffUserInputRequest,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
"""Sample: Simple handoff workflow with single-tier triage-to-specialist routing.
|
||||
|
||||
This sample demonstrates the basic handoff pattern where only the triage agent can
|
||||
route to specialists. Specialists cannot hand off to other specialists - after any
|
||||
specialist responds, control returns to the user for the next input.
|
||||
|
||||
Routing Pattern:
|
||||
User → Triage Agent → Specialist → Back to User → Triage Agent → ...
|
||||
|
||||
This is the simplest handoff configuration, suitable for straightforward support
|
||||
scenarios where a triage agent dispatches to domain specialists, and each specialist
|
||||
works independently.
|
||||
|
||||
For multi-tier specialist-to-specialist handoffs, see handoff_specialist_to_specialist.py.
|
||||
|
||||
Prerequisites:
|
||||
- `az login` (Azure CLI authentication)
|
||||
- Environment variables configured for AzureOpenAIChatClient (AZURE_OPENAI_ENDPOINT, etc.)
|
||||
|
||||
Key Concepts:
|
||||
- Single-tier routing: Only triage agent has handoff capabilities
|
||||
- Auto-registered handoff tools: HandoffBuilder creates tools automatically
|
||||
- Termination condition: Controls when the workflow stops requesting user input
|
||||
- Request/response cycle: Workflow requests input, user responds, cycle continues
|
||||
"""
|
||||
|
||||
|
||||
def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAgent, ChatAgent, ChatAgent]:
|
||||
"""Create and configure the triage and specialist agents.
|
||||
|
||||
The triage agent is responsible for:
|
||||
- Receiving all user input first
|
||||
- Deciding whether to handle the request directly or hand off to a specialist
|
||||
- Signaling handoff by calling one of the explicit handoff tools exposed to it
|
||||
|
||||
Specialist agents are invoked only when the triage agent explicitly hands off to them.
|
||||
After a specialist responds, control returns to the triage agent.
|
||||
|
||||
Returns:
|
||||
Tuple of (triage_agent, refund_agent, order_agent, support_agent)
|
||||
"""
|
||||
# Triage agent: Acts as the frontline dispatcher
|
||||
# NOTE: The instructions explicitly tell it to call the correct handoff tool when routing.
|
||||
# The HandoffBuilder intercepts these tool calls and routes to the matching specialist.
|
||||
triage = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are frontline support triage. Read the latest user message and decide whether "
|
||||
"to hand off to refund_agent, order_agent, or support_agent. Provide a brief natural-language "
|
||||
"response for the user. When delegation is required, call the matching handoff tool "
|
||||
"(`handoff_to_refund_agent`, `handoff_to_order_agent`, or `handoff_to_support_agent`)."
|
||||
),
|
||||
name="triage_agent",
|
||||
)
|
||||
|
||||
# Refund specialist: Handles refund requests
|
||||
refund = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You handle refund workflows. Ask for any order identifiers you require and outline the refund steps."
|
||||
),
|
||||
name="refund_agent",
|
||||
)
|
||||
|
||||
# Order/shipping specialist: Resolves delivery issues
|
||||
order = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You resolve shipping and fulfillment issues. Clarify the delivery problem and describe the actions "
|
||||
"you will take to remedy it."
|
||||
),
|
||||
name="order_agent",
|
||||
)
|
||||
|
||||
# General support specialist: Fallback for other issues
|
||||
support = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are a general support agent. Offer empathetic troubleshooting and gather missing details if the "
|
||||
"issue does not match other specialists."
|
||||
),
|
||||
name="support_agent",
|
||||
)
|
||||
|
||||
return triage, refund, order, support
|
||||
|
||||
|
||||
async def _drain(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:
|
||||
"""Collect all events from an async stream into a list.
|
||||
|
||||
This helper drains the workflow's event stream so we can process events
|
||||
synchronously after each workflow step completes.
|
||||
|
||||
Args:
|
||||
stream: Async iterable of WorkflowEvent
|
||||
|
||||
Returns:
|
||||
List of all events from the stream
|
||||
"""
|
||||
return [event async for event in stream]
|
||||
|
||||
|
||||
def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
"""Process workflow events and extract any pending user input requests.
|
||||
|
||||
This function inspects each event type and:
|
||||
- Prints workflow status changes (IDLE, IDLE_WITH_PENDING_REQUESTS, etc.)
|
||||
- Displays final conversation snapshots when workflow completes
|
||||
- Prints user input request prompts
|
||||
- Collects all RequestInfoEvent instances for response handling
|
||||
|
||||
Args:
|
||||
events: List of WorkflowEvent to process
|
||||
|
||||
Returns:
|
||||
List of RequestInfoEvent representing pending user input requests
|
||||
"""
|
||||
requests: list[RequestInfoEvent] = []
|
||||
|
||||
for event in events:
|
||||
# WorkflowStatusEvent: Indicates workflow state changes
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state in {
|
||||
WorkflowRunState.IDLE,
|
||||
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
|
||||
}:
|
||||
print(f"[status] {event.state.name}")
|
||||
|
||||
# WorkflowOutputEvent: Contains the final conversation when workflow terminates
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
conversation = cast(list[ChatMessage], event.data)
|
||||
if isinstance(conversation, list):
|
||||
print("\n=== Final Conversation Snapshot ===")
|
||||
for message in conversation:
|
||||
speaker = message.author_name or message.role.value
|
||||
print(f"- {speaker}: {message.text}")
|
||||
print("===================================")
|
||||
|
||||
# RequestInfoEvent: Workflow is requesting user input
|
||||
elif isinstance(event, RequestInfoEvent):
|
||||
if isinstance(event.data, HandoffUserInputRequest):
|
||||
_print_handoff_request(event.data)
|
||||
requests.append(event)
|
||||
|
||||
return requests
|
||||
|
||||
|
||||
def _print_handoff_request(request: HandoffUserInputRequest) -> None:
|
||||
"""Display a user input request prompt with conversation context.
|
||||
|
||||
The HandoffUserInputRequest contains the full conversation history so far,
|
||||
allowing the user to see what's been discussed before providing their next input.
|
||||
|
||||
Args:
|
||||
request: The user input request containing conversation and prompt
|
||||
"""
|
||||
print("\n=== User Input Requested ===")
|
||||
for message in request.conversation:
|
||||
speaker = message.author_name or message.role.value
|
||||
print(f"- {speaker}: {message.text}")
|
||||
print("============================")
|
||||
|
||||
|
||||
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
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
|
||||
# Create all agents: triage + specialists
|
||||
triage, refund, order, support = create_agents(chat_client)
|
||||
|
||||
# Build the handoff workflow
|
||||
# - participants: All agents that can participate (triage MUST be first or explicitly set as set_coordinator)
|
||||
# - set_coordinator: The triage agent receives all user input first
|
||||
# - with_termination_condition: Custom logic to stop the request/response loop
|
||||
# Default is 10 user messages; here we terminate after 4 to match our scripted demo
|
||||
workflow = (
|
||||
HandoffBuilder(
|
||||
name="customer_support_handoff",
|
||||
participants=[triage, refund, order, support],
|
||||
)
|
||||
.set_coordinator("triage_agent")
|
||||
.with_termination_condition(
|
||||
# Terminate after 4 user messages (initial + 3 scripted responses)
|
||||
# Count only USER role messages to avoid counting agent responses
|
||||
lambda conv: sum(1 for msg in conv if msg.role.value == "user") >= 4
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
# Scripted user responses for reproducible demo
|
||||
# In a console application, replace this with:
|
||||
# user_input = input("Your response: ")
|
||||
# or integrate with a UI/chat interface
|
||||
scripted_responses = [
|
||||
"My order 1234 arrived damaged and the packaging was destroyed.",
|
||||
"Yes, I'd like a refund if that's possible.",
|
||||
"Thanks for resolving this.",
|
||||
]
|
||||
|
||||
# Start the workflow with the initial user message
|
||||
# run_stream() returns an async iterator of WorkflowEvent
|
||||
print("\n[Starting workflow with initial user message...]")
|
||||
events = await _drain(workflow.run_stream("Hello, I need assistance with my recent purchase."))
|
||||
pending_requests = _handle_events(events)
|
||||
|
||||
# Process the request/response cycle
|
||||
# The workflow will continue requesting input until:
|
||||
# 1. The termination condition is met (4 user messages in this case), OR
|
||||
# 2. We run out of scripted responses
|
||||
while pending_requests and scripted_responses:
|
||||
# Get the next scripted response
|
||||
user_response = scripted_responses.pop(0)
|
||||
print(f"\n[User responding: {user_response}]")
|
||||
|
||||
# Send response(s) to all pending requests
|
||||
# In this demo, there's typically one request per cycle, but the API supports multiple
|
||||
responses = {req.request_id: user_response for req in pending_requests}
|
||||
|
||||
# Send responses and get new events
|
||||
events = await _drain(workflow.send_responses_streaming(responses))
|
||||
pending_requests = _handle_events(events)
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
[Starting workflow with initial user message...]
|
||||
|
||||
=== User Input Requested ===
|
||||
- user: Hello, I need assistance with my recent purchase.
|
||||
- triage_agent: I'd be happy to help you with your recent purchase. Could you please provide more details about the issue you're experiencing?
|
||||
============================
|
||||
[status] IDLE_WITH_PENDING_REQUESTS
|
||||
|
||||
[User responding: My order 1234 arrived damaged and the packaging was destroyed.]
|
||||
|
||||
=== User Input Requested ===
|
||||
- user: Hello, I need assistance with my recent purchase.
|
||||
- triage_agent: I'd be happy to help you with your recent purchase. Could you please provide more details about the issue you're experiencing?
|
||||
- user: My order 1234 arrived damaged and the packaging was destroyed.
|
||||
- triage_agent: I'm sorry to hear that your order arrived damaged and the packaging was destroyed. I will connect you with a specialist who can assist you further with this issue.
|
||||
|
||||
Tool Call: handoff_to_support_agent (awaiting approval)
|
||||
- support_agent: I'm so sorry to hear that your order arrived in such poor condition. I'll help you get this sorted out.
|
||||
|
||||
To assist you better, could you please let me know:
|
||||
- Which item(s) from order 1234 arrived damaged?
|
||||
- Could you describe the damage, or provide photos if possible?
|
||||
- Would you prefer a replacement or a refund?
|
||||
|
||||
Once I have this information, I can help resolve this for you as quickly as possible.
|
||||
============================
|
||||
[status] IDLE_WITH_PENDING_REQUESTS
|
||||
|
||||
[User responding: Yes, I'd like a refund if that's possible.]
|
||||
|
||||
=== User Input Requested ===
|
||||
- user: Hello, I need assistance with my recent purchase.
|
||||
- triage_agent: I'd be happy to help you with your recent purchase. Could you please provide more details about the issue you're experiencing?
|
||||
- user: My order 1234 arrived damaged and the packaging was destroyed.
|
||||
- triage_agent: I'm sorry to hear that your order arrived damaged and the packaging was destroyed. I will connect you with a specialist who can assist you further with this issue.
|
||||
|
||||
Tool Call: handoff_to_support_agent (awaiting approval)
|
||||
- support_agent: I'm so sorry to hear that your order arrived in such poor condition. I'll help you get this sorted out.
|
||||
|
||||
To assist you better, could you please let me know:
|
||||
- Which item(s) from order 1234 arrived damaged?
|
||||
- Could you describe the damage, or provide photos if possible?
|
||||
- Would you prefer a replacement or a refund?
|
||||
|
||||
Once I have this information, I can help resolve this for you as quickly as possible.
|
||||
- user: Yes, I'd like a refund if that's possible.
|
||||
- triage_agent: Thank you for letting me know you'd prefer a refund. I'll connect you with a specialist who can process your refund request.
|
||||
|
||||
Tool Call: handoff_to_refund_agent (awaiting approval)
|
||||
- refund_agent: Thank you for confirming that you'd like a refund for order 1234.
|
||||
|
||||
Here's what will happen next:
|
||||
|
||||
...
|
||||
|
||||
Tool Call: handoff_to_refund_agent (awaiting approval)
|
||||
- refund_agent: Thank you for confirming that you'd like a refund for order 1234.
|
||||
|
||||
Here's what will happen next:
|
||||
|
||||
**1. Verification:**
|
||||
I will need to verify a few more details to proceed.
|
||||
- Can you confirm the items in order 1234 that arrived damaged?
|
||||
- Do you have any photos of the damaged items/packaging? (Photos help speed up the process.)
|
||||
|
||||
**2. Refund Request Submission:**
|
||||
- Once I have the details, I will submit your refund request for review.
|
||||
|
||||
**3. Return Instructions (if needed):**
|
||||
- In some cases, we may provide instructions on how to return the damaged items.
|
||||
- You will receive a prepaid return label if necessary.
|
||||
|
||||
**4. Refund Processing:**
|
||||
- After your request is approved (and any returns are received if required), your refund will be processed.
|
||||
- Refunds usually appear on your original payment method within 5-10 business days.
|
||||
|
||||
Could you please reply with the specific item(s) damaged and, if possible, attach photos? This will help me get your refund started right away.
|
||||
- user: Thanks for resolving this.
|
||||
===================================
|
||||
[status] IDLE
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Sample: Multi-tier handoff workflow with specialist-to-specialist routing.
|
||||
|
||||
This sample demonstrates advanced handoff routing where specialist agents can hand off
|
||||
to other specialists, enabling complex multi-tier workflows. Unlike the simple handoff
|
||||
pattern (see handoff_simple.py), specialists here can delegate to other specialists
|
||||
without returning control to the user until the specialist chain completes.
|
||||
|
||||
Routing Pattern:
|
||||
User → Triage → Specialist A → Specialist B → Back to User
|
||||
|
||||
This pattern is useful for complex support scenarios where different specialists need
|
||||
to collaborate or escalate to each other before returning to the user. For example:
|
||||
- Replacement agent needs shipping info → hands off to delivery agent
|
||||
- Technical support needs billing info → hands off to billing agent
|
||||
- Level 1 support escalates to Level 2 → hands off to escalation agent
|
||||
|
||||
Configuration uses `.add_handoff()` to explicitly define the routing graph.
|
||||
|
||||
Prerequisites:
|
||||
- `az login` (Azure CLI authentication)
|
||||
- Environment variables configured for AzureOpenAIChatClient
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import (
|
||||
ChatMessage,
|
||||
HandoffBuilder,
|
||||
HandoffUserInputRequest,
|
||||
RequestInfoEvent,
|
||||
WorkflowEvent,
|
||||
WorkflowOutputEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
|
||||
def create_agents(chat_client: AzureOpenAIChatClient):
|
||||
"""Create triage and specialist agents with multi-tier handoff capabilities.
|
||||
|
||||
Returns:
|
||||
Tuple of (triage_agent, replacement_agent, delivery_agent, billing_agent)
|
||||
"""
|
||||
triage = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You are a customer support triage agent. Assess the user's issue and route appropriately:\n"
|
||||
"- For product replacement issues: call handoff_to_replacement_agent\n"
|
||||
"- For delivery/shipping inquiries: call handoff_to_delivery_agent\n"
|
||||
"- For billing/payment issues: call handoff_to_billing_agent\n"
|
||||
"Be concise and friendly."
|
||||
),
|
||||
name="triage_agent",
|
||||
)
|
||||
|
||||
replacement = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You handle product replacement requests. Ask for order number and reason for replacement.\n"
|
||||
"If the user also needs shipping/delivery information, call handoff_to_delivery_agent to "
|
||||
"get tracking details. Otherwise, process the replacement and confirm with the user.\n"
|
||||
"Be concise and helpful."
|
||||
),
|
||||
name="replacement_agent",
|
||||
)
|
||||
|
||||
delivery = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You handle shipping and delivery inquiries. Provide tracking information, estimated "
|
||||
"delivery dates, and address any delivery concerns.\n"
|
||||
"If billing issues come up, call handoff_to_billing_agent.\n"
|
||||
"Be concise and clear."
|
||||
),
|
||||
name="delivery_agent",
|
||||
)
|
||||
|
||||
billing = chat_client.create_agent(
|
||||
instructions=(
|
||||
"You handle billing and payment questions. Help with refunds, payment methods, "
|
||||
"and invoice inquiries. Be concise."
|
||||
),
|
||||
name="billing_agent",
|
||||
)
|
||||
|
||||
return triage, replacement, delivery, billing
|
||||
|
||||
|
||||
async def _drain(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]:
|
||||
"""Collect all events from an async stream into a list."""
|
||||
return [event async for event in stream]
|
||||
|
||||
|
||||
def _handle_events(events: list[WorkflowEvent]) -> list[RequestInfoEvent]:
|
||||
"""Process workflow events and extract pending user input requests."""
|
||||
requests: list[RequestInfoEvent] = []
|
||||
|
||||
for event in events:
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state in {
|
||||
WorkflowRunState.IDLE,
|
||||
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
|
||||
}:
|
||||
print(f"[status] {event.state.name}")
|
||||
|
||||
elif isinstance(event, WorkflowOutputEvent):
|
||||
conversation = cast(list[ChatMessage], event.data)
|
||||
if isinstance(conversation, list):
|
||||
print("\n=== Final Conversation ===")
|
||||
for message in conversation:
|
||||
# Filter out messages with no text (tool calls)
|
||||
if not message.text.strip():
|
||||
continue
|
||||
speaker = message.author_name or message.role.value
|
||||
print(f"- {speaker}: {message.text}")
|
||||
print("==========================")
|
||||
|
||||
elif isinstance(event, RequestInfoEvent):
|
||||
if isinstance(event.data, HandoffUserInputRequest):
|
||||
_print_handoff_request(event.data)
|
||||
requests.append(event)
|
||||
|
||||
return requests
|
||||
|
||||
|
||||
def _print_handoff_request(request: HandoffUserInputRequest) -> None:
|
||||
"""Display a user input request with conversation context."""
|
||||
print("\n=== User Input Requested ===")
|
||||
# Filter out messages with no text for cleaner display
|
||||
messages_with_text = [msg for msg in request.conversation if msg.text.strip()]
|
||||
print(f"Last {len(messages_with_text)} messages in conversation:")
|
||||
for message in messages_with_text[-5:]: # Show last 5 for brevity
|
||||
speaker = message.author_name or message.role.value
|
||||
text = message.text[:100] + "..." if len(message.text) > 100 else message.text
|
||||
print(f" {speaker}: {text}")
|
||||
print("============================")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Demonstrate specialist-to-specialist handoffs in a multi-tier support scenario.
|
||||
|
||||
This sample shows:
|
||||
1. Triage agent routes to replacement specialist
|
||||
2. Replacement specialist hands off to delivery specialist
|
||||
3. Delivery specialist can hand off to billing if needed
|
||||
4. All transitions are seamless without returning to user until complete
|
||||
|
||||
The workflow configuration explicitly defines which agents can hand off to which others:
|
||||
- triage_agent → replacement_agent, delivery_agent, billing_agent
|
||||
- replacement_agent → delivery_agent, billing_agent
|
||||
- delivery_agent → billing_agent
|
||||
"""
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
triage, replacement, delivery, billing = create_agents(chat_client)
|
||||
|
||||
# Configure multi-tier handoffs using fluent add_handoff() API
|
||||
# This allows specialists to hand off to other specialists
|
||||
workflow = (
|
||||
HandoffBuilder(
|
||||
name="multi_tier_support",
|
||||
participants=[triage, replacement, delivery, billing],
|
||||
)
|
||||
.set_coordinator(triage)
|
||||
.add_handoff(triage, [replacement, delivery, billing]) # Triage can route to any specialist
|
||||
.add_handoff(replacement, [delivery, billing]) # Replacement can delegate to delivery or billing
|
||||
.add_handoff(delivery, billing) # Delivery can escalate to billing
|
||||
# Termination condition: Stop when more than 4 user messages exist.
|
||||
# This allows agents to respond to the 4th user message before the 5th triggers termination.
|
||||
# In this sample: initial message + 3 scripted responses = 4 messages, then 5th message ends workflow.
|
||||
.with_termination_condition(lambda conv: sum(1 for msg in conv if msg.role.value == "user") > 4)
|
||||
.build()
|
||||
)
|
||||
|
||||
# Scripted user responses simulating a multi-tier handoff scenario
|
||||
# Note: The initial run_stream() call sends the first user message,
|
||||
# then these scripted responses are sent in sequence (total: 4 user messages).
|
||||
# A 5th response triggers termination after agents respond to the 4th message.
|
||||
scripted_responses = [
|
||||
"I need help with order 12345. I want a replacement and need to know when it will arrive.",
|
||||
"The item arrived damaged. I'd like a replacement shipped to the same address.",
|
||||
"Great! Can you confirm the shipping cost won't be charged again?",
|
||||
"Thank you!", # Final response to trigger termination after billing agent answers
|
||||
]
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("SPECIALIST-TO-SPECIALIST HANDOFF DEMONSTRATION")
|
||||
print("=" * 80)
|
||||
print("\nScenario: Customer needs replacement + shipping info + billing confirmation")
|
||||
print("Expected flow: User → Triage → Replacement → Delivery → Billing → User")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
# Start workflow with initial message
|
||||
print("[User]: I need help with order 12345. I want a replacement and need to know when it will arrive.\n")
|
||||
events = await _drain(
|
||||
workflow.run_stream("I need help with order 12345. I want a replacement and need to know when it will arrive.")
|
||||
)
|
||||
pending_requests = _handle_events(events)
|
||||
|
||||
# Process scripted responses
|
||||
response_index = 0
|
||||
while pending_requests and response_index < len(scripted_responses):
|
||||
user_response = scripted_responses[response_index]
|
||||
print(f"\n[User]: {user_response}\n")
|
||||
|
||||
responses = {req.request_id: user_response for req in pending_requests}
|
||||
events = await _drain(workflow.send_responses_streaming(responses))
|
||||
pending_requests = _handle_events(events)
|
||||
|
||||
response_index += 1
|
||||
|
||||
"""
|
||||
Sample Output:
|
||||
|
||||
================================================================================
|
||||
SPECIALIST-TO-SPECIALIST HANDOFF DEMONSTRATION
|
||||
================================================================================
|
||||
|
||||
Scenario: Customer needs replacement + shipping info + billing confirmation
|
||||
Expected flow: User → Triage → Replacement → Delivery → Billing → User
|
||||
================================================================================
|
||||
|
||||
[User]: I need help with order 12345. I want a replacement and need to know when it will arrive.
|
||||
|
||||
|
||||
=== User Input Requested ===
|
||||
Last 5 messages in conversation:
|
||||
user: I need help with order 12345. I want a replacement and need to know when it will arrive.
|
||||
triage_agent: I'm connecting you to our replacement team to assist with your request, and to our delivery team for...
|
||||
replacement_agent: To assist with your replacement for order 12345 and provide tracking details for delivery, I've reac...
|
||||
delivery_agent: I'm handing over your request for a replacement of order 12345, as well as your inquiry about estima...
|
||||
billing_agent: I handle billing and payment questions. For replacement and delivery details for order 12345, please...
|
||||
============================
|
||||
[status] IDLE_WITH_PENDING_REQUESTS
|
||||
|
||||
[User]: I need help with order 12345. I want a replacement and need to know when it will arrive.
|
||||
|
||||
|
||||
=== User Input Requested ===
|
||||
Last 7 messages in conversation:
|
||||
replacement_agent: To assist with your replacement for order 12345 and provide tracking details for delivery, I've reac...
|
||||
delivery_agent: I'm handing over your request for a replacement of order 12345, as well as your inquiry about estima...
|
||||
billing_agent: I handle billing and payment questions. For replacement and delivery details for order 12345, please...
|
||||
user: I need help with order 12345. I want a replacement and need to know when it will arrive.
|
||||
triage_agent: I'm connecting you with our replacement team to help with your request, and our delivery team for in...
|
||||
============================
|
||||
[status] IDLE_WITH_PENDING_REQUESTS
|
||||
|
||||
[User]: The item arrived damaged. I'd like a replacement shipped to the same address.
|
||||
|
||||
|
||||
=== User Input Requested ===
|
||||
Last 12 messages in conversation:
|
||||
user: The item arrived damaged. I'd like a replacement shipped to the same address.
|
||||
triage_agent: Thank you for letting us know your item from order 12345 arrived damaged. Im connecting you to our ...
|
||||
replacement_agent: Your replacement request for order 12345 (damaged item) will be processed, and shipped to your curre...
|
||||
delivery_agent: I've forwarded your request for a replacement of the damaged item from order 12345 to our team and a...
|
||||
billing_agent: Thank you for the information. I've forwarded your request to our support team to arrange a replacem...
|
||||
============================
|
||||
[status] IDLE_WITH_PENDING_REQUESTS
|
||||
|
||||
[User]: Great! Can you confirm the shipping cost won't be charged again?
|
||||
|
||||
|
||||
=== Final Conversation ===
|
||||
- user: I need help with order 12345. I want a replacement and need to know when it will arrive.
|
||||
- triage_agent: I'm connecting you to our replacement team to assist with your request, and to our delivery team for updates on when your replacement will arrive. They'll help you shortly!
|
||||
- replacement_agent: To assist with your replacement for order 12345 and provide tracking details for delivery, I've reached out to our teams. Can you please provide the reason for your replacement request? This will help to process things faster.
|
||||
- delivery_agent: I'm handing over your request for a replacement of order 12345, as well as your inquiry about estimated delivery timing, to the appropriate team. They'll assist you further with the replacement and delivery details very soon.
|
||||
- billing_agent: I handle billing and payment questions. For replacement and delivery details for order 12345, please contact our support team or the order department. Is there a billing or payment issue I can help you with?
|
||||
- user: I need help with order 12345. I want a replacement and need to know when it will arrive.
|
||||
- triage_agent: I'm connecting you with our replacement team to help with your request, and our delivery team for information about when your replacement will arrive. They'll assist you shortly!
|
||||
- user: The item arrived damaged. I'd like a replacement shipped to the same address.
|
||||
- triage_agent: Thank you for letting us know your item from order 12345 arrived damaged. I'm connecting you to our replacement team to arrange a replacement shipped to your address, and to our delivery team to update you on the arrival date of your replacement. They'll assist you shortly!
|
||||
- replacement_agent: Your replacement request for order 12345 (damaged item) will be processed, and shipped to your current address. The delivery team will also provide tracking and estimated arrival details for your new item soon.
|
||||
- delivery_agent: I've forwarded your request for a replacement of the damaged item from order 12345 to our team and asked them to ship it to your current address. You'll also receive tracking information and an estimated delivery date for the replacement soon.
|
||||
- billing_agent: Thank you for the information. I've forwarded your request to our support team to arrange a replacement for the damaged item from order 12345. Your replacement will be shipped to the same address, and you'll receive delivery updates soon. If you need a refund instead or have any billing questions, please let me know.
|
||||
- user: Great! Can you confirm the shipping cost won't be charged again?
|
||||
==========================
|
||||
[status] IDLE
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user