From c7362100ebc9d93449d7ce341f420664eb3ac361 Mon Sep 17 00:00:00 2001 From: Kathy Wu Date: Thu, 5 Feb 2026 22:05:39 -0800 Subject: [PATCH] feat: Add models.py and prompt.py to adk/skills to use in skill toolset Also redefined schemas in models.py to be pydantic. Co-authored-by: Kathy Wu PiperOrigin-RevId: 866270203 --- src/google/adk/skills/__init__.py | 29 +++++ src/google/adk/skills/models.py | 148 ++++++++++++++++++++++++++ src/google/adk/skills/prompt.py | 53 +++++++++ tests/unittests/skills/__init__.py | 13 +++ tests/unittests/skills/test_models.py | 70 ++++++++++++ tests/unittests/skills/test_prompt.py | 49 +++++++++ 6 files changed, 362 insertions(+) create mode 100644 src/google/adk/skills/__init__.py create mode 100644 src/google/adk/skills/models.py create mode 100644 src/google/adk/skills/prompt.py create mode 100644 tests/unittests/skills/__init__.py create mode 100644 tests/unittests/skills/test_models.py create mode 100644 tests/unittests/skills/test_prompt.py diff --git a/src/google/adk/skills/__init__.py b/src/google/adk/skills/__init__.py new file mode 100644 index 00000000..3480bc7d --- /dev/null +++ b/src/google/adk/skills/__init__.py @@ -0,0 +1,29 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Agent Development Kit - Skills.""" + +from .models import Frontmatter +from .models import Resources +from .models import Script +from .models import Skill +from .prompt import format_skills_as_xml + +__all__ = [ + "Frontmatter", + "Resources", + "Script", + "Skill", + "format_skills_as_xml", +] diff --git a/src/google/adk/skills/models.py b/src/google/adk/skills/models.py new file mode 100644 index 00000000..7f5d75b4 --- /dev/null +++ b/src/google/adk/skills/models.py @@ -0,0 +1,148 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Data models for Agent Skills.""" + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel + + +class Frontmatter(BaseModel): + """L1 skill content: metadata parsed from SKILL.md frontmatter for skill discovery. + + Attributes: + name: Skill name in kebab-case (required). + description: What the skill does and when the model should use it + (required). + license: License for the skill (optional). + compatibility: Compatibility information for the skill (optional). + allowed_tools: Tool patterns the skill requires (optional, experimental). + metadata: Key-value pairs for client-specific properties (defaults to + empty dict). + """ + + name: str + description: str + license: Optional[str] = None + compatibility: Optional[str] = None + allowed_tools: Optional[str] = None + metadata: dict[str, str] = {} + + +class Script(BaseModel): + """Wrapper for script content.""" + + src: str + + def __str__(self) -> str: + """Returns the string representation of the script content. + + This ensures that any script type can be converted to a string, which is + useful for including the script in prompts or saving it to the file system. + """ + return self.src + + +class Resources(BaseModel): + """L3 skill content: additional instructions, assets, and scripts, loaded as needed. + + Attributes: + references: Additional markdown files with instructions, workflows, or + guidance. + assets: Resource materials like database schemas, API documentation, + templates, or examples. + scripts: Executable scripts that can be run via bash. + """ + + references: dict[str, str] = {} + assets: dict[str, str] = {} + scripts: dict[str, Script] = {} + + def get_reference(self, reference_id: str) -> Optional[str]: + """Get content of a reference file. + + Args: + reference_id: Unique path or name of the reference file. + + Returns: + Reference content as string, or None if not found + """ + return self.references.get(reference_id) + + def get_asset(self, asset_id: str) -> Optional[str]: + """Get content of an asset file. + + Args: + asset_id: Unique path or name of the asset file. + + Returns: + Asset content as string, or None if not found + """ + return self.assets.get(asset_id) + + def get_script(self, script_id: str) -> Optional[Script]: + """Get content of a script file. + + Args: + script_id: Unique path or name of the script file. + + Returns: + Script object, or None if not found + """ + return self.scripts.get(script_id) + + def list_references(self) -> list[str]: + """List all available reference paths.""" + return list(self.references.keys()) + + def list_assets(self) -> list[str]: + """List all available asset paths.""" + return list(self.assets.keys()) + + def list_scripts(self) -> list[str]: + """List all available script paths.""" + return list(self.scripts.keys()) + + +class Skill(BaseModel): + """Complete skill representation including frontmatter, instructions, and resources. + + A skill combines: + - L1: Frontmatter for discovery (name, description). + - L2: Instructions from SKILL.md body, loaded when skill is triggered. + - L3: Resources including additional instructions, assets, and scripts, + loaded as needed. + + Attributes: + frontmatter: Parsed skill frontmatter from SKILL.md. + instructions: L2 skill content: markdown instruction from SKILL.md body. + resources: L3 skill content: additional instructions, assets, and scripts. + """ + + frontmatter: Frontmatter + instructions: str + resources: Resources = Resources() + + @property + def name(self) -> str: + """Convenience property to access skill name.""" + return self.frontmatter.name + + @property + def description(self) -> str: + """Convenience property to access skill description.""" + return self.frontmatter.description diff --git a/src/google/adk/skills/prompt.py b/src/google/adk/skills/prompt.py new file mode 100644 index 00000000..e9840ab2 --- /dev/null +++ b/src/google/adk/skills/prompt.py @@ -0,0 +1,53 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module for skill prompt generation.""" + +from __future__ import annotations + +import html +from typing import List + +from . import models + + +def format_skills_as_xml(skills: List[models.Frontmatter]) -> str: + """Formats available skills into a standard XML string. + + Args: + skills: A list of skill frontmatter objects. + + Returns: + XML string with block containing each skill's + name and description. + """ + + if not skills: + return "\n" + + lines = [""] + + for skill in skills: + lines.append("") + lines.append("") + lines.append(html.escape(skill.name)) + lines.append("") + lines.append("") + lines.append(html.escape(skill.description)) + lines.append("") + lines.append("") + + lines.append("") + + return "\n".join(lines) diff --git a/tests/unittests/skills/__init__.py b/tests/unittests/skills/__init__.py new file mode 100644 index 00000000..58d482ea --- /dev/null +++ b/tests/unittests/skills/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/unittests/skills/test_models.py b/tests/unittests/skills/test_models.py new file mode 100644 index 00000000..6ecdd51f --- /dev/null +++ b/tests/unittests/skills/test_models.py @@ -0,0 +1,70 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for skill models.""" + +from google.adk.skills import models +import pytest + + +def test_frontmatter(): + """Tests Frontmatter model.""" + frontmatter = models.Frontmatter( + name="test-skill", + description="Test description", + license="Apache 2.0", + compatibility="test", + allowed_tools="test", + metadata={"key": "value"}, + ) + assert frontmatter.name == "test-skill" + assert frontmatter.description == "Test description" + assert frontmatter.license == "Apache 2.0" + assert frontmatter.compatibility == "test" + assert frontmatter.allowed_tools == "test" + assert frontmatter.metadata == {"key": "value"} + + +def test_resources(): + """Tests Resources model.""" + resources = models.Resources( + references={"ref1": "ref content"}, + assets={"asset1": "asset content"}, + scripts={"script1": models.Script(src="print('hello')")}, + ) + assert resources.get_reference("ref1") == "ref content" + assert resources.get_asset("asset1") == "asset content" + assert resources.get_script("script1").src == "print('hello')" + assert resources.get_reference("ref2") is None + assert resources.get_asset("asset2") is None + assert resources.get_script("script2") is None + assert resources.list_references() == ["ref1"] + assert resources.list_assets() == ["asset1"] + assert resources.list_scripts() == ["script1"] + + +def test_skill_properties(): + """Tests Skill model.""" + frontmatter = models.Frontmatter( + name="my-skill", description="my description" + ) + skill = models.Skill(frontmatter=frontmatter, instructions="do this") + assert skill.name == "my-skill" + assert skill.description == "my description" + + +def test_script_to_string(): + """Tests Script model.""" + script = models.Script(src="print('hello')") + assert str(script) == "print('hello')" diff --git a/tests/unittests/skills/test_prompt.py b/tests/unittests/skills/test_prompt.py new file mode 100644 index 00000000..f5395f3c --- /dev/null +++ b/tests/unittests/skills/test_prompt.py @@ -0,0 +1,49 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for prompt.""" + +from google.adk.skills import models +from google.adk.skills import prompt +import pytest + + +class TestPrompt: + + def test_format_skills_as_xml(self): + skills = [ + models.Frontmatter(name="skill1", description="desc1"), + models.Frontmatter(name="skill2", description="desc2"), + ] + xml = prompt.format_skills_as_xml(skills) + + assert "\nskill1\n" in xml + assert "\ndesc1\n" in xml + assert "" not in xml + assert "\nskill2\n" in xml + assert "\ndesc2\n" in xml + assert xml.startswith("") + assert xml.endswith("") + + def test_format_skills_as_xml_empty(self): + xml = prompt.format_skills_as_xml([]) + assert xml == "\n" + + def test_format_skills_as_xml_escaping(self): + skills = [ + models.Frontmatter(name="skill&name", description="desc"), + ] + xml = prompt.format_skills_as_xml(skills) + assert "skill&name" in xml + assert "desc<ription>" in xml