Download mini_agent/tools/skill_loader.py from AbdulElahGwaith/Mini-Agent: direct link, hf CLI and curl.
- Browser
- Download file 8.82 kB
-
https://huggingface.co/AbdulElahGwaith/Mini-Agent/resolve/main/mini_agent/tools/skill_loader.py
- Command line
-
hf download hf://AbdulElahGwaith/Mini-Agent/mini_agent/tools/skill_loader.py
-
curl -L -o skill_loader.py https://huggingface.co/AbdulElahGwaith/Mini-Agent/resolve/main/mini_agent/tools/skill_loader.py
8.82 kB
| """ | |
| Skill Loader - Load Claude Skills | |
| Supports loading skills from SKILL.md files and providing them to Agent | |
| """ | |
| import re | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Dict, List, Optional | |
| import yaml | |
| class Skill: | |
| """Skill data structure""" | |
| name: str | |
| description: str | |
| content: str | |
| license: Optional[str] = None | |
| allowed_tools: Optional[List[str]] = None | |
| metadata: Optional[Dict[str, str]] = None | |
| skill_path: Optional[Path] = None | |
| def to_prompt(self) -> str: | |
| """Convert skill to prompt format""" | |
| # Inject skill root directory path for context | |
| skill_root = str(self.skill_path.parent) if self.skill_path else "unknown" | |
| return f""" | |
| # Skill: {self.name} | |
| {self.description} | |
| **Skill Root Directory:** `{skill_root}` | |
| All files and references in this skill are relative to this directory. | |
| --- | |
| {self.content} | |
| """ | |
| class SkillLoader: | |
| """Skill loader""" | |
| def __init__(self, skills_dir: str = "./skills"): | |
| """ | |
| Initialize Skill Loader | |
| Args: | |
| skills_dir: Skills directory path | |
| """ | |
| self.skills_dir = Path(skills_dir) | |
| self.loaded_skills: Dict[str, Skill] = {} | |
| def load_skill(self, skill_path: Path) -> Optional[Skill]: | |
| """ | |
| Load single skill from SKILL.md file | |
| Args: | |
| skill_path: SKILL.md file path | |
| Returns: | |
| Skill object, or None if loading fails | |
| """ | |
| try: | |
| content = skill_path.read_text(encoding="utf-8") | |
| # Parse YAML frontmatter | |
| frontmatter_match = re.match(r"^---\n(.*?)\n---\n(.*)$", content, re.DOTALL) | |
| if not frontmatter_match: | |
| print(f"⚠️ {skill_path} missing YAML frontmatter") | |
| return None | |
| frontmatter_text = frontmatter_match.group(1) | |
| skill_content = frontmatter_match.group(2).strip() | |
| # Parse YAML | |
| try: | |
| frontmatter = yaml.safe_load(frontmatter_text) | |
| except yaml.YAMLError as e: | |
| print(f"❌ Failed to parse YAML frontmatter: {e}") | |
| return None | |
| # Required fields | |
| if "name" not in frontmatter or "description" not in frontmatter: | |
| print(f"⚠️ {skill_path} missing required fields (name or description)") | |
| return None | |
| # Get skill directory (parent of SKILL.md) | |
| skill_dir = skill_path.parent | |
| # Replace relative paths in content with absolute paths | |
| # This ensures scripts and resources can be found from any working directory | |
| processed_content = self._process_skill_paths(skill_content, skill_dir) | |
| # Create Skill object | |
| skill = Skill( | |
| name=frontmatter["name"], | |
| description=frontmatter["description"], | |
| content=processed_content, | |
| license=frontmatter.get("license"), | |
| allowed_tools=frontmatter.get("allowed-tools"), | |
| metadata=frontmatter.get("metadata"), | |
| skill_path=skill_path, | |
| ) | |
| return skill | |
| except Exception as e: | |
| print(f"❌ Failed to load skill ({skill_path}): {e}") | |
| return None | |
| def _process_skill_paths(self, content: str, skill_dir: Path) -> str: | |
| """ | |
| Process skill content to replace relative paths with absolute paths. | |
| Supports Progressive Disclosure Level 3+: converts relative file references | |
| to absolute paths so Agent can easily read nested resources. | |
| Args: | |
| content: Original skill content | |
| skill_dir: Skill directory path | |
| Returns: | |
| Processed content with absolute paths | |
| """ | |
| import re | |
| # Pattern 1: Directory-based paths (scripts/, references/, assets/) | |
| # See https://agentskills.io/specification#optional-directories | |
| def replace_dir_path(match): | |
| prefix = match.group(1) # e.g., "python " or "`" | |
| rel_path = match.group(2) # e.g., "scripts/with_server.py" | |
| abs_path = skill_dir / rel_path | |
| if abs_path.exists(): | |
| return f"{prefix}{abs_path}" | |
| return match.group(0) | |
| pattern_dirs = r"(python\s+|`)((?:scripts|references|assets)/[^\s`\)]+)" | |
| content = re.sub(pattern_dirs, replace_dir_path, content) | |
| # Pattern 2: Direct markdown/document references (forms.md, reference.md, etc.) | |
| # Matches phrases like "see reference.md" or "read forms.md" | |
| def replace_doc_path(match): | |
| prefix = match.group(1) # e.g., "see ", "read " | |
| filename = match.group(2) # e.g., "reference.md" | |
| suffix = match.group(3) # e.g., punctuation | |
| abs_path = skill_dir / filename | |
| if abs_path.exists(): | |
| # Add helpful instruction for Agent | |
| return f"{prefix}`{abs_path}` (use read_file to access){suffix}" | |
| return match.group(0) | |
| # Match patterns like: "see reference.md" or "read forms.md" | |
| pattern_docs = r"(see|read|refer to|check)\s+([a-zA-Z0-9_-]+\.(?:md|txt|json|yaml))([.,;\s])" | |
| content = re.sub(pattern_docs, replace_doc_path, content, flags=re.IGNORECASE) | |
| # Pattern 3: Markdown links - supports multiple formats: | |
| # - [`filename.md`](filename.md) - simple filename | |
| # - [text](./reference/file.md) - relative path with ./ | |
| # - [text](scripts/file.js) - directory-based path | |
| # Matches patterns like: "Read [`docx-js.md`](docx-js.md)" or "Load [Guide](./reference/guide.md)" | |
| def replace_markdown_link(match): | |
| prefix = match.group(1) if match.group(1) else "" # e.g., "Read ", "Load ", or empty | |
| link_text = match.group(2) # e.g., "`docx-js.md`" or "Guide" | |
| filepath = match.group(3) # e.g., "docx-js.md", "./reference/file.md", "scripts/file.js" | |
| # Remove leading ./ if present | |
| clean_path = filepath[2:] if filepath.startswith("./") else filepath | |
| abs_path = skill_dir / clean_path | |
| if abs_path.exists(): | |
| # Preserve the link text style (with or without backticks) | |
| return f"{prefix}[{link_text}](`{abs_path}`) (use read_file to access)" | |
| return match.group(0) | |
| # Match markdown link patterns with optional prefix words | |
| # Captures: (optional prefix word) [link text] (complete file path including ./) | |
| pattern_markdown = ( | |
| r"(?:(Read|See|Check|Refer to|Load|View)\s+)?\[(`?[^`\]]+`?)\]\(((?:\./)?[^)]+\.(?:md|txt|json|yaml|js|py|html))\)" | |
| ) | |
| content = re.sub(pattern_markdown, replace_markdown_link, content, flags=re.IGNORECASE) | |
| return content | |
| def discover_skills(self) -> List[Skill]: | |
| """ | |
| Discover and load all skills in the skills directory | |
| Returns: | |
| List of Skills | |
| """ | |
| skills = [] | |
| if not self.skills_dir.exists(): | |
| print(f"⚠️ Skills directory does not exist: {self.skills_dir}") | |
| return skills | |
| # Recursively find all SKILL.md files | |
| for skill_file in self.skills_dir.rglob("SKILL.md"): | |
| skill = self.load_skill(skill_file) | |
| if skill: | |
| skills.append(skill) | |
| self.loaded_skills[skill.name] = skill | |
| return skills | |
| def get_skill(self, name: str) -> Optional[Skill]: | |
| """ | |
| Get loaded skill | |
| Args: | |
| name: Skill name | |
| Returns: | |
| Skill object, or None if not found | |
| """ | |
| return self.loaded_skills.get(name) | |
| def list_skills(self) -> List[str]: | |
| """ | |
| List all loaded skill names | |
| Returns: | |
| List of skill names | |
| """ | |
| return list(self.loaded_skills.keys()) | |
| def get_skills_metadata_prompt(self) -> str: | |
| """ | |
| Generate prompt containing ONLY metadata (name + description) for all skills. | |
| This implements Progressive Disclosure - Level 1. | |
| Returns: | |
| Metadata-only prompt string | |
| """ | |
| if not self.loaded_skills: | |
| return "" | |
| prompt_parts = ["## Available Skills\n"] | |
| prompt_parts.append("You have access to specialized skills. Each skill provides expert guidance for specific tasks.\n") | |
| prompt_parts.append("Load a skill's full content using the appropriate skill tool when needed.\n") | |
| # List all skills with their descriptions | |
| for skill in self.loaded_skills.values(): | |
| prompt_parts.append(f"- `{skill.name}`: {skill.description}") | |
| return "\n".join(prompt_parts) | |