Skip to content

Commit 3243652

Browse files
dmytrostrukCopilot
andauthored
Python: Filter conversation_id when passing kwargs to agent as tool (#3266)
* Filter conversation_id when passing kwargs to agent as tool * Small fix * Update python/samples/getting_started/agents/azure_ai/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update python/samples/getting_started/agents/openai/openai_responses_client_with_agent_as_tool.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update python/samples/getting_started/agents/azure_ai/azure_ai_with_agent_as_tool.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 915df3b commit 3243652

6 files changed

Lines changed: 182 additions & 2 deletions

File tree

python/packages/core/agent_framework/_agents.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,8 +470,8 @@ async def agent_wrapper(**kwargs: Any) -> str:
470470
# Extract the input from kwargs using the specified arg_name
471471
input_text = kwargs.get(arg_name, "")
472472

473-
# Forward all kwargs except the arg_name to support runtime context propagation
474-
forwarded_kwargs = {k: v for k, v in kwargs.items() if k != arg_name}
473+
# Forward runtime context kwargs, excluding arg_name and conversation_id.
474+
forwarded_kwargs = {k: v for k, v in kwargs.items() if k not in (arg_name, "conversation_id")}
475475

476476
if stream_callback is None:
477477
# Use non-streaming mode

python/packages/core/tests/core/test_as_tool_kwargs_propagation.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,44 @@ async def capture_middleware(
313313
# Verify second call had its own kwargs (not leaked from first)
314314
assert second_call_kwargs.get("session_id") == "session-2"
315315
assert second_call_kwargs.get("api_token") == "token-2"
316+
317+
async def test_as_tool_excludes_conversation_id_from_forwarded_kwargs(self, chat_client: MockChatClient) -> None:
318+
"""Test that conversation_id is not forwarded to sub-agent."""
319+
captured_kwargs: dict[str, Any] = {}
320+
321+
@agent_middleware
322+
async def capture_middleware(
323+
context: AgentRunContext, next: Callable[[AgentRunContext], Awaitable[None]]
324+
) -> None:
325+
captured_kwargs.update(context.kwargs)
326+
await next(context)
327+
328+
# Setup mock response
329+
chat_client.responses = [
330+
ChatResponse(messages=[ChatMessage(role="assistant", text="Response from sub-agent")]),
331+
]
332+
333+
sub_agent = ChatAgent(
334+
chat_client=chat_client,
335+
name="sub_agent",
336+
middleware=[capture_middleware],
337+
)
338+
339+
tool = sub_agent.as_tool(name="delegate", arg_name="task")
340+
341+
# Invoke tool with conversation_id in kwargs (simulating parent's conversation state)
342+
await tool.invoke(
343+
arguments=tool.input_model(task="Test delegation"),
344+
conversation_id="conv-parent-456",
345+
api_token="secret-xyz-123",
346+
user_id="user-456",
347+
)
348+
349+
# Verify conversation_id was NOT forwarded to sub-agent
350+
assert "conversation_id" not in captured_kwargs, (
351+
f"conversation_id should not be forwarded, but got: {captured_kwargs}"
352+
)
353+
354+
# Verify other kwargs were still forwarded
355+
assert captured_kwargs.get("api_token") == "secret-xyz-123"
356+
assert captured_kwargs.get("user_id") == "user-456"

python/samples/getting_started/agents/azure_ai/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This folder contains examples demonstrating different ways to create and use age
99
| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIProjectAgentProvider`. Demonstrates both streaming and non-streaming responses with function tools. Shows automatic agent creation and basic weather functionality. |
1010
| [`azure_ai_provider_methods.py`](azure_ai_provider_methods.py) | Comprehensive guide to `AzureAIProjectAgentProvider` methods: `create_agent()` for creating new agents, `get_agent()` for retrieving existing agents (by name, reference, or details), and `as_agent()` for wrapping SDK objects without HTTP calls. |
1111
| [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation by using `provider.get_agent()` to retrieve the latest version. |
12+
| [`azure_ai_with_agent_as_tool.py`](azure_ai_with_agent_as_tool.py) | Shows how to use the agent-as-tool pattern with Azure AI agents, where one agent delegates work to specialized sub-agents wrapped as tools using `as_tool()`. Demonstrates hierarchical agent architectures. |
1213
| [`azure_ai_with_agent_to_agent.py`](azure_ai_with_agent_to_agent.py) | Shows how to use Agent-to-Agent (A2A) capabilities with Azure AI agents to enable communication with other agents using the A2A protocol. Requires an A2A connection configured in your Azure AI project. |
1314
| [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Shows how to use Azure AI Search with Azure AI agents to search through indexed data and answer user questions with proper citations. Requires an Azure AI Search connection and index configured in your Azure AI project. |
1415
| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to search the web for current information and provide grounded responses with citations. Requires a Bing connection configured in your Azure AI project. |
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
import asyncio
4+
from collections.abc import Awaitable, Callable
5+
6+
from agent_framework import FunctionInvocationContext
7+
from agent_framework.azure import AzureAIProjectAgentProvider
8+
from azure.identity.aio import AzureCliCredential
9+
10+
"""
11+
Azure AI Agent-as-Tool Example
12+
13+
Demonstrates hierarchical agent architectures where one agent delegates
14+
work to specialized sub-agents wrapped as tools using as_tool().
15+
16+
This pattern is useful when you want a coordinator agent to orchestrate
17+
multiple specialized agents, each focusing on specific tasks.
18+
"""
19+
20+
21+
async def logging_middleware(
22+
context: FunctionInvocationContext,
23+
next: Callable[[FunctionInvocationContext], Awaitable[None]],
24+
) -> None:
25+
"""Middleware that logs tool invocations to show the delegation flow."""
26+
print(f"[Calling tool: {context.function.name}]")
27+
print(f"[Request: {context.arguments}]")
28+
29+
await next(context)
30+
31+
print(f"[Response: {context.result}]")
32+
33+
34+
async def main() -> None:
35+
print("=== Azure AI Agent-as-Tool Pattern ===")
36+
37+
async with (
38+
AzureCliCredential() as credential,
39+
AzureAIProjectAgentProvider(credential=credential) as provider,
40+
):
41+
# Create a specialized writer agent
42+
writer = await provider.create_agent(
43+
name="WriterAgent",
44+
instructions="You are a creative writer. Write short, engaging content.",
45+
)
46+
47+
# Convert writer agent to a tool using as_tool()
48+
writer_tool = writer.as_tool(
49+
name="creative_writer",
50+
description="Generate creative content like taglines, slogans, or short copy",
51+
arg_name="request",
52+
arg_description="What to write",
53+
)
54+
55+
# Create coordinator agent with writer as a tool
56+
coordinator = await provider.create_agent(
57+
name="CoordinatorAgent",
58+
instructions="You coordinate with specialized agents. Delegate writing tasks to the creative_writer tool.",
59+
tools=[writer_tool],
60+
middleware=[logging_middleware],
61+
)
62+
63+
query = "Create a tagline for a coffee shop"
64+
print(f"User: {query}")
65+
result = await coordinator.run(query)
66+
print(f"Coordinator: {result}\n")
67+
68+
69+
if __name__ == "__main__":
70+
asyncio.run(main())

python/samples/getting_started/agents/openai/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ This folder contains examples demonstrating different ways to create and use age
2727
| [`openai_responses_client_image_generation.py`](openai_responses_client_image_generation.py) | Demonstrates how to use image generation capabilities with OpenAI agents to create images based on text descriptions. Requires PIL (Pillow) for image display. |
2828
| [`openai_responses_client_reasoning.py`](openai_responses_client_reasoning.py) | Demonstrates how to use reasoning capabilities with OpenAI agents, showing how the agent can provide detailed reasoning for its responses. |
2929
| [`openai_responses_client_streaming_image_generation.py`](openai_responses_client_streaming_image_generation.py) | Demonstrates streaming image generation with partial images for real-time image creation feedback and improved user experience. |
30+
| [`openai_responses_client_with_agent_as_tool.py`](openai_responses_client_with_agent_as_tool.py) | Shows how to use the agent-as-tool pattern with OpenAI Responses Client, where one agent delegates work to specialized sub-agents wrapped as tools using `as_tool()`. Demonstrates hierarchical agent architectures. |
3031
| [`openai_responses_client_with_code_interpreter.py`](openai_responses_client_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with OpenAI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. |
3132
| [`openai_responses_client_with_explicit_settings.py`](openai_responses_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific responses client, configuring settings explicitly including API key and model ID. |
3233
| [`openai_responses_client_with_file_search.py`](openai_responses_client_with_file_search.py) | Demonstrates how to use file search capabilities with OpenAI agents, allowing the agent to search through uploaded files to answer questions. |
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
import asyncio
4+
from collections.abc import Awaitable, Callable
5+
6+
from agent_framework import FunctionInvocationContext
7+
from agent_framework.openai import OpenAIResponsesClient
8+
9+
"""
10+
OpenAI Responses Client Agent-as-Tool Example
11+
12+
Demonstrates hierarchical agent architectures where one agent delegates
13+
work to specialized sub-agents wrapped as tools using as_tool().
14+
15+
This pattern is useful when you want a coordinator agent to orchestrate
16+
multiple specialized agents, each focusing on specific tasks.
17+
"""
18+
19+
20+
async def logging_middleware(
21+
context: FunctionInvocationContext,
22+
next: Callable[[FunctionInvocationContext], Awaitable[None]],
23+
) -> None:
24+
"""Middleware that logs tool invocations to show the delegation flow."""
25+
print(f"[Calling tool: {context.function.name}]")
26+
print(f"[Request: {context.arguments}]")
27+
28+
await next(context)
29+
30+
print(f"[Response: {context.result}]")
31+
32+
33+
async def main() -> None:
34+
print("=== OpenAI Responses Client Agent-as-Tool Pattern ===")
35+
36+
client = OpenAIResponsesClient()
37+
38+
# Create a specialized writer agent
39+
writer = client.as_agent(
40+
name="WriterAgent",
41+
instructions="You are a creative writer. Write short, engaging content.",
42+
)
43+
44+
# Convert writer agent to a tool using as_tool()
45+
writer_tool = writer.as_tool(
46+
name="creative_writer",
47+
description="Generate creative content like taglines, slogans, or short copy",
48+
arg_name="request",
49+
arg_description="What to write",
50+
)
51+
52+
# Create coordinator agent with writer as a tool
53+
coordinator = client.as_agent(
54+
name="CoordinatorAgent",
55+
instructions="You coordinate with specialized agents. Delegate writing tasks to the creative_writer tool.",
56+
tools=[writer_tool],
57+
middleware=[logging_middleware],
58+
)
59+
60+
query = "Create a tagline for a coffee shop"
61+
print(f"User: {query}")
62+
result = await coordinator.run(query)
63+
print(f"Coordinator: {result}\n")
64+
65+
66+
if __name__ == "__main__":
67+
asyncio.run(main())

0 commit comments

Comments
 (0)