From eaf3cf5cef78b7ea51fbc024d6a296175905872e Mon Sep 17 00:00:00 2001 From: maxpetrusenkoagent Date: Sun, 14 Jun 2026 01:09:09 -0400 Subject: [PATCH] feat(agentchat): allow scoped AgentTool metadata --- README.md | 17 +++++++- .../src/autogen_agentchat/tools/_agent.py | 42 +++++++++++++++++-- .../tests/test_task_runner_tool.py | 31 ++++++++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 25f7cc162ae9..7d33fdb36055 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,11 @@ async def main() -> None: description="A math expert assistant.", model_client_stream=True, ) - math_agent_tool = AgentTool(math_agent, return_value_as_last_message=True) + math_agent_tool = AgentTool( + math_agent, + return_value_as_last_message=True, + description="Answer math questions. Does not have additional tools attached.", + ) chemistry_agent = AssistantAgent( "chemistry_expert", @@ -133,7 +137,11 @@ async def main() -> None: description="A chemistry expert assistant.", model_client_stream=True, ) - chemistry_agent_tool = AgentTool(chemistry_agent, return_value_as_last_message=True) + chemistry_agent_tool = AgentTool( + chemistry_agent, + return_value_as_last_message=True, + description="Answer chemistry questions. Does not have additional tools attached.", + ) agent = AssistantAgent( "assistant", @@ -150,6 +158,11 @@ async def main() -> None: asyncio.run(main()) ``` +When wrapping agents as tools, expose only the delegated capability you intend the +caller to use. The `AgentTool` name and description control the tool surface shown +to the caller, but they do not enforce authorization; attach separately scoped tools +or workbenches to the wrapped agent when you need least-privilege execution. + For more advanced multi-agent orchestrations and workflows, read [AgentChat documentation](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/index.html). diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/tools/_agent.py b/python/packages/autogen-agentchat/src/autogen_agentchat/tools/_agent.py index ba83bea6b61d..d3ade2bf8390 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/tools/_agent.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/tools/_agent.py @@ -13,6 +13,12 @@ class AgentToolConfig(BaseModel): agent: ComponentModel """The agent to be used for running the task.""" + name: str | None = None + """Optional tool name to expose to the caller.""" + + description: str | None = None + """Optional tool description to expose to the caller.""" + return_value_as_last_message: bool = False """Whether to return the value as the last message of the task result.""" @@ -31,12 +37,23 @@ class AgentTool(TaskRunnerTool, Component[AgentToolConfig]): Args: agent (BaseChatAgent): The agent to be used for running the task. + name (str | None, optional): The name of the tool exposed to the caller. If not provided, + the wrapped agent's name is used. + description (str | None, optional): The description of the tool exposed to the caller. If not provided, + the wrapped agent's description is used. This only controls the prompt-visible tool metadata; + it does not enforce an authorization boundary. Use separately scoped tools or workbenches on the + wrapped agent to enforce least privilege. return_value_as_last_message (bool): Whether to use the last message content of the task result as the return value of the tool in :meth:`~autogen_agentchat.tools.TaskRunnerTool.return_value_as_string`. If set to True, the last message content will be returned as a string. If set to False, the tool will return all messages in the task result as a string concatenated together, with each message prefixed by its source (e.g., "writer: ...", "assistant: ..."). + .. versionadded:: v0.7.6 + + The ``name`` and ``description`` parameters allow exposing a narrower task-specific capability surface + than the wrapped agent's full identity. + Example: .. code-block:: python @@ -76,18 +93,37 @@ async def main() -> None: component_config_schema = AgentToolConfig component_provider_override = "autogen_agentchat.tools.AgentTool" - def __init__(self, agent: BaseChatAgent, return_value_as_last_message: bool = False) -> None: + def __init__( + self, + agent: BaseChatAgent, + return_value_as_last_message: bool = False, + *, + name: str | None = None, + description: str | None = None, + ) -> None: self._agent = agent + self._name_override = name + self._description_override = description super().__init__( - agent, agent.name, agent.description, return_value_as_last_message=return_value_as_last_message + agent, + name if name is not None else agent.name, + description if description is not None else agent.description, + return_value_as_last_message=return_value_as_last_message, ) def _to_config(self) -> AgentToolConfig: return AgentToolConfig( agent=self._agent.dump_component(), + name=self._name_override, + description=self._description_override, return_value_as_last_message=self._return_value_as_last_message, ) @classmethod def _from_config(cls, config: AgentToolConfig) -> Self: - return cls(BaseChatAgent.load_component(config.agent), config.return_value_as_last_message) + return cls( + BaseChatAgent.load_component(config.agent), + config.return_value_as_last_message, + name=config.name, + description=config.description, + ) diff --git a/python/packages/autogen-agentchat/tests/test_task_runner_tool.py b/python/packages/autogen-agentchat/tests/test_task_runner_tool.py index 2449524593eb..9fd92e38b2cc 100644 --- a/python/packages/autogen-agentchat/tests/test_task_runner_tool.py +++ b/python/packages/autogen-agentchat/tests/test_task_runner_tool.py @@ -48,6 +48,8 @@ def test_agent_tool_component() -> None: tool = AgentTool(agent=agent) config = tool.dump_component() assert config.provider == "autogen_agentchat.tools.AgentTool" + assert "name" not in config.config + assert "description" not in config.config tool2 = AgentTool.load_component(config) assert isinstance(tool2, AgentTool) @@ -55,6 +57,35 @@ def test_agent_tool_component() -> None: assert tool2.description == agent.description +def test_agent_tool_component_with_scoped_tool_metadata() -> None: + """Test AgentTool can expose scoped metadata without mutating the wrapped agent.""" + model_client = ReplayChatCompletionClient(["test"]) + agent = AssistantAgent( + name="repo_agent", + model_client=model_client, + description="Can read, write, and execute repository tasks.", + ) + tool = AgentTool( + agent=agent, + name="repo_reader", + description="Read-only repository helper. Does not write files or execute commands.", + ) + + assert tool.name == "repo_reader" + assert tool.description == "Read-only repository helper. Does not write files or execute commands." + assert agent.name == "repo_agent" + assert agent.description == "Can read, write, and execute repository tasks." + + config = tool.dump_component() + assert config.config["name"] == "repo_reader" + assert config.config["description"] == "Read-only repository helper. Does not write files or execute commands." + + tool2 = AgentTool.load_component(config) + assert isinstance(tool2, AgentTool) + assert tool2.name == "repo_reader" + assert tool2.description == "Read-only repository helper. Does not write files or execute commands." + + @pytest.mark.asyncio async def test_team_tool() -> None: """Test running a task with TeamTool."""