69 lines
2.2 KiB
Python
69 lines
2.2 KiB
Python
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 ""
|