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 <wukathy@google.com>
PiperOrigin-RevId: 866270203
This commit is contained in:
Kathy Wu
2026-02-05 22:05:39 -08:00
committed by Copybara-Service
parent 483c5bab94
commit c7362100eb
6 changed files with 362 additions and 0 deletions
+29
View File
@@ -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",
]
+148
View File
@@ -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
+53
View File
@@ -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 <available_skills> block containing each skill's
name and description.
"""
if not skills:
return "<available_skills>\n</available_skills>"
lines = ["<available_skills>"]
for skill in skills:
lines.append("<skill>")
lines.append("<name>")
lines.append(html.escape(skill.name))
lines.append("</name>")
lines.append("<description>")
lines.append(html.escape(skill.description))
lines.append("</description>")
lines.append("</skill>")
lines.append("</available_skills>")
return "\n".join(lines)
+13
View File
@@ -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.
+70
View File
@@ -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')"
+49
View File
@@ -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 "<name>\nskill1\n</name>" in xml
assert "<description>\ndesc1\n</description>" in xml
assert "<location>" not in xml
assert "<name>\nskill2\n</name>" in xml
assert "<description>\ndesc2\n</description>" in xml
assert xml.startswith("<available_skills>")
assert xml.endswith("</available_skills>")
def test_format_skills_as_xml_empty(self):
xml = prompt.format_skills_as_xml([])
assert xml == "<available_skills>\n</available_skills>"
def test_format_skills_as_xml_escaping(self):
skills = [
models.Frontmatter(name="skill&name", description="desc<ription>"),
]
xml = prompt.format_skills_as_xml(skills)
assert "skill&amp;name" in xml
assert "desc&lt;ription&gt;" in xml