Skip to content

Commit 5e49cfa

Browse files
wukathcopybara-github
authored andcommitted
feat: Implement Skill Registry interface in ADK
Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 908414335
1 parent dbec8e9 commit 5e49cfa

3 files changed

Lines changed: 3 additions & 295 deletions

File tree

src/google/adk/skills/skill_registry.py

Lines changed: 0 additions & 83 deletions
This file was deleted.

src/google/adk/tools/skill_toolset.py

Lines changed: 3 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
from ..features import FeatureName
3838
from ..skills import models
3939
from ..skills import prompt
40-
from ..skills.skill_registry import SkillRegistry
4140
from .base_tool import BaseTool
4241
from .base_toolset import BaseToolset
4342
from .function_tool import FunctionTool
@@ -156,20 +155,6 @@ async def run_async(
156155
}
157156

158157
skill = self._toolset._get_skill(skill_name)
159-
if not skill and self._toolset._registry:
160-
try:
161-
skill = await self._toolset._registry.get_skill(name=skill_name)
162-
if skill:
163-
self._toolset._skills[skill_name] = skill
164-
except Exception as e:
165-
logger.exception(
166-
"Failed to fetch skill '%s' from registry.", skill_name
167-
)
168-
return {
169-
"error": f"Failed to fetch skill '{skill_name}' from registry: {e}",
170-
"error_code": "REGISTRY_ERROR",
171-
}
172-
173158
if not skill:
174159
return {
175160
"error": f"Skill '{skill_name}' not found.",
@@ -786,75 +771,14 @@ async def run_async(
786771
)
787772

788773

789-
@experimental(FeatureName.SKILL_TOOLSET)
790-
class SearchSkillsTool(BaseTool):
791-
"""Tool to search for relevant skills in the registry."""
792-
793-
def __init__(self, toolset: "SkillToolset"):
794-
super().__init__(
795-
name="search_skills",
796-
description=toolset._registry.get_search_description(),
797-
)
798-
self._toolset = toolset
799-
800-
def _get_declaration(self) -> types.FunctionDeclaration | None:
801-
properties = {
802-
"query": {
803-
"type": "string",
804-
"description": "Semantic or keyword search query.",
805-
},
806-
}
807-
filter_schema = self._toolset._registry.get_filter_schema()
808-
if filter_schema:
809-
properties["filters"] = filter_schema
810-
return types.FunctionDeclaration(
811-
name=self.name,
812-
description=self.description,
813-
parameters_json_schema={
814-
"type": "object",
815-
"properties": properties,
816-
"required": ["query"],
817-
},
818-
)
819-
820-
async def run_async(
821-
self, *, args: dict[str, Any], tool_context: ToolContext
822-
) -> Any:
823-
query = args.get("query")
824-
filters = args.get("filters")
825-
826-
if not query:
827-
return {
828-
"error": "Argument 'query' is required.",
829-
"error_code": "INVALID_ARGUMENTS",
830-
}
831-
832-
results = await self._toolset._registry.search_skills(
833-
query=query, filters=filters
834-
)
835-
836-
formatted_results = []
837-
for r in results:
838-
if r.name in self._toolset._skills:
839-
logger.warning(
840-
"Naming conflict detected: Skill '%s' exists both locally and in"
841-
" the registry. Filtering out the registry skill.",
842-
r.name,
843-
)
844-
continue
845-
formatted_results.append(r.model_dump())
846-
return formatted_results
847-
848-
849774
@experimental(FeatureName.SKILL_TOOLSET)
850775
class SkillToolset(BaseToolset):
851776
"""A toolset for managing and interacting with agent skills."""
852777

853778
def __init__(
854779
self,
855-
skills: list[models.Skill] | None = None,
780+
skills: list[models.Skill],
856781
*,
857-
registry: Optional[SkillRegistry] = None,
858782
code_executor: Optional[BaseCodeExecutor] = None,
859783
script_timeout: int = _DEFAULT_SCRIPT_TIMEOUT,
860784
additional_tools: list[ToolUnion] | None = None,
@@ -863,7 +787,6 @@ def __init__(
863787
864788
Args:
865789
skills: List of skills to register.
866-
registry: Optional skill registry for dynamic discovery.
867790
code_executor: Optional code executor for script execution.
868791
script_timeout: Timeout in seconds for shell script execution via
869792
subprocess.run. Defaults to 300 seconds. Does not apply to Python
@@ -873,13 +796,12 @@ def __init__(
873796

874797
# Check for duplicate skill names
875798
seen: set[str] = set()
876-
for skill in skills or []:
799+
for skill in skills:
877800
if skill.name in seen:
878801
raise ValueError(f"Duplicate skill name '{skill.name}'.")
879802
seen.add(skill.name)
880803

881-
self._skills = {skill.name: skill for skill in skills or []}
882-
self._registry = registry
804+
self._skills = {skill.name: skill for skill in skills}
883805
self._code_executor = code_executor
884806
self._script_timeout = script_timeout
885807
self._use_invocation_cache = False
@@ -902,8 +824,6 @@ def __init__(
902824
LoadSkillResourceTool(self),
903825
RunSkillScriptTool(self),
904826
]
905-
if self._registry:
906-
self._tools.append(SearchSkillsTool(self))
907827

908828
async def get_tools(
909829
self, readonly_context: ReadonlyContext | None = None
@@ -984,12 +904,6 @@ async def process_llm_request(
984904
skills_xml = prompt.format_skills_as_xml(skills)
985905
instructions = []
986906
instructions.append(_DEFAULT_SKILL_SYSTEM_INSTRUCTION)
987-
if self._registry:
988-
instructions.append(
989-
"\nYou can also use the `search_skills` tool to discover additional"
990-
" skills in the registry if the available skills listed below are"
991-
" not sufficient.\n"
992-
)
993907
instructions.append(skills_xml)
994908
llm_request.append_instructions(instructions)
995909

tests/unittests/tools/test_skill_toolset.py

Lines changed: 0 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from google.adk.code_executors.unsafe_local_code_executor import UnsafeLocalCodeExecutor
2323
from google.adk.models import llm_request as llm_request_model
2424
from google.adk.skills import models
25-
from google.adk.skills.skill_registry import SkillRegistry
2625
from google.adk.tools import skill_toolset
2726
from google.adk.tools import tool_context
2827
from google.genai import types
@@ -474,128 +473,6 @@ async def test_scripts_resource_not_found(mock_skill1, tool_context_instance):
474473
assert result["error_code"] == "RESOURCE_NOT_FOUND"
475474

476475

477-
class MockSkillRegistry(SkillRegistry):
478-
479-
def __init__(self):
480-
self.skills = {}
481-
self.search_results = []
482-
483-
async def get_skill(self, *, name, version=None):
484-
return self.skills.get(name)
485-
486-
async def search_skills(self, *, query, filters=None, **kwargs):
487-
return self.search_results
488-
489-
def get_filter_schema(self):
490-
return None
491-
492-
493-
@pytest.mark.asyncio
494-
async def test_skill_toolset_init_with_registry(mock_skill1):
495-
registry = MockSkillRegistry()
496-
toolset = skill_toolset.SkillToolset([mock_skill1], registry=registry)
497-
assert toolset._registry == registry
498-
tools = await toolset.get_tools()
499-
assert len(tools) == 5 # 4 default + SearchSkillsTool
500-
assert isinstance(tools[4], skill_toolset.SearchSkillsTool)
501-
502-
503-
@pytest.mark.asyncio
504-
async def test_search_skills_tool_run_async(mock_skill1, tool_context_instance):
505-
registry = MockSkillRegistry()
506-
frontmatter = mock.create_autospec(models.Frontmatter, instance=True)
507-
frontmatter.name = "remote-skill"
508-
frontmatter.model_dump.return_value = {"name": "remote-skill"}
509-
registry.search_results = [frontmatter]
510-
511-
toolset = skill_toolset.SkillToolset([mock_skill1], registry=registry)
512-
tool = skill_toolset.SearchSkillsTool(toolset)
513-
514-
result = await tool.run_async(
515-
args={"query": "test"}, tool_context=tool_context_instance
516-
)
517-
assert result == [{"name": "remote-skill"}]
518-
519-
520-
@pytest.mark.asyncio
521-
async def test_search_skills_tool_collision(
522-
mock_skill1, tool_context_instance, caplog
523-
):
524-
registry = MockSkillRegistry()
525-
frontmatter = mock.create_autospec(models.Frontmatter, instance=True)
526-
frontmatter.name = "skill1" # Same name as mock_skill1
527-
frontmatter.model_dump.return_value = {"name": "skill1"}
528-
529-
frontmatter2 = mock.create_autospec(models.Frontmatter, instance=True)
530-
frontmatter2.name = "remote-skill"
531-
frontmatter2.model_dump.return_value = {"name": "remote-skill"}
532-
533-
registry.search_results = [frontmatter, frontmatter2]
534-
535-
toolset = skill_toolset.SkillToolset([mock_skill1], registry=registry)
536-
tool = skill_toolset.SearchSkillsTool(toolset)
537-
538-
with caplog.at_level(logging.WARNING):
539-
result = await tool.run_async(
540-
args={"query": "test"}, tool_context=tool_context_instance
541-
)
542-
assert result == [{"name": "remote-skill"}]
543-
assert "Naming conflict detected" in caplog.text
544-
545-
546-
@pytest.mark.asyncio
547-
async def test_load_skill_tool_fetches_from_registry(
548-
tool_context_instance, mock_skill1
549-
):
550-
registry = MockSkillRegistry()
551-
registry.skills["my-skill"] = mock_skill1
552-
553-
toolset = skill_toolset.SkillToolset([], registry=registry)
554-
tool = skill_toolset.LoadSkillTool(toolset)
555-
556-
result = await tool.run_async(
557-
args={"skill_name": "my-skill"}, tool_context=tool_context_instance
558-
)
559-
assert result["skill_name"] == "my-skill"
560-
assert toolset._skills["my-skill"] == mock_skill1
561-
562-
563-
@pytest.mark.asyncio
564-
async def test_load_skill_tool_registry_error(tool_context_instance):
565-
registry = MockSkillRegistry()
566-
registry.get_skill = mock.AsyncMock(
567-
side_effect=Exception("Test registry error")
568-
)
569-
570-
toolset = skill_toolset.SkillToolset([], registry=registry)
571-
tool = skill_toolset.LoadSkillTool(toolset)
572-
573-
result = await tool.run_async(
574-
args={"skill_name": "my-skill"}, tool_context=tool_context_instance
575-
)
576-
assert result["error_code"] == "REGISTRY_ERROR"
577-
assert "Failed to fetch skill 'my-skill' from registry" in result["error"]
578-
579-
580-
@pytest.mark.asyncio
581-
async def test_process_llm_request_with_registry(
582-
mock_skill1, tool_context_instance
583-
):
584-
registry = MockSkillRegistry()
585-
toolset = skill_toolset.SkillToolset([mock_skill1], registry=registry)
586-
llm_req = mock.create_autospec(llm_request_model.LlmRequest, instance=True)
587-
588-
await toolset.process_llm_request(
589-
tool_context=tool_context_instance, llm_request=llm_req
590-
)
591-
592-
llm_req.append_instructions.assert_called_once()
593-
args, _ = llm_req.append_instructions.call_args
594-
instructions = args[0]
595-
assert len(instructions) == 3 # default + search instruction + skills xml
596-
assert "search_skills" in instructions[1]
597-
598-
599476
# RunSkillScriptTool tests
600477

601478

0 commit comments

Comments
 (0)