Skip to content

Commit 67eb5df

Browse files
committed
Python: Add handoff sample for Azure AI Agent Service with pre-registered tools
Add sample demonstrating create_handoff_tools() usage with AzureAIProjectAgentProvider, showing how to pre-register handoff tools at agent creation time for services that don't support runtime tool registration.
1 parent 1457e40 commit 67eb5df

2 files changed

Lines changed: 245 additions & 0 deletions

File tree

python/samples/getting_started/workflows/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Once comfortable with these, explore the rest of the samples below.
4646
| Workflow as Agent with Thread | [agents/workflow_as_agent_with_thread.py](./agents/workflow_as_agent_with_thread.py) | Use AgentThread to maintain conversation history across workflow-as-agent invocations |
4747
| Workflow as Agent kwargs | [agents/workflow_as_agent_kwargs.py](./agents/workflow_as_agent_kwargs.py) | Pass custom context (data, user tokens) via kwargs through workflow.as_agent() to @ai_function tools |
4848
| Handoff Workflow as Agent | [agents/handoff_workflow_as_agent.py](./agents/handoff_workflow_as_agent.py) | Use a HandoffBuilder workflow as an agent with HITL via FunctionCallContent/FunctionResultContent |
49+
| Azure AI Handoff (Pre-Registered Tools) | [agents/handoff_with_azure_ai_agents.py](./agents/handoff_with_azure_ai_agents.py) | Use create_handoff_tools() to pre-register handoff tools at agent creation time for Azure AI Agent Service |
4950

5051
### checkpoint
5152

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
import asyncio
4+
from typing import Annotated, cast
5+
6+
from agent_framework import (
7+
AgentResponse,
8+
Message,
9+
WorkflowEvent,
10+
WorkflowRunState,
11+
tool,
12+
)
13+
from agent_framework.azure import AzureAIProjectAgentProvider
14+
from agent_framework.orchestrations import (
15+
HandoffAgentUserRequest,
16+
HandoffBuilder,
17+
create_handoff_tools,
18+
)
19+
from azure.identity.aio import AzureCliCredential
20+
21+
"""Sample: Handoff Workflow with Azure AI Agent Service using Pre-Registered Tools.
22+
23+
Azure AI Agent Service requires tools to be registered at agent creation time, not
24+
dynamically at request time. This sample demonstrates how to use create_handoff_tools()
25+
to pre-create handoff tools and pass them to provider.create_agent(), enabling the
26+
handoff workflow pattern with Azure AI agents.
27+
28+
Prerequisites:
29+
- Azure AI Agent Service configured with required environment variables
30+
(AZURE_AI_PROJECT_ENDPOINT, AZURE_AI_MODEL_DEPLOYMENT_NAME)
31+
- `az login` (Azure CLI authentication)
32+
33+
Key Concepts:
34+
- create_handoff_tools(): Creates handoff tools upfront for agent creation
35+
- Pre-registration pattern: Tools passed to provider.create_agent(tools=...)
36+
- Duplicate handling: HandoffBuilder gracefully skips pre-registered tools
37+
instead of raising ValueError
38+
"""
39+
40+
41+
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
42+
# See:
43+
# samples/getting_started/tools/function_tool_with_approval.py
44+
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
45+
@tool(approval_mode="never_require")
46+
def process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str:
47+
"""Simulated function to process a refund for a given order number."""
48+
return f"Refund processed successfully for order {order_number}."
49+
50+
51+
@tool(approval_mode="never_require")
52+
def check_order_status(order_number: Annotated[str, "Order number to check status for"]) -> str:
53+
"""Simulated function to check the status of a given order number."""
54+
return f"Order {order_number} is currently being processed and will ship in 2 business days."
55+
56+
57+
def _handle_events(events: list[WorkflowEvent]) -> list[WorkflowEvent[HandoffAgentUserRequest]]:
58+
"""Process workflow events and extract any pending user input requests.
59+
60+
Args:
61+
events: List of WorkflowEvent to process
62+
63+
Returns:
64+
List of WorkflowEvent[HandoffAgentUserRequest] representing pending user input requests
65+
"""
66+
requests: list[WorkflowEvent[HandoffAgentUserRequest]] = []
67+
68+
for event in events:
69+
if event.type == "handoff_sent":
70+
print(f"\n[Handoff from {event.data.source} to {event.data.target} initiated.]")
71+
elif event.type == "status" and event.state in {
72+
WorkflowRunState.IDLE,
73+
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
74+
}:
75+
print(f"\n[Workflow Status] {event.state}")
76+
elif event.type == "output":
77+
data = event.data
78+
if isinstance(data, AgentResponse):
79+
for message in data.messages:
80+
if not message.text:
81+
continue
82+
speaker = message.author_name or message.role
83+
print(f"- {speaker}: {message.text}")
84+
elif isinstance(data, list):
85+
conversation = cast(list[Message], data)
86+
print("\n=== Final Conversation Snapshot ===")
87+
for message in conversation:
88+
speaker = message.author_name or message.role
89+
print(f"- {speaker}: {message.text or [content.type for content in message.contents]}")
90+
print("===================================")
91+
elif event.type == "request_info" and isinstance(event.data, HandoffAgentUserRequest):
92+
_print_handoff_agent_user_request(event.data.agent_response)
93+
requests.append(cast(WorkflowEvent[HandoffAgentUserRequest], event))
94+
95+
return requests
96+
97+
98+
def _print_handoff_agent_user_request(response: AgentResponse) -> None:
99+
"""Display the agent's response messages when requesting user input."""
100+
if not response.messages:
101+
raise RuntimeError("Cannot print agent responses: response has no messages.")
102+
103+
print("\n[Agent is requesting your input...]")
104+
for message in response.messages:
105+
if not message.text:
106+
continue
107+
speaker = message.author_name or message.role
108+
print(f"- {speaker}: {message.text}")
109+
110+
111+
async def main() -> None:
112+
"""Main entry point for the Azure AI handoff workflow demo.
113+
114+
This function demonstrates:
115+
1. Using create_handoff_tools() to pre-create handoff tools for Azure AI Agent Service
116+
2. Creating agents with pre-registered handoff tools via provider.create_agent()
117+
3. Building a handoff workflow where HandoffBuilder skips pre-registered tools
118+
4. Running the workflow with scripted user responses
119+
"""
120+
async with (
121+
AzureCliCredential() as credential,
122+
AzureAIProjectAgentProvider(credential=credential) as provider,
123+
):
124+
# ============================================================
125+
# KEY PATTERN: Pre-create handoff tools BEFORE creating agents
126+
# ============================================================
127+
# Azure AI Agent Service requires tools at agent creation time.
128+
# create_handoff_tools() generates the same tools that HandoffBuilder
129+
# would auto-register, but upfront so they can be passed to
130+
# provider.create_agent().
131+
# NOTE: Azure AI Agent Service requires agent names to use only
132+
# alphanumeric characters and hyphens (no underscores).
133+
specialist_ids = ["refund-agent", "order-agent"]
134+
triage_handoff_tools = create_handoff_tools(
135+
specialist_ids,
136+
descriptions={
137+
"refund-agent": "Transfer to refund specialist for processing refunds.",
138+
"order-agent": "Transfer to order specialist for shipping and order inquiries.",
139+
},
140+
)
141+
142+
# Create triage agent with BOTH handoff tools and no domain tools.
143+
# The handoff tools are pre-registered at creation time.
144+
triage = await provider.create_agent(
145+
instructions=(
146+
"You are frontline support triage. Route customer issues to the appropriate specialist agents "
147+
"based on the problem described."
148+
),
149+
name="triage-agent",
150+
tools=triage_handoff_tools,
151+
)
152+
153+
# Create specialist agents with their domain-specific tools.
154+
# Specialists don't need handoff tools pre-registered because they
155+
# don't route to other agents in this example.
156+
refund = await provider.create_agent(
157+
instructions="You process refund requests.",
158+
name="refund-agent",
159+
tools=[process_refund],
160+
)
161+
162+
order = await provider.create_agent(
163+
instructions="You handle order and shipping inquiries.",
164+
name="order-agent",
165+
tools=[check_order_status],
166+
)
167+
168+
# Build the handoff workflow.
169+
# HandoffBuilder will detect that triage already has handoff tools
170+
# pre-registered and skip them instead of raising ValueError.
171+
workflow = (
172+
HandoffBuilder(
173+
name="azure_ai_customer_support",
174+
participants=[triage, refund, order],
175+
termination_condition=lambda conversation: (
176+
len(conversation) > 0 and "welcome" in conversation[-1].text.lower()
177+
),
178+
)
179+
.with_start_agent(triage)
180+
.build()
181+
)
182+
183+
# Scripted user responses for reproducible demo.
184+
# In a real application, replace with actual user input collection.
185+
scripted_responses = [
186+
"My order 1234 arrived damaged and I'd like a refund.",
187+
"Thanks for resolving this.",
188+
]
189+
190+
# Start the workflow with the initial user message
191+
print("[Starting Azure AI handoff workflow with pre-registered tools...]\n")
192+
initial_message = "Hello, I need assistance with my recent purchase."
193+
print(f"- User: {initial_message}")
194+
workflow_result = workflow.run(initial_message, stream=True)
195+
pending_requests = _handle_events([event async for event in workflow_result])
196+
197+
# Process the request/response cycle
198+
while pending_requests:
199+
if not scripted_responses:
200+
responses = {req.request_id: HandoffAgentUserRequest.terminate() for req in pending_requests}
201+
else:
202+
user_response = scripted_responses.pop(0)
203+
print(f"\n- User: {user_response}")
204+
responses = {
205+
req.request_id: HandoffAgentUserRequest.create_response(user_response) for req in pending_requests
206+
}
207+
208+
events = await workflow.run(responses=responses)
209+
pending_requests = _handle_events(events)
210+
211+
"""
212+
Sample Output:
213+
214+
[Starting Azure AI handoff workflow with pre-registered tools...]
215+
216+
- User: Hello, I need assistance with my recent purchase.
217+
- triage-agent: Could you please provide more details about the issue?
218+
219+
[Workflow Status] IDLE_WITH_PENDING_REQUESTS
220+
221+
- User: My order 1234 arrived damaged and I'd like a refund.
222+
223+
[Handoff from triage-agent to refund-agent initiated.]
224+
- refund-agent: Refund processed successfully for order 1234.
225+
226+
[Workflow Status] IDLE_WITH_PENDING_REQUESTS
227+
228+
- User: Thanks for resolving this.
229+
230+
=== Final Conversation Snapshot ===
231+
- user: Hello, I need assistance with my recent purchase.
232+
- triage-agent: Could you please provide more details about the issue?
233+
- user: My order 1234 arrived damaged and I'd like a refund.
234+
- refund-agent: Refund processed successfully for order 1234.
235+
- user: Thanks for resolving this.
236+
- triage-agent: You're welcome! Have a great day!
237+
===================================
238+
239+
[Workflow Status] IDLE
240+
""" # noqa: E501
241+
242+
243+
if __name__ == "__main__":
244+
asyncio.run(main())

0 commit comments

Comments
 (0)