82 lines
2.4 KiB
Python
82 lines
2.4 KiB
Python
"""Skill 注册表 (Anthropic 标准格式)。
|
|
|
|
每个 skill 是 skills/<name>/ 目录,内含 SKILL.md(带 frontmatter)+ 可选的
|
|
references/、scripts/、assets/。启动时只读 frontmatter 做 discovery,完整 SKILL.md
|
|
和 references 由 agent 按需加载(渐进披露)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Dict, Optional, Tuple
|
|
|
|
import yaml
|
|
|
|
|
|
_FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n?", re.DOTALL)
|
|
|
|
|
|
def parse_frontmatter(text: str) -> Tuple[dict, str]:
|
|
"""解析 markdown 顶部的 YAML frontmatter。返回 (meta, body)。"""
|
|
m = _FRONTMATTER_RE.match(text)
|
|
if not m:
|
|
return {}, text
|
|
meta = yaml.safe_load(m.group(1)) or {}
|
|
if not isinstance(meta, dict):
|
|
meta = {}
|
|
return meta, text[m.end():]
|
|
|
|
|
|
@dataclass
|
|
class Skill:
|
|
name: str
|
|
description: str
|
|
skill_dir: Path
|
|
|
|
@property
|
|
def skill_md(self) -> Path:
|
|
return self.skill_dir / "SKILL.md"
|
|
|
|
def full_content(self) -> str:
|
|
return self.skill_md.read_text(encoding="utf-8")
|
|
|
|
@classmethod
|
|
def from_dir(cls, skill_dir: Path) -> Optional["Skill"]:
|
|
md = skill_dir / "SKILL.md"
|
|
if not md.exists():
|
|
return None
|
|
meta, _ = parse_frontmatter(md.read_text(encoding="utf-8"))
|
|
name = meta.get("name") or skill_dir.name
|
|
desc = meta.get("description") or ""
|
|
if not desc:
|
|
return None # description 是 discovery 的关键,缺了不收
|
|
return cls(name=name, description=desc, skill_dir=skill_dir)
|
|
|
|
|
|
class SkillRegistry:
|
|
def __init__(self, skills_dir: Path) -> None:
|
|
self.skills_dir = Path(skills_dir)
|
|
self.skills: Dict[str, Skill] = {}
|
|
self._scan()
|
|
|
|
def _scan(self) -> None:
|
|
if not self.skills_dir.exists():
|
|
return
|
|
for child in sorted(self.skills_dir.iterdir()):
|
|
if not child.is_dir():
|
|
continue
|
|
skill = Skill.from_dir(child)
|
|
if skill is not None:
|
|
self.skills[skill.name] = skill
|
|
|
|
def discovery_block(self) -> str:
|
|
"""启动时注入 system prompt 的 skill 列表(name + description)。"""
|
|
if not self.skills:
|
|
return ""
|
|
lines = [f"- **{s.name}**: {s.description}" for s in self.skills.values()]
|
|
return "\n".join(lines)
|
|
|
|
def get(self, name: str) -> Optional[Skill]:
|
|
return self.skills.get(name)
|