From 21be6adcb86722a585b26f600c45c85e593b4ee0 Mon Sep 17 00:00:00 2001 From: Ke Wang Date: Fri, 13 Feb 2026 18:44:11 -0800 Subject: [PATCH] feat: Make skill instruction optimizable and can adapt to user tasks PiperOrigin-RevId: 869971535 --- contributing/samples/skills_agent/agent.py | 5 +- src/google/adk/skills/__init__.py | 2 + src/google/adk/skills/prompt.py | 15 +++++ src/google/adk/tools/skill_toolset.py | 62 ++++++++++----------- tests/unittests/tools/test_skill_toolset.py | 32 ++++------- 5 files changed, 61 insertions(+), 55 deletions(-) diff --git a/contributing/samples/skills_agent/agent.py b/contributing/samples/skills_agent/agent.py index 5c2fb91c..3e590c5e 100644 --- a/contributing/samples/skills_agent/agent.py +++ b/contributing/samples/skills_agent/agent.py @@ -19,6 +19,7 @@ import pathlib from google.adk import Agent from google.adk.skills import load_skill_from_dir from google.adk.skills import models +from google.adk.skills.prompt import DEFAULT_SKILL_SYSTEM_INSTRUCTION from google.adk.tools import skill_toolset greeting_skill = models.Skill( @@ -52,9 +53,7 @@ root_agent = Agent( model="gemini-2.5-flash", name="skill_user_agent", description="An agent that can use specialized skills.", - instruction=( - "You are a helpful assistant that can leverage skills to perform tasks." - ), + instruction=DEFAULT_SKILL_SYSTEM_INSTRUCTION, tools=[ my_skill_toolset, ], diff --git a/src/google/adk/skills/__init__.py b/src/google/adk/skills/__init__.py index 73184b2b..b54535bb 100644 --- a/src/google/adk/skills/__init__.py +++ b/src/google/adk/skills/__init__.py @@ -18,6 +18,7 @@ from .models import Frontmatter from .models import Resources from .models import Script from .models import Skill +from .prompt import DEFAULT_SKILL_SYSTEM_INSTRUCTION from .utils import load_skill_from_dir __all__ = [ @@ -25,5 +26,6 @@ __all__ = [ "Resources", "Script", "Skill", + "DEFAULT_SKILL_SYSTEM_INSTRUCTION", "load_skill_from_dir", ] diff --git a/src/google/adk/skills/prompt.py b/src/google/adk/skills/prompt.py index e9840ab2..5997bb53 100644 --- a/src/google/adk/skills/prompt.py +++ b/src/google/adk/skills/prompt.py @@ -21,6 +21,21 @@ from typing import List from . import models +DEFAULT_SKILL_SYSTEM_INSTRUCTION = """You can use specialized 'skills' to help you with complex tasks. You MUST use the skill tools to interact with these skills. + +Skills are folders of instructions and resources that extend your capabilities for specialized tasks. Each skill folder contains: +- **SKILL.md** (required): The main instruction file with skill metadata and detailed markdown instructions. +- **references/** (Optional): Additional documentation or examples for skill usage. +- **assets/** (Optional): Templates, scripts or other resources used by the skill. + +This is very important: + +1. Use the `list_skills` tool to discover available skills. +2. If a skill seems relevant to the current user query, you MUST use the `load_skill` tool with `name=""` to read its full instructions before proceeding. +3. Once you have read the instructions, follow them exactly as documented before replying to the user. For example, If the instruction lists multiple steps, please make sure you complete all of them in order. +4. The `load_skill_resource` tool is for viewing files within a skill's directory (e.g., `references/*`, `assets/*`). Do NOT use other tools to access these files. +""" + def format_skills_as_xml(skills: List[models.Frontmatter]) -> str: """Formats available skills into a standard XML string. diff --git a/src/google/adk/tools/skill_toolset.py b/src/google/adk/tools/skill_toolset.py index 46566658..d83f8657 100644 --- a/src/google/adk/tools/skill_toolset.py +++ b/src/google/adk/tools/skill_toolset.py @@ -23,7 +23,6 @@ from google.genai import types from ..agents.readonly_context import ReadonlyContext from ..features import experimental from ..features import FeatureName -from ..models.llm_request import LlmRequest from ..skills import models from ..skills import prompt from .base_tool import BaseTool @@ -31,6 +30,36 @@ from .base_toolset import BaseToolset from .tool_context import ToolContext +@experimental(FeatureName.SKILL_TOOLSET) +class ListSkillsTool(BaseTool): + """Tool to list all available skills.""" + + def __init__(self, toolset: "SkillToolset"): + super().__init__( + name="list_skills", + description=( + "Lists all available skills with their names and descriptions." + ), + ) + self._toolset = toolset + + def _get_declaration(self) -> types.FunctionDeclaration | None: + return types.FunctionDeclaration( + name=self.name, + description=self.description, + parameters_json_schema={ + "type": "object", + "properties": {}, + }, + ) + + async def run_async( + self, *, args: dict[str, Any], tool_context: ToolContext + ) -> Any: + skill_frontmatters = self._toolset._list_skills() + return prompt.format_skills_as_xml(skill_frontmatters) + + @experimental(FeatureName.SKILL_TOOLSET) class LoadSkillTool(BaseTool): """Tool to load a skill's instructions.""" @@ -179,6 +208,7 @@ class SkillToolset(BaseToolset): super().__init__() self._skills = {skill.name: skill for skill in skills} self._tools = [ + ListSkillsTool(self), LoadSkillTool(self), LoadSkillResourceTool(self), ] @@ -196,33 +226,3 @@ class SkillToolset(BaseToolset): def _list_skills(self) -> list[models.Frontmatter]: """Lists the frontmatter of all available skills.""" return [s.frontmatter for s in self._skills.values()] - - async def process_llm_request( - self, - *, - tool_context: ToolContext, - llm_request: LlmRequest, - ) -> None: - """Adds available skills to the system instruction.""" - - skill_frontmatters = self._list_skills() - - # Append the skill instruction into the system instruction - skills_xml = prompt.format_skills_as_xml(skill_frontmatters) - skill_si = f""" -You can use specialized 'skills' to help you with complex tasks. Each skill has a name and a description listed below: -{skills_xml} - -Skills are folders of instructions and resources that extend your capabilities for specialized tasks. Each skill folder contains: -- **SKILL.md** (required): The main instruction file with skill metadata and detailed markdown instructions. -- **references/** (Optional): Additional documentation or examples for skill usage. -- **assets/** (Optional): Templates, scripts or other resources used by the skill. - -This is very important: - -1. If a skill seems relevant to the current user query, you MUST use the `load_skill` tool with `name=""` to read its full instructions before proceeding. -2. Once you have read the instructions, follow them exactly as documented before replying to the user. For example, If the instruction lists multiple steps, please make sure you complete all of them in order. -3. The `load_skill_resource` tool is for viewing files within a skill's directory (e.g., `references/*`, `assets/*`). Do NOT use other tools to access these files. -""" - - llm_request.append_instructions([skill_si]) diff --git a/tests/unittests/tools/test_skill_toolset.py b/tests/unittests/tools/test_skill_toolset.py index b6ceb879..9606c498 100644 --- a/tests/unittests/tools/test_skill_toolset.py +++ b/tests/unittests/tools/test_skill_toolset.py @@ -14,7 +14,6 @@ from unittest import mock -from google.adk.models import llm_request from google.adk.skills import models from google.adk.tools import skill_toolset from google.adk.tools import tool_context @@ -124,32 +123,23 @@ def test_list_skills(mock_skill1, mock_skill2): async def test_get_tools(mock_skill1, mock_skill2): toolset = skill_toolset.SkillToolset([mock_skill1, mock_skill2]) tools = await toolset.get_tools() - assert len(tools) == 2 - assert isinstance(tools[0], skill_toolset.LoadSkillTool) - assert isinstance(tools[1], skill_toolset.LoadSkillResourceTool) + assert len(tools) == 3 + assert isinstance(tools[0], skill_toolset.ListSkillsTool) + assert isinstance(tools[1], skill_toolset.LoadSkillTool) + assert isinstance(tools[2], skill_toolset.LoadSkillResourceTool) @pytest.mark.asyncio -async def test_process_llm_request( +@pytest.mark.asyncio +async def test_list_skills_tool( mock_skill1, mock_skill2, tool_context_instance ): toolset = skill_toolset.SkillToolset([mock_skill1, mock_skill2]) - mock_llm_request = llm_request.LlmRequest() - mock_llm_request.config.system_instruction = "existing instruction" - await toolset.process_llm_request( - tool_context=tool_context_instance, llm_request=mock_llm_request - ) - assert "" in mock_llm_request.config.system_instruction - assert ( - "You can use specialized 'skills'" - in mock_llm_request.config.system_instruction - ) - assert ( - "skills are folders" in mock_llm_request.config.system_instruction.lower() - ) - assert mock_llm_request.config.system_instruction.startswith( - "existing instruction" - ) + tool = skill_toolset.ListSkillsTool(toolset) + result = await tool.run_async(args={}, tool_context=tool_context_instance) + assert "" in result + assert "skill1" in result + assert "skill2" in result @pytest.mark.asyncio