Skip to content

Commit 83b973c

Browse files
Merge branch 'main' into fix/precise-timestamp-timezone-postgresql
2 parents 0c74725 + c736210 commit 83b973c

6 files changed

Lines changed: 362 additions & 0 deletions

File tree

src/google/adk/skills/__init__.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Agent Development Kit - Skills."""
16+
17+
from .models import Frontmatter
18+
from .models import Resources
19+
from .models import Script
20+
from .models import Skill
21+
from .prompt import format_skills_as_xml
22+
23+
__all__ = [
24+
"Frontmatter",
25+
"Resources",
26+
"Script",
27+
"Skill",
28+
"format_skills_as_xml",
29+
]

src/google/adk/skills/models.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Data models for Agent Skills."""
16+
17+
from __future__ import annotations
18+
19+
from typing import Optional
20+
21+
from pydantic import BaseModel
22+
23+
24+
class Frontmatter(BaseModel):
25+
"""L1 skill content: metadata parsed from SKILL.md frontmatter for skill discovery.
26+
27+
Attributes:
28+
name: Skill name in kebab-case (required).
29+
description: What the skill does and when the model should use it
30+
(required).
31+
license: License for the skill (optional).
32+
compatibility: Compatibility information for the skill (optional).
33+
allowed_tools: Tool patterns the skill requires (optional, experimental).
34+
metadata: Key-value pairs for client-specific properties (defaults to
35+
empty dict).
36+
"""
37+
38+
name: str
39+
description: str
40+
license: Optional[str] = None
41+
compatibility: Optional[str] = None
42+
allowed_tools: Optional[str] = None
43+
metadata: dict[str, str] = {}
44+
45+
46+
class Script(BaseModel):
47+
"""Wrapper for script content."""
48+
49+
src: str
50+
51+
def __str__(self) -> str:
52+
"""Returns the string representation of the script content.
53+
54+
This ensures that any script type can be converted to a string, which is
55+
useful for including the script in prompts or saving it to the file system.
56+
"""
57+
return self.src
58+
59+
60+
class Resources(BaseModel):
61+
"""L3 skill content: additional instructions, assets, and scripts, loaded as needed.
62+
63+
Attributes:
64+
references: Additional markdown files with instructions, workflows, or
65+
guidance.
66+
assets: Resource materials like database schemas, API documentation,
67+
templates, or examples.
68+
scripts: Executable scripts that can be run via bash.
69+
"""
70+
71+
references: dict[str, str] = {}
72+
assets: dict[str, str] = {}
73+
scripts: dict[str, Script] = {}
74+
75+
def get_reference(self, reference_id: str) -> Optional[str]:
76+
"""Get content of a reference file.
77+
78+
Args:
79+
reference_id: Unique path or name of the reference file.
80+
81+
Returns:
82+
Reference content as string, or None if not found
83+
"""
84+
return self.references.get(reference_id)
85+
86+
def get_asset(self, asset_id: str) -> Optional[str]:
87+
"""Get content of an asset file.
88+
89+
Args:
90+
asset_id: Unique path or name of the asset file.
91+
92+
Returns:
93+
Asset content as string, or None if not found
94+
"""
95+
return self.assets.get(asset_id)
96+
97+
def get_script(self, script_id: str) -> Optional[Script]:
98+
"""Get content of a script file.
99+
100+
Args:
101+
script_id: Unique path or name of the script file.
102+
103+
Returns:
104+
Script object, or None if not found
105+
"""
106+
return self.scripts.get(script_id)
107+
108+
def list_references(self) -> list[str]:
109+
"""List all available reference paths."""
110+
return list(self.references.keys())
111+
112+
def list_assets(self) -> list[str]:
113+
"""List all available asset paths."""
114+
return list(self.assets.keys())
115+
116+
def list_scripts(self) -> list[str]:
117+
"""List all available script paths."""
118+
return list(self.scripts.keys())
119+
120+
121+
class Skill(BaseModel):
122+
"""Complete skill representation including frontmatter, instructions, and resources.
123+
124+
A skill combines:
125+
- L1: Frontmatter for discovery (name, description).
126+
- L2: Instructions from SKILL.md body, loaded when skill is triggered.
127+
- L3: Resources including additional instructions, assets, and scripts,
128+
loaded as needed.
129+
130+
Attributes:
131+
frontmatter: Parsed skill frontmatter from SKILL.md.
132+
instructions: L2 skill content: markdown instruction from SKILL.md body.
133+
resources: L3 skill content: additional instructions, assets, and scripts.
134+
"""
135+
136+
frontmatter: Frontmatter
137+
instructions: str
138+
resources: Resources = Resources()
139+
140+
@property
141+
def name(self) -> str:
142+
"""Convenience property to access skill name."""
143+
return self.frontmatter.name
144+
145+
@property
146+
def description(self) -> str:
147+
"""Convenience property to access skill description."""
148+
return self.frontmatter.description

src/google/adk/skills/prompt.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Module for skill prompt generation."""
16+
17+
from __future__ import annotations
18+
19+
import html
20+
from typing import List
21+
22+
from . import models
23+
24+
25+
def format_skills_as_xml(skills: List[models.Frontmatter]) -> str:
26+
"""Formats available skills into a standard XML string.
27+
28+
Args:
29+
skills: A list of skill frontmatter objects.
30+
31+
Returns:
32+
XML string with <available_skills> block containing each skill's
33+
name and description.
34+
"""
35+
36+
if not skills:
37+
return "<available_skills>\n</available_skills>"
38+
39+
lines = ["<available_skills>"]
40+
41+
for skill in skills:
42+
lines.append("<skill>")
43+
lines.append("<name>")
44+
lines.append(html.escape(skill.name))
45+
lines.append("</name>")
46+
lines.append("<description>")
47+
lines.append(html.escape(skill.description))
48+
lines.append("</description>")
49+
lines.append("</skill>")
50+
51+
lines.append("</available_skills>")
52+
53+
return "\n".join(lines)

tests/unittests/skills/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Unit tests for skill models."""
16+
17+
from google.adk.skills import models
18+
import pytest
19+
20+
21+
def test_frontmatter():
22+
"""Tests Frontmatter model."""
23+
frontmatter = models.Frontmatter(
24+
name="test-skill",
25+
description="Test description",
26+
license="Apache 2.0",
27+
compatibility="test",
28+
allowed_tools="test",
29+
metadata={"key": "value"},
30+
)
31+
assert frontmatter.name == "test-skill"
32+
assert frontmatter.description == "Test description"
33+
assert frontmatter.license == "Apache 2.0"
34+
assert frontmatter.compatibility == "test"
35+
assert frontmatter.allowed_tools == "test"
36+
assert frontmatter.metadata == {"key": "value"}
37+
38+
39+
def test_resources():
40+
"""Tests Resources model."""
41+
resources = models.Resources(
42+
references={"ref1": "ref content"},
43+
assets={"asset1": "asset content"},
44+
scripts={"script1": models.Script(src="print('hello')")},
45+
)
46+
assert resources.get_reference("ref1") == "ref content"
47+
assert resources.get_asset("asset1") == "asset content"
48+
assert resources.get_script("script1").src == "print('hello')"
49+
assert resources.get_reference("ref2") is None
50+
assert resources.get_asset("asset2") is None
51+
assert resources.get_script("script2") is None
52+
assert resources.list_references() == ["ref1"]
53+
assert resources.list_assets() == ["asset1"]
54+
assert resources.list_scripts() == ["script1"]
55+
56+
57+
def test_skill_properties():
58+
"""Tests Skill model."""
59+
frontmatter = models.Frontmatter(
60+
name="my-skill", description="my description"
61+
)
62+
skill = models.Skill(frontmatter=frontmatter, instructions="do this")
63+
assert skill.name == "my-skill"
64+
assert skill.description == "my description"
65+
66+
67+
def test_script_to_string():
68+
"""Tests Script model."""
69+
script = models.Script(src="print('hello')")
70+
assert str(script) == "print('hello')"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Unit tests for prompt."""
16+
17+
from google.adk.skills import models
18+
from google.adk.skills import prompt
19+
import pytest
20+
21+
22+
class TestPrompt:
23+
24+
def test_format_skills_as_xml(self):
25+
skills = [
26+
models.Frontmatter(name="skill1", description="desc1"),
27+
models.Frontmatter(name="skill2", description="desc2"),
28+
]
29+
xml = prompt.format_skills_as_xml(skills)
30+
31+
assert "<name>\nskill1\n</name>" in xml
32+
assert "<description>\ndesc1\n</description>" in xml
33+
assert "<location>" not in xml
34+
assert "<name>\nskill2\n</name>" in xml
35+
assert "<description>\ndesc2\n</description>" in xml
36+
assert xml.startswith("<available_skills>")
37+
assert xml.endswith("</available_skills>")
38+
39+
def test_format_skills_as_xml_empty(self):
40+
xml = prompt.format_skills_as_xml([])
41+
assert xml == "<available_skills>\n</available_skills>"
42+
43+
def test_format_skills_as_xml_escaping(self):
44+
skills = [
45+
models.Frontmatter(name="skill&name", description="desc<ription>"),
46+
]
47+
xml = prompt.format_skills_as_xml(skills)
48+
assert "skill&amp;name" in xml
49+
assert "desc&lt;ription&gt;" in xml

0 commit comments

Comments
 (0)