From 937f94f845ccaca2cc51be920701bd42760452b5 Mon Sep 17 00:00:00 2001 From: Anuyog Rohilla Date: Mon, 2 Feb 2026 23:31:53 +0530 Subject: [PATCH] feat: add tags support to tools for A2A AgentCard skill metadata - Add tags field to ToolSpec TypedDict as NotRequired[list[str]] - Update @tool decorator to accept optional tags parameter - Modify A2A server to extract tags from tool config - Add comprehensive tests for tags functionality Resolves #1261 --- src/strands/multiagent/a2a/server.py | 7 ++- src/strands/tools/decorator.py | 5 +++ src/strands/types/tools.py | 2 + tests/strands/multiagent/a2a/test_server.py | 33 ++++++++++++++ tests/strands/tools/test_decorator.py | 48 +++++++++++++++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/strands/multiagent/a2a/server.py b/src/strands/multiagent/a2a/server.py index 7b4c4c73a..3fb494533 100644 --- a/src/strands/multiagent/a2a/server.py +++ b/src/strands/multiagent/a2a/server.py @@ -164,7 +164,12 @@ def _get_skills_from_tools(self) -> list[AgentSkill]: list[AgentSkill]: A list of skills this agent provides. """ return [ - AgentSkill(name=config["name"], id=config["name"], description=config["description"], tags=[]) + AgentSkill( + name=config["name"], + id=config["name"], + description=config["description"], + tags=config.get("tags", []), + ) for config in self.strands_agent.tool_registry.get_all_tools_config().values() ] diff --git a/src/strands/tools/decorator.py b/src/strands/tools/decorator.py index 04c14e452..a1424909c 100644 --- a/src/strands/tools/decorator.py +++ b/src/strands/tools/decorator.py @@ -682,6 +682,7 @@ def tool( inputSchema: JSONSchema | None = None, name: str | None = None, context: bool | str = False, + tags: list[str] | None = None, ) -> Callable[[Callable[P, R]], DecoratedFunctionTool[P, R]]: ... # Suppressing the type error because we want callers to be able to use both `tool` and `tool()` at the # call site, but the actual implementation handles that and it's not representable via the type-system @@ -691,6 +692,7 @@ def tool( # type: ignore inputSchema: JSONSchema | None = None, name: str | None = None, context: bool | str = False, + tags: list[str] | None = None, ) -> DecoratedFunctionTool[P, R] | Callable[[Callable[P, R]], DecoratedFunctionTool[P, R]]: """Decorator that transforms a Python function into a Strands tool. @@ -719,6 +721,7 @@ def tool( # type: ignore context: When provided, places an object in the designated parameter. If True, the param name defaults to 'tool_context', or if an override is needed, set context equal to a string to designate the param name. + tags: Optional list of tags for categorizing this tool (e.g., ["data", "api"]). Returns: An AgentTool that also mimics the original function when invoked @@ -773,6 +776,8 @@ def decorator(f: T) -> "DecoratedFunctionTool[P, R]": tool_spec["description"] = description if inputSchema is not None: tool_spec["inputSchema"] = inputSchema + if tags is not None: + tool_spec["tags"] = tags tool_name = tool_spec.get("name", f.__name__) diff --git a/src/strands/types/tools.py b/src/strands/types/tools.py index 6fc0d703c..5f42d7b54 100644 --- a/src/strands/types/tools.py +++ b/src/strands/types/tools.py @@ -30,12 +30,14 @@ class ToolSpec(TypedDict): outputSchema: Optional JSON Schema defining the expected output format. Note: Not all model providers support this field. Providers that don't support it should filter it out before sending to their API. + tags: Optional list of tags for categorization and metadata. """ description: str inputSchema: JSONSchema name: str outputSchema: NotRequired[JSONSchema] + tags: NotRequired[list[str]] class Tool(TypedDict): diff --git a/tests/strands/multiagent/a2a/test_server.py b/tests/strands/multiagent/a2a/test_server.py index 647fce230..41e38276c 100644 --- a/tests/strands/multiagent/a2a/test_server.py +++ b/tests/strands/multiagent/a2a/test_server.py @@ -73,6 +73,39 @@ def test_a2a_agent_initialization_with_custom_skills(mock_strands_agent): assert a2a_agent.agent_skills[1].name == "another_skill" +def test_a2a_agent_skills_with_tags(mock_strands_agent): + """Test that A2AAgent properly extracts tags from tool configs.""" + + mock_tool_config = { + "weather_tool": { + "name": "weather_tool", + "description": "Get weather information", + "tags": ["weather", "data", "api"], + "inputSchema": {"json": {"type": "object", "properties": {}}}, + }, + "no_tags_tool": { + "name": "no_tags_tool", + "description": "Tool without tags", + "inputSchema": {"json": {"type": "object", "properties": {}}}, + }, + } + + mock_strands_agent.tool_registry.get_all_tools_config.return_value = mock_tool_config + + a2a_agent = A2AServer(mock_strands_agent) + + skills = a2a_agent.agent_skills + assert len(skills) == 2 + + # Check tool with tags + weather_skill = next(s for s in skills if s.name == "weather_tool") + assert weather_skill.tags == ["weather", "data", "api"] + + # Check tool without tags (should default to empty list) + no_tags_skill = next(s for s in skills if s.name == "no_tags_tool") + assert no_tags_skill.tags == [] + + def test_public_agent_card(mock_strands_agent): """Test that public_agent_card returns a valid AgentCard.""" # Mock empty tool registry for this test diff --git a/tests/strands/tools/test_decorator.py b/tests/strands/tools/test_decorator.py index 42213fcb8..5a6518127 100644 --- a/tests/strands/tools/test_decorator.py +++ b/tests/strands/tools/test_decorator.py @@ -1901,3 +1901,51 @@ def my_tool(name: str, tag: str | None = None) -> str: # Since tag is not required, anyOf should be simplified away assert "anyOf" not in schema["properties"]["tag"] assert schema["properties"]["tag"]["type"] == "string" + + +def test_tool_decorator_with_tags(): + """Test that @tool decorator properly handles tags parameter.""" + + @strands.tool(tags=["test", "example"]) + def tagged_tool(input: str) -> str: + """A tool with tags. + + Args: + input: Input string + """ + return f"Result: {input}" + + assert tagged_tool.tool_spec.get("tags") == ["test", "example"] + + +def test_tool_decorator_without_tags(): + """Test that @tool decorator works without tags parameter.""" + + @strands.tool + def untagged_tool(input: str) -> str: + """A tool without tags. + + Args: + input: Input string + """ + return f"Result: {input}" + + # tags should not be in the spec when not provided + assert "tags" not in untagged_tool.tool_spec + + +def test_tool_decorator_with_multiple_tags(): + """Test that @tool decorator handles multiple tags correctly.""" + + @strands.tool(tags=["data", "api", "external", "production"]) + def multi_tagged_tool(query: str) -> str: + """A tool with multiple tags. + + Args: + query: Query string + """ + return f"Query: {query}" + + tags = multi_tagged_tool.tool_spec.get("tags") + assert tags == ["data", "api", "external", "production"] + assert len(tags) == 4