-
Notifications
You must be signed in to change notification settings - Fork 818
Add skills to deepagents CLI #315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: rlm/fetch-tool
Are you sure you want to change the base?
Changes from 1 commit
6e3e456
7785b93
098854e
d4daf97
ccd5a7c
620c2b4
ccfe875
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| """Skill loader for parsing and loading agent skills from SKILL.md files. | ||
|
|
||
| This module implements Anthropic's agent skills pattern with YAML frontmatter parsing. | ||
| Each skill is a directory containing a SKILL.md file with: | ||
| - YAML frontmatter (name, description required) | ||
| - Markdown instructions for the agent | ||
| - Optional supporting files (scripts, configs, etc.) | ||
|
|
||
| Example SKILL.md structure: | ||
| ```markdown | ||
| --- | ||
| name: web-research | ||
| description: Structured approach to conducting thorough web research | ||
| --- | ||
|
|
||
| # Web Research Skill | ||
|
|
||
| ## When to Use | ||
| - User asks you to research a topic | ||
| ... | ||
| ``` | ||
| """ | ||
|
|
||
| import re | ||
| from pathlib import Path | ||
| from typing import TypedDict | ||
|
|
||
|
|
||
| class SkillMetadata(TypedDict): | ||
| """Metadata for a skill.""" | ||
|
|
||
| name: str | ||
| """Name of the skill.""" | ||
|
|
||
| description: str | ||
| """Description of what the skill does.""" | ||
|
|
||
| path: str | ||
| """Path to the SKILL.md file.""" | ||
|
|
||
|
|
||
| class SkillLoader: | ||
| """Loader for agent skills with YAML frontmatter parsing. | ||
|
|
||
| Skills are organized as: | ||
| skills/ | ||
| ├── skill-name/ | ||
| │ ├── SKILL.md # Required: instructions with YAML frontmatter | ||
| │ ├── script.py # Optional: supporting files | ||
| │ └── config.json # Optional: supporting files | ||
|
|
||
| Example: | ||
| ```python | ||
| loader = SkillLoader(skills_dir="~/.deepagents/skills") | ||
| skills = loader.load_skills() | ||
| for skill in skills: | ||
| print(f"{skill['name']}: {skill['description']}") | ||
| ``` | ||
| """ | ||
|
|
||
| def __init__(self, skills_dir: str | Path = "~/.deepagents/skills") -> None: | ||
| """Initialize the skill loader. | ||
|
|
||
| Args: | ||
| skills_dir: Path to the skills directory. Defaults to ~/.deepagents/skills | ||
| """ | ||
| self.skills_dir = Path(skills_dir).expanduser() | ||
| self.skills: list[SkillMetadata] = [] | ||
|
|
||
| def _parse_skill_metadata(self, skill_md_path: Path) -> SkillMetadata | None: | ||
| """Parse YAML frontmatter from a SKILL.md file. | ||
|
|
||
| Args: | ||
| skill_md_path: Path to the SKILL.md file. | ||
|
|
||
| Returns: | ||
| SkillMetadata with name, description, and path, or None if parsing fails. | ||
| """ | ||
| try: | ||
| content = skill_md_path.read_text(encoding="utf-8") | ||
|
|
||
| # Match YAML frontmatter between --- delimiters | ||
| frontmatter_pattern = r"^---\s*\n(.*?)\n---\s*\n" | ||
| match = re.match(frontmatter_pattern, content, re.DOTALL) | ||
|
|
||
| if not match: | ||
| return None | ||
|
|
||
| frontmatter = match.group(1) | ||
|
|
||
| # Parse key-value pairs from YAML (simple parsing, no nested structures) | ||
| metadata: dict[str, str] = {} | ||
| for line in frontmatter.split("\n"): | ||
| # Match "key: value" pattern | ||
| kv_match = re.match(r"^(\w+):\s*(.+)$", line.strip()) | ||
| if kv_match: | ||
| key, value = kv_match.groups() | ||
| metadata[key] = value.strip() | ||
|
|
||
| # Validate required fields | ||
| if "name" not in metadata or "description" not in metadata: | ||
| return None | ||
|
|
||
| return SkillMetadata( | ||
| name=metadata["name"], | ||
| description=metadata["description"], | ||
| path=str(skill_md_path), | ||
| ) | ||
|
|
||
| except Exception: | ||
|
||
| # Silently skip malformed files | ||
| return None | ||
|
|
||
| def load_skills(self) -> list[SkillMetadata]: | ||
|
||
| """Load all skills from the skills directory. | ||
|
|
||
| Scans the skills directory for subdirectories containing SKILL.md files, | ||
| parses YAML frontmatter, and returns skill metadata. | ||
|
|
||
| Returns: | ||
| List of skill metadata dictionaries with name, description, and path. | ||
| """ | ||
| self.skills = [] | ||
|
||
|
|
||
| # Check if skills directory exists | ||
| if not self.skills_dir.exists(): | ||
| return self.skills | ||
|
|
||
| # Iterate through subdirectories | ||
| for skill_dir in self.skills_dir.iterdir(): | ||
| if not skill_dir.is_dir(): | ||
| continue | ||
|
|
||
| # Look for SKILL.md file | ||
| skill_md_path = skill_dir / "SKILL.md" | ||
| if not skill_md_path.exists(): | ||
| continue | ||
|
|
||
| # Parse metadata | ||
| metadata = self._parse_skill_metadata(skill_md_path) | ||
| if metadata: | ||
| self.skills.append(metadata) | ||
|
|
||
| return self.skills | ||
|
|
||
| def get_skill_names(self) -> list[str]: | ||
|
||
| """Get list of loaded skill names. | ||
|
|
||
| Returns: | ||
| List of skill names. | ||
| """ | ||
| return [skill["name"] for skill in self.skills] | ||
|
|
||
| def format_skills_for_system_message(self) -> str: | ||
| """Format skills metadata for injection into system prompt. | ||
|
|
||
| Creates a formatted list of skills with their descriptions and paths, | ||
| following Anthropic's progressive disclosure pattern. | ||
|
|
||
| Returns: | ||
| Formatted string for system prompt. | ||
| """ | ||
| if not self.skills: | ||
| return "No skills available." | ||
|
|
||
| lines = ["Available skills:"] | ||
| for skill in self.skills: | ||
| lines.append(f"- **{skill['name']}**: {skill['description']}") | ||
| lines.append(f" Read `/skills/{Path(skill['path']).parent.name}/SKILL.md` for details") | ||
|
|
||
| return "\n".join(lines) | ||
|
|
||
|
|
||
| def load_skills(skills_dir: str | Path = "~/.deepagents/skills") -> tuple[list[SkillMetadata], str]: | ||
| """Convenience function to load skills and format for system prompt. | ||
|
|
||
| Args: | ||
| skills_dir: Path to the skills directory. | ||
|
|
||
| Returns: | ||
| Tuple of (skills metadata list, formatted string for system prompt). | ||
| """ | ||
| loader = SkillLoader(skills_dir) | ||
| skills = loader.load_skills() | ||
| formatted = loader.format_skills_for_system_message() | ||
| return skills, formatted | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's avoid hard-coding a defaul there, and if possible restrict input to just Path. This isn't user facing, so wide inputs are bad