Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/strands/multiagent/a2a/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
]

Expand Down
5 changes: 5 additions & 0 deletions src/strands/tools/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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__)

Expand Down
2 changes: 2 additions & 0 deletions src/strands/types/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
33 changes: 33 additions & 0 deletions tests/strands/multiagent/a2a/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions tests/strands/tools/test_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading