Skip to content

Commit ab0c153

Browse files
committed
feat: Add create_handoff_tools() for Azure AI Agent Service compatibility
This PR adds support for Azure AI Agent Service and other providers that require tools to be registered at agent creation time. Changes: - Add create_handoff_tools() function to pre-create handoff FunctionTools - Modify _apply_auto_tools() to skip duplicates instead of raising ValueError - Add get_handoff_tool_name() to public exports - Add tests for Azure AI compatibility scenario The create_handoff_tools() function allows users to pre-create handoff tools that can be included in the agent's default_options when creating agents with Azure AI Agent Service. The HandoffBuilder now detects pre-existing handoff tools and gracefully skips creating duplicates, logging a debug message instead of raising an error. Fixes #3713
1 parent 0f3f4db commit ab0c153

5 files changed

Lines changed: 167 additions & 4 deletions

File tree

python/packages/core/agent_framework/orchestrations/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"HandoffBuilder",
1818
"HandoffConfiguration",
1919
"HandoffSentEvent",
20+
"create_handoff_tools",
21+
"get_handoff_tool_name",
2022
# Base orchestrator
2123
"BaseGroupChatOrchestrator",
2224
"GroupChatRequestMessage",

python/packages/core/agent_framework/orchestrations/__init__.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ from agent_framework_orchestrations import (
3636
SequentialBuilder,
3737
StandardMagenticManager,
3838
__version__,
39+
create_handoff_tools,
40+
get_handoff_tool_name,
3941
)
4042

4143
__all__ = [
@@ -74,4 +76,6 @@ __all__ = [
7476
"SequentialBuilder",
7577
"StandardMagenticManager",
7678
"__version__",
79+
"create_handoff_tools",
80+
"get_handoff_tool_name",
7781
]

python/packages/orchestrations/agent_framework_orchestrations/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
HandoffBuilder,
4040
HandoffConfiguration,
4141
HandoffSentEvent,
42+
create_handoff_tools,
43+
get_handoff_tool_name,
4244
)
4345
from ._magentic import (
4446
MAGENTIC_MANAGER_NAME,
@@ -107,4 +109,6 @@
107109
"__version__",
108110
"clean_conversation_for_handoff",
109111
"create_completion_message",
112+
"create_handoff_tools",
113+
"get_handoff_tool_name",
110114
]

python/packages/orchestrations/agent_framework_orchestrations/_handoff.py

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,75 @@ def get_handoff_tool_name(target_id: str) -> str:
116116
return f"handoff_to_{target_id}"
117117

118118

119+
def create_handoff_tools(
120+
target_agent_ids: Sequence[str],
121+
descriptions: Mapping[str, str] | None = None,
122+
) -> list[FunctionTool[Any, Any]]:
123+
"""Create handoff tools for pre-registration with agents.
124+
125+
This function is particularly useful when using Azure AI Agent Service or other
126+
providers that require tools to be registered at agent creation time rather than
127+
at request time. By pre-creating handoff tools, you can include them in the agent's
128+
tool list when creating the agent.
129+
130+
The HandoffBuilder will automatically detect pre-existing handoff tools and skip
131+
creating duplicates, ensuring seamless integration.
132+
133+
Args:
134+
target_agent_ids: Sequence of target agent identifiers that can be handed off to.
135+
descriptions: Optional mapping of agent IDs to custom tool descriptions.
136+
If not provided, a default description will be used.
137+
138+
Returns:
139+
List of FunctionTool instances ready for use with ChatAgent.
140+
141+
Example:
142+
.. code-block:: python
143+
144+
from agent_framework.orchestrations import create_handoff_tools
145+
146+
# Pre-create handoff tools for Azure AI Agent Service compatibility
147+
handoff_tools = create_handoff_tools(
148+
["specialist", "escalation"],
149+
descriptions={
150+
"specialist": "Route to specialist for technical issues",
151+
"escalation": "Escalate to supervisor for complex cases",
152+
},
153+
)
154+
155+
# Include tools when creating the agent
156+
agent = ChatAgent(
157+
chat_client=azure_client,
158+
name="triage",
159+
default_options={"tools": handoff_tools + other_tools},
160+
)
161+
162+
See Also:
163+
- `get_handoff_tool_name`: Get the standardized tool name for a target agent.
164+
- `HandoffBuilder`: Builder for creating handoff workflows.
165+
"""
166+
descriptions = descriptions or {}
167+
tools: list[FunctionTool[Any, Any]] = []
168+
169+
for target_id in target_agent_ids:
170+
tool_name = get_handoff_tool_name(target_id)
171+
doc = descriptions.get(target_id) or f"Handoff to the {target_id} agent."
172+
173+
# Note: approval_mode is set to "never_require" for handoff tools because
174+
# they are framework-internal signals that trigger routing logic, not
175+
# actual function executions. They are automatically intercepted by
176+
# _AutoHandoffMiddleware which short-circuits execution and provides synthetic
177+
# results, so the function body never actually runs in practice.
178+
@tool(name=tool_name, description=doc, approval_mode="never_require")
179+
def _handoff_tool(context: str | None = None, *, _target: str = target_id) -> str:
180+
"""Return a deterministic acknowledgement that encodes the target alias."""
181+
return f"Handoff to {_target}"
182+
183+
tools.append(_handoff_tool)
184+
185+
return tools
186+
187+
119188
HANDOFF_FUNCTION_RESULT_KEY = "handoff_to"
120189

121190

@@ -331,11 +400,15 @@ def _apply_auto_tools(self, agent: ChatAgent, targets: Sequence[HandoffConfigura
331400
for target in targets:
332401
handoff_tool = self._create_handoff_tool(target.target_id, target.description)
333402
if handoff_tool.name in existing_names:
334-
raise ValueError(
335-
f"Agent '{resolve_agent_id(agent)}' already has a tool named '{handoff_tool.name}'. "
336-
f"Handoff tool name '{handoff_tool.name}' conflicts with existing tool."
337-
"Please rename the existing tool or modify the target agent ID to avoid conflicts."
403+
# Skip adding duplicate tools - this supports Azure AI Agent Service
404+
# where users pre-create handoff tools using create_handoff_tools()
405+
# and register them at agent creation time.
406+
logger.debug(
407+
"Handoff tool '%s' already exists for agent '%s', skipping duplicate creation.",
408+
handoff_tool.name,
409+
resolve_agent_id(agent),
338410
)
411+
continue
339412
new_tools.append(handoff_tool)
340413

341414
if new_tools:

python/packages/orchestrations/tests/test_handoff.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,3 +727,83 @@ def create_specialist() -> MockHandoffAgent:
727727

728728

729729
# endregion Participant Factory Tests
730+
731+
732+
# region Azure AI Agent Service Compatibility Tests
733+
734+
735+
def test_create_handoff_tools_basic():
736+
"""Test that create_handoff_tools creates tools with correct names and descriptions."""
737+
from agent_framework.orchestrations import create_handoff_tools, get_handoff_tool_name
738+
739+
target_ids = ["specialist", "escalation"]
740+
tools = create_handoff_tools(target_ids)
741+
742+
assert len(tools) == 2
743+
assert tools[0].name == get_handoff_tool_name("specialist")
744+
assert tools[1].name == get_handoff_tool_name("escalation")
745+
assert "specialist" in tools[0].description.lower()
746+
assert "escalation" in tools[1].description.lower()
747+
748+
749+
def test_create_handoff_tools_with_custom_descriptions():
750+
"""Test that create_handoff_tools accepts custom descriptions."""
751+
from agent_framework.orchestrations import create_handoff_tools
752+
753+
target_ids = ["specialist", "escalation"]
754+
descriptions = {
755+
"specialist": "Route to technical specialist for complex issues",
756+
"escalation": "Escalate to supervisor for urgent matters",
757+
}
758+
tools = create_handoff_tools(target_ids, descriptions=descriptions)
759+
760+
assert len(tools) == 2
761+
assert tools[0].description == descriptions["specialist"]
762+
assert tools[1].description == descriptions["escalation"]
763+
764+
765+
def test_get_handoff_tool_name():
766+
"""Test that get_handoff_tool_name returns consistent naming."""
767+
from agent_framework.orchestrations import get_handoff_tool_name
768+
769+
assert get_handoff_tool_name("specialist") == "handoff_to_specialist"
770+
assert get_handoff_tool_name("my_agent") == "handoff_to_my_agent"
771+
772+
773+
def test_handoff_builder_skips_duplicate_tools():
774+
"""Test that HandoffBuilder skips adding duplicate handoff tools.
775+
776+
This is crucial for Azure AI Agent Service compatibility where users
777+
pre-create handoff tools using create_handoff_tools() and register
778+
them at agent creation time.
779+
"""
780+
from agent_framework.orchestrations import create_handoff_tools
781+
782+
# Pre-create handoff tools (simulating Azure AI Agent Service pattern)
783+
pre_created_tools = create_handoff_tools(["specialist"])
784+
785+
# Create agent with pre-created tools
786+
triage_client = MockChatClient(name="triage", handoff_to="specialist")
787+
triage = ChatAgent(
788+
chat_client=triage_client,
789+
name="triage",
790+
default_options={"tools": pre_created_tools},
791+
)
792+
793+
specialist_client = MockChatClient(name="specialist")
794+
specialist = ChatAgent(chat_client=specialist_client, name="specialist")
795+
796+
# Build workflow - should NOT raise ValueError for duplicate tools
797+
workflow = (
798+
HandoffBuilder()
799+
.participants([triage, specialist])
800+
.with_start_agent(triage)
801+
.add_handoff(triage, [specialist])
802+
.build()
803+
)
804+
805+
# Verify workflow was built successfully
806+
assert workflow is not None
807+
808+
809+
# endregion Azure AI Agent Service Compatibility Tests

0 commit comments

Comments
 (0)