feat: Add load_skill_from_dir() method

This allows users to load skills from a directory and pass it into the SkillToolset constructor.

Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 868929937
This commit is contained in:
Kathy Wu
2026-02-11 18:10:54 -08:00
committed by Copybara-Service
parent b7f9110b52
commit 9f7d5b3f14
6 changed files with 192 additions and 4 deletions
+9 -2
View File
@@ -14,9 +14,10 @@
"""Example agent demonstrating the use of SkillToolset.""" """Example agent demonstrating the use of SkillToolset."""
import inspect import pathlib
from google.adk import Agent from google.adk import Agent
from google.adk.skills import load_skill_from_dir
from google.adk.skills import models from google.adk.skills import models
from google.adk.tools import skill_toolset from google.adk.tools import skill_toolset
@@ -39,7 +40,13 @@ greeting_skill = models.Skill(
), ),
) )
my_skill_toolset = skill_toolset.SkillToolset(skills=[greeting_skill]) weather_skill = load_skill_from_dir(
pathlib.Path(__file__).parent / "skills" / "weather_skill"
)
my_skill_toolset = skill_toolset.SkillToolset(
skills=[greeting_skill, weather_skill]
)
root_agent = Agent( root_agent = Agent(
model="gemini-2.5-flash", model="gemini-2.5-flash",
@@ -0,0 +1,7 @@
---
name: weather-skill
description: A skill that provides weather information based on reference data.
---
Step 1: Check 'references/weather_info.md' for the current weather.
Step 2: Provide the weather update to the user.
@@ -0,0 +1,6 @@
# Weather Information
- **Location:** San Francisco, CA
- **Condition:** Sunny ☀️
- **Temperature:** 72°F (22°C)
- **Forecast:** Clear skies all day.
+2 -2
View File
@@ -18,12 +18,12 @@ from .models import Frontmatter
from .models import Resources from .models import Resources
from .models import Script from .models import Script
from .models import Skill from .models import Skill
from .prompt import format_skills_as_xml from .utils import load_skill_from_dir
__all__ = [ __all__ = [
"Frontmatter", "Frontmatter",
"Resources", "Resources",
"Script", "Script",
"Skill", "Skill",
"format_skills_as_xml", "load_skill_from_dir",
] ]
+112
View File
@@ -0,0 +1,112 @@
# 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.
"""Utility functions for Agent Skills."""
from __future__ import annotations
import pathlib
from typing import Union
import yaml
from . import models
def _load_dir(directory: pathlib.Path) -> dict[str, str]:
"""Recursively load files from a directory into a dictionary.
Args:
directory: Path to the directory to load.
Returns:
Dictionary mapping relative file paths to their string content.
"""
files = {}
if directory.exists() and directory.is_dir():
for file_path in directory.rglob("*"):
if file_path.is_file():
relative_path = file_path.relative_to(directory)
files[str(relative_path)] = file_path.read_text(encoding="utf-8")
return files
def load_skill_from_dir(skill_dir: Union[str, pathlib.Path]) -> models.Skill:
"""Load a complete skill from a directory.
Args:
skill_dir: Path to the skill directory.
Returns:
Skill object with all components loaded.
Raises:
FileNotFoundError: If the skill directory or SKILL.md is not found.
ValueError: If SKILL.md is invalid.
"""
skill_dir = pathlib.Path(skill_dir).resolve()
if not skill_dir.is_dir():
raise FileNotFoundError(f"Skill directory '{skill_dir}' not found.")
skill_md = None
for name in ("SKILL.md", "skill.md"):
path = skill_dir / name
if path.exists():
skill_md = path
break
if skill_md is None:
raise FileNotFoundError(f"SKILL.md not found in '{skill_dir}'.")
content = skill_md.read_text(encoding="utf-8")
if not content.startswith("---"):
raise ValueError("SKILL.md must start with YAML frontmatter (---)")
parts = content.split("---", 2)
if len(parts) < 3:
raise ValueError("SKILL.md frontmatter not properly closed with ---")
frontmatter_str = parts[1]
body = parts[2].strip()
try:
parsed = yaml.safe_load(frontmatter_str)
except yaml.YAMLError as e:
raise ValueError(f"Invalid YAML in frontmatter: {e}") from e
if not isinstance(parsed, dict):
raise ValueError("SKILL.md frontmatter must be a YAML mapping")
# Frontmatter class handles required field validation
frontmatter = models.Frontmatter(**parsed)
references = _load_dir(skill_dir / "references")
assets = _load_dir(skill_dir / "assets")
raw_scripts = _load_dir(skill_dir / "scripts")
scripts = {
name: models.Script(src=content) for name, content in raw_scripts.items()
}
resources = models.Resources(
references=references,
assets=assets,
scripts=scripts,
)
return models.Skill(
frontmatter=frontmatter,
instructions=body,
resources=resources,
)
+56
View File
@@ -0,0 +1,56 @@
# 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 utilities."""
from google.adk.skills import load_skill_from_dir
import pytest
def test_load_skill_from_dir(tmp_path):
"""Tests loading a skill from a directory."""
skill_dir = tmp_path / "test-skill"
skill_dir.mkdir()
skill_md_content = """---
name: test-skill
description: Test description
---
Test instructions
"""
(skill_dir / "SKILL.md").write_text(skill_md_content)
# Create references
ref_dir = skill_dir / "references"
ref_dir.mkdir()
(ref_dir / "ref1.md").write_text("ref1 content")
# Create assets
assets_dir = skill_dir / "assets"
assets_dir.mkdir()
(assets_dir / "asset1.txt").write_text("asset1 content")
# Create scripts
scripts_dir = skill_dir / "scripts"
scripts_dir.mkdir()
(scripts_dir / "script1.sh").write_text("echo hello")
skill = load_skill_from_dir(skill_dir)
assert skill.name == "test-skill"
assert skill.description == "Test description"
assert skill.instructions == "Test instructions"
assert skill.resources.get_reference("ref1.md") == "ref1 content"
assert skill.resources.get_asset("asset1.txt") == "asset1 content"
assert skill.resources.get_script("script1.sh").src == "echo hello"