Skip to content

Commit 066fcec

Browse files
wukathcopybara-github
authored andcommitted
feat: Add support for toolsets to additional_tools field of SkillToolset
Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 882212923
1 parent ae565be commit 066fcec

File tree

2 files changed

+70
-9
lines changed

2 files changed

+70
-9
lines changed

src/google/adk/tools/skill_toolset.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -702,8 +702,11 @@ def __init__(
702702
self._script_timeout = script_timeout
703703

704704
self._provided_tools_by_name = {}
705+
self._provided_toolsets = []
705706
for tool_union in additional_tools or []:
706-
if isinstance(tool_union, BaseTool):
707+
if isinstance(tool_union, BaseToolset):
708+
self._provided_toolsets.append(tool_union)
709+
elif isinstance(tool_union, BaseTool):
707710
self._provided_tools_by_name[tool_union.name] = tool_union
708711
elif callable(tool_union):
709712
ft = FunctionTool(tool_union)
@@ -754,11 +757,22 @@ async def _resolve_additional_tools_from_state(
754757
if not additional_tool_names:
755758
return []
756759

760+
# Collect all candidate tools from both individual tools and toolsets
761+
candidate_tools = self._provided_tools_by_name.copy()
762+
if self._provided_toolsets:
763+
ts_results = await asyncio.gather(*(
764+
ts.get_tools_with_prefix(readonly_context)
765+
for ts in self._provided_toolsets
766+
))
767+
for ts_tools in ts_results:
768+
for t in ts_tools:
769+
candidate_tools[t.name] = t
770+
757771
resolved_tools = []
758772
existing_tool_names = {t.name for t in self._tools}
759773
for name in additional_tool_names:
760-
if name in self._provided_tools_by_name:
761-
tool = self._provided_tools_by_name[name]
774+
if name in candidate_tools:
775+
tool = candidate_tools[name]
762776
if tool.name in existing_tool_names:
763777
logger.error(
764778
"Tool name collision: tool '%s' already exists.", tool.name

tests/unittests/tools/test_skill_toolset.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import logging
1616
from unittest import mock
1717

18+
from google.adk.agents.readonly_context import ReadonlyContext
1819
from google.adk.code_executors.base_code_executor import BaseCodeExecutor
1920
from google.adk.code_executors.code_execution_utils import CodeExecutionResult
2021
from google.adk.models import llm_request as llm_request_model
@@ -1290,41 +1291,87 @@ async def test_execute_script_binary_content_packaged():
12901291

12911292

12921293
@pytest.mark.asyncio
1293-
async def test_skill_toolset_dynamic_tool_resolution(mock_skill1):
1294-
# Set up a skill with additional_tools in metadata
1294+
async def test_skill_toolset_dynamic_tool_resolution(mock_skill1, mock_skill2):
1295+
# Set up skills with additional_tools in metadata
12951296
mock_skill1.frontmatter.metadata = {
1296-
"adk_additional_tools": ["my_custom_tool", "my_func"]
1297+
"adk_additional_tools": ["my_custom_tool", "my_func", "shared_tool"]
12971298
}
12981299
mock_skill1.name = "skill1"
12991300

1301+
mock_skill2.frontmatter.metadata = {
1302+
"adk_additional_tools": [
1303+
"skill2_tool",
1304+
"shared_tool",
1305+
"prefixed_mock_tool",
1306+
]
1307+
}
1308+
mock_skill2.name = "skill2"
1309+
13001310
# Prepare additional tools
13011311
custom_tool = mock.create_autospec(skill_toolset.BaseTool, instance=True)
13021312
custom_tool.name = "my_custom_tool"
13031313

1314+
skill2_tool = mock.create_autospec(skill_toolset.BaseTool, instance=True)
1315+
skill2_tool.name = "skill2_tool"
1316+
1317+
shared_tool = mock.create_autospec(skill_toolset.BaseTool, instance=True)
1318+
shared_tool.name = "shared_tool"
1319+
13041320
def my_func():
13051321
"""My function description."""
13061322
pass
13071323

1324+
# Setup prefixed toolset
1325+
mock_tool = mock.create_autospec(skill_toolset.BaseTool, instance=True)
1326+
mock_tool.name = "prefixed_mock_tool"
1327+
prefixed_set = mock.create_autospec(skill_toolset.BaseToolset, instance=True)
1328+
prefixed_set.get_tools_with_prefix.return_value = [mock_tool]
1329+
13081330
toolset = skill_toolset.SkillToolset(
1309-
[mock_skill1],
1310-
additional_tools=[custom_tool, my_func],
1331+
[mock_skill1, mock_skill2],
1332+
additional_tools=[
1333+
custom_tool,
1334+
skill2_tool,
1335+
shared_tool,
1336+
my_func,
1337+
prefixed_set,
1338+
],
13111339
)
13121340

13131341
ctx = _make_tool_context_with_agent()
13141342
# Initial tools (only core)
13151343
tools = await toolset.get_tools(readonly_context=ctx)
13161344
assert len(tools) == 4
13171345

1318-
# Activate skill
1346+
# Activate skills
13191347
load_tool = skill_toolset.LoadSkillTool(toolset)
13201348
await load_tool.run_async(args={"name": "skill1"}, tool_context=ctx)
1349+
await load_tool.run_async(args={"name": "skill2"}, tool_context=ctx)
13211350

13221351
# Dynamic tools should now be resolved
13231352
tools = await toolset.get_tools(readonly_context=ctx)
13241353
tool_names = {t.name for t in tools}
1354+
1355+
# Core tools
1356+
assert "list_skills" in tool_names
1357+
assert "load_skill" in tool_names
1358+
assert "load_skill_resource" in tool_names
1359+
assert "run_skill_script" in tool_names
1360+
1361+
# Skill 1 tools
13251362
assert "my_custom_tool" in tool_names
13261363
assert "my_func" in tool_names
13271364

1365+
# Skill 2 tools
1366+
assert "skill2_tool" in tool_names
1367+
1368+
# Shared tool (should only appear once)
1369+
assert "shared_tool" in tool_names
1370+
assert len([t for t in tools if t.name == "shared_tool"]) == 1
1371+
1372+
# Prefixed toolset tool
1373+
assert "prefixed_mock_tool" in tool_names
1374+
13281375
# Check specific tool resolution details
13291376
my_func_tool = next(t for t in tools if t.name == "my_func")
13301377
assert isinstance(my_func_tool, skill_toolset.FunctionTool)

0 commit comments

Comments
 (0)