mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
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:
committed by
Copybara-Service
parent
b7f9110b52
commit
9f7d5b3f14
@@ -14,9 +14,10 @@
|
||||
|
||||
"""Example agent demonstrating the use of SkillToolset."""
|
||||
|
||||
import inspect
|
||||
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.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(
|
||||
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.
|
||||
@@ -18,12 +18,12 @@ from .models import Frontmatter
|
||||
from .models import Resources
|
||||
from .models import Script
|
||||
from .models import Skill
|
||||
from .prompt import format_skills_as_xml
|
||||
from .utils import load_skill_from_dir
|
||||
|
||||
__all__ = [
|
||||
"Frontmatter",
|
||||
"Resources",
|
||||
"Script",
|
||||
"Skill",
|
||||
"format_skills_as_xml",
|
||||
"load_skill_from_dir",
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user