from pathlib import Path import yaml from pydantic import BaseModel class Skill(BaseModel): id: str title: str description: str version: int tags: list[str] = [] required_tools: list[str] = [] risk_level: str = "low" inputs: list[str] = [] outputs: list[str] = [] success_criteria: list[str] = [] procedure: str = "" examples: str = "" notes: str = "" class SkillCandidate(BaseModel): skill: Skill score: float reason: str class SkillRegistry: def __init__(self, skills_dir: str = "skills"): self.skills_dir = Path(skills_dir) self._cache: dict[str, Skill] | None = None def load_skills(self) -> list[Skill]: skills: dict[str, Skill] = {} if not self.skills_dir.exists(): self._cache = {} return [] for path in sorted(self.skills_dir.glob("*/skill.yaml")): data = yaml.safe_load(path.read_text()) or {} root = path.parent data["procedure"] = self._read_optional(root / "procedure.md") data["examples"] = self._read_optional(root / "examples.md") data["notes"] = self._read_optional(root / "notes.md") skill = Skill(**data) skills[skill.id] = skill self._cache = skills return list(skills.values()) def get_skill(self, skill_id: str) -> Skill | None: if self._cache is None: self.load_skills() return (self._cache or {}).get(skill_id) async def find_candidate_skills(self, user_request: str, limit: int = 3) -> list[SkillCandidate]: terms = set(user_request.lower().split()) candidates: list[SkillCandidate] = [] for skill in self.load_skills(): haystack = " ".join([skill.title, skill.description, " ".join(skill.tags)]).lower() score = sum(1 for term in terms if term in haystack) if score: candidates.append( SkillCandidate(skill=skill, score=float(score), reason="keyword match") ) return sorted(candidates, key=lambda item: item.score, reverse=True)[:limit] def _read_optional(self, path: Path) -> str: return path.read_text() if path.exists() else ""