"""
SkillLoader for loading and managing character skills.
"""
from pathlib import Path
from typing import Any, Dict, List, Optional
import yaml
from .skill import Skill
class SkillLoader:
"""Loads and manages character skills."""
def __init__(self, skills_dir: Path, voice_refs_dir: Path):
"""
Initialize the skill loader.
Args:
skills_dir: Directory containing skill subdirectories with SKILL.md files.
voice_refs_dir: Directory containing voice reference audio files.
"""
self.skills_dir = Path(skills_dir)
self.voice_refs_dir = Path(voice_refs_dir)
self._cache: Dict[str, Skill] = {}
def list_skills(self) -> List[Dict[str, str]]:
"""List all available skills."""
skills = []
if not self.skills_dir.exists():
return skills
for skill_dir in self.skills_dir.iterdir():
if not skill_dir.is_dir():
continue
skill_file = skill_dir / "SKILL.md"
if not skill_file.exists():
continue
# Parse just the frontmatter for listing
try:
meta = self._parse_frontmatter(skill_file)
skills.append(
{
"id": meta.get("id", skill_dir.name),
"name": meta.get("name", skill_dir.name),
"display_name": meta.get(
"display_name", meta.get("name", skill_dir.name)
),
"description": meta.get("description", "No description"),
"emoji": meta.get("emoji", ""),
}
)
except Exception as e:
print(f"Warning: Failed to parse {skill_file}: {e}")
return skills
def load_skill(self, skill_id: str) -> Optional[Skill]:
"""Load a skill by ID."""
if skill_id in self._cache:
return self._cache[skill_id]
# Find skill directory - check various naming patterns
skill_dir = None
for d in self.skills_dir.iterdir():
if not d.is_dir():
continue
# Match by directory name or by ID in SKILL.md
if d.name == skill_id or d.name.startswith(f"{skill_id}-"):
skill_dir = d
break
# Also check the SKILL.md for matching ID
skill_file = d / "SKILL.md"
if skill_file.exists():
try:
meta = self._parse_frontmatter(skill_file)
if meta.get("id") == skill_id:
skill_dir = d
break
except Exception:
pass
if not skill_dir:
print(f"Skill not found: {skill_id}")
return None
skill_file = skill_dir / "SKILL.md"
if not skill_file.exists():
print(f"SKILL.md not found in {skill_dir}")
return None
# Parse full skill
try:
meta, body = self._parse_skill_file(skill_file)
# Find voice file
voice_file = None
voice_name = meta.get("voice", f"{skill_id}.wav")
for voice_path in [
skill_dir / voice_name,
self.voice_refs_dir / voice_name,
skill_dir / "reference.wav",
]:
if voice_path.exists():
voice_file = voice_path
break
# Find avatar
avatar_file = None
avatar_name = meta.get("avatar", f"{skill_id}.png")
for avatar_path in [
skill_dir / avatar_name,
skill_dir / "avatar.png",
]:
if avatar_path.exists():
avatar_file = avatar_path
break
skill = Skill(
id=meta.get("id", skill_id),
name=meta.get("name", skill_id),
display_name=meta.get("display_name", meta.get("name", skill_id)),
description=meta.get("description", ""),
system_prompt=body,
emoji=meta.get("emoji", ""),
voice_file=voice_file,
avatar_file=avatar_file,
initial_memories=meta.get("initial_memories", []),
metadata=meta.get("metadata", {}),
personality_traits=meta.get("personality_traits", []),
speech_patterns=meta.get("speech_patterns", []),
allowed_tools=meta.get("allowed_tools", []),
output_filters=meta.get("output_filters", []),
input_validation=meta.get("input_validation", {}),
)
self._cache[skill_id] = skill
return skill
except Exception as e:
print(f"Error loading skill {skill_id}: {e}")
import traceback
traceback.print_exc()
return None
def _parse_frontmatter(self, path: Path) -> Dict[str, Any]:
"""Parse YAML frontmatter from a markdown file."""
content = path.read_text(encoding="utf-8")
if not content.startswith("---"):
return {}
# Find end of frontmatter
end_idx = content.find("---", 3)
if end_idx == -1:
return {}
yaml_content = content[3:end_idx].strip()
return yaml.safe_load(yaml_content) or {}
def _parse_skill_file(self, path: Path) -> tuple[Dict[str, Any], str]:
"""Parse skill file, returning (frontmatter, body)."""
content = path.read_text(encoding="utf-8")
if not content.startswith("---"):
return {}, content
# Find end of frontmatter
end_idx = content.find("---", 3)
if end_idx == -1:
return {}, content
yaml_content = content[3:end_idx].strip()
body = content[end_idx + 3 :].strip()
meta = yaml.safe_load(yaml_content) or {}
return meta, body
def clear_cache(self) -> None:
"""Clear the skill cache."""
self._cache.clear()