diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index 628ac7fb17..e54fef5aeb 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -470,8 +470,8 @@ async def agent_wrapper(**kwargs: Any) -> str: # Extract the input from kwargs using the specified arg_name input_text = kwargs.get(arg_name, "") - # Forward all kwargs except the arg_name to support runtime context propagation - forwarded_kwargs = {k: v for k, v in kwargs.items() if k != arg_name} + # Forward runtime context kwargs, excluding arg_name and conversation_id. + forwarded_kwargs = {k: v for k, v in kwargs.items() if k not in (arg_name, "conversation_id")} if stream_callback is None: # Use non-streaming mode diff --git a/python/packages/core/tests/core/test_as_tool_kwargs_propagation.py b/python/packages/core/tests/core/test_as_tool_kwargs_propagation.py index 8669ecc3d6..6bd52975c2 100644 --- a/python/packages/core/tests/core/test_as_tool_kwargs_propagation.py +++ b/python/packages/core/tests/core/test_as_tool_kwargs_propagation.py @@ -313,3 +313,44 @@ async def capture_middleware( # Verify second call had its own kwargs (not leaked from first) assert second_call_kwargs.get("session_id") == "session-2" assert second_call_kwargs.get("api_token") == "token-2" + + async def test_as_tool_excludes_conversation_id_from_forwarded_kwargs(self, chat_client: MockChatClient) -> None: + """Test that conversation_id is not forwarded to sub-agent.""" + captured_kwargs: dict[str, Any] = {} + + @agent_middleware + async def capture_middleware( + context: AgentRunContext, next: Callable[[AgentRunContext], Awaitable[None]] + ) -> None: + captured_kwargs.update(context.kwargs) + await next(context) + + # Setup mock response + chat_client.responses = [ + ChatResponse(messages=[ChatMessage(role="assistant", text="Response from sub-agent")]), + ] + + sub_agent = ChatAgent( + chat_client=chat_client, + name="sub_agent", + middleware=[capture_middleware], + ) + + tool = sub_agent.as_tool(name="delegate", arg_name="task") + + # Invoke tool with conversation_id in kwargs (simulating parent's conversation state) + await tool.invoke( + arguments=tool.input_model(task="Test delegation"), + conversation_id="conv-parent-456", + api_token="secret-xyz-123", + user_id="user-456", + ) + + # Verify conversation_id was NOT forwarded to sub-agent + assert "conversation_id" not in captured_kwargs, ( + f"conversation_id should not be forwarded, but got: {captured_kwargs}" + ) + + # Verify other kwargs were still forwarded + assert captured_kwargs.get("api_token") == "secret-xyz-123" + assert captured_kwargs.get("user_id") == "user-456" diff --git a/python/samples/getting_started/agents/azure_ai/README.md b/python/samples/getting_started/agents/azure_ai/README.md index f60b64cf18..c949476ca0 100644 --- a/python/samples/getting_started/agents/azure_ai/README.md +++ b/python/samples/getting_started/agents/azure_ai/README.md @@ -9,6 +9,7 @@ This folder contains examples demonstrating different ways to create and use age | [`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. | | [`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. | | [`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. | +| [`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. | | [`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. | | [`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. | | [`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. | diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_agent_as_tool.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_agent_as_tool.py new file mode 100644 index 0000000000..041f632d2f --- /dev/null +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_agent_as_tool.py @@ -0,0 +1,70 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +from collections.abc import Awaitable, Callable + +from agent_framework import FunctionInvocationContext +from agent_framework.azure import AzureAIProjectAgentProvider +from azure.identity.aio import AzureCliCredential + +""" +Azure AI Agent-as-Tool Example + +Demonstrates hierarchical agent architectures where one agent delegates +work to specialized sub-agents wrapped as tools using as_tool(). + +This pattern is useful when you want a coordinator agent to orchestrate +multiple specialized agents, each focusing on specific tasks. +""" + + +async def logging_middleware( + context: FunctionInvocationContext, + next: Callable[[FunctionInvocationContext], Awaitable[None]], +) -> None: + """Middleware that logs tool invocations to show the delegation flow.""" + print(f"[Calling tool: {context.function.name}]") + print(f"[Request: {context.arguments}]") + + await next(context) + + print(f"[Response: {context.result}]") + + +async def main() -> None: + print("=== Azure AI Agent-as-Tool Pattern ===") + + async with ( + AzureCliCredential() as credential, + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + # Create a specialized writer agent + writer = await provider.create_agent( + name="WriterAgent", + instructions="You are a creative writer. Write short, engaging content.", + ) + + # Convert writer agent to a tool using as_tool() + writer_tool = writer.as_tool( + name="creative_writer", + description="Generate creative content like taglines, slogans, or short copy", + arg_name="request", + arg_description="What to write", + ) + + # Create coordinator agent with writer as a tool + coordinator = await provider.create_agent( + name="CoordinatorAgent", + instructions="You coordinate with specialized agents. Delegate writing tasks to the creative_writer tool.", + tools=[writer_tool], + middleware=[logging_middleware], + ) + + query = "Create a tagline for a coffee shop" + print(f"User: {query}") + result = await coordinator.run(query) + print(f"Coordinator: {result}\n") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/agents/openai/README.md b/python/samples/getting_started/agents/openai/README.md index d744531845..4feff05d22 100644 --- a/python/samples/getting_started/agents/openai/README.md +++ b/python/samples/getting_started/agents/openai/README.md @@ -27,6 +27,7 @@ This folder contains examples demonstrating different ways to create and use age | [`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. | | [`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. | | [`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. | +| [`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. | | [`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. | | [`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. | | [`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. | diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_agent_as_tool.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_agent_as_tool.py new file mode 100644 index 0000000000..13b472e2a3 --- /dev/null +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_agent_as_tool.py @@ -0,0 +1,67 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +from collections.abc import Awaitable, Callable + +from agent_framework import FunctionInvocationContext +from agent_framework.openai import OpenAIResponsesClient + +""" +OpenAI Responses Client Agent-as-Tool Example + +Demonstrates hierarchical agent architectures where one agent delegates +work to specialized sub-agents wrapped as tools using as_tool(). + +This pattern is useful when you want a coordinator agent to orchestrate +multiple specialized agents, each focusing on specific tasks. +""" + + +async def logging_middleware( + context: FunctionInvocationContext, + next: Callable[[FunctionInvocationContext], Awaitable[None]], +) -> None: + """Middleware that logs tool invocations to show the delegation flow.""" + print(f"[Calling tool: {context.function.name}]") + print(f"[Request: {context.arguments}]") + + await next(context) + + print(f"[Response: {context.result}]") + + +async def main() -> None: + print("=== OpenAI Responses Client Agent-as-Tool Pattern ===") + + client = OpenAIResponsesClient() + + # Create a specialized writer agent + writer = client.as_agent( + name="WriterAgent", + instructions="You are a creative writer. Write short, engaging content.", + ) + + # Convert writer agent to a tool using as_tool() + writer_tool = writer.as_tool( + name="creative_writer", + description="Generate creative content like taglines, slogans, or short copy", + arg_name="request", + arg_description="What to write", + ) + + # Create coordinator agent with writer as a tool + coordinator = client.as_agent( + name="CoordinatorAgent", + instructions="You coordinate with specialized agents. Delegate writing tasks to the creative_writer tool.", + tools=[writer_tool], + middleware=[logging_middleware], + ) + + query = "Create a tagline for a coffee shop" + print(f"User: {query}") + result = await coordinator.run(query) + print(f"Coordinator: {result}\n") + + +if __name__ == "__main__": + asyncio.run(main())