Files
evotraders/backend/agents/skill_metadata.py
2026-03-30 17:46:44 +08:00

84 lines
2.5 KiB
Python

# -*- coding: utf-8 -*-
"""Skill metadata parsing helpers for SKILL.md files."""
from dataclasses import dataclass, field
from pathlib import Path
from typing import List
import yaml
@dataclass(frozen=True)
class SkillMetadata:
"""Parsed metadata for a skill package."""
skill_name: str
path: Path
source: str
name: str
description: str
version: str = ""
tools: List[str] = field(default_factory=list)
allowed_tools: List[str] = field(default_factory=list)
denied_tools: List[str] = field(default_factory=list)
def parse_skill_metadata(skill_dir: Path, source: str) -> SkillMetadata:
"""Parse SKILL.md frontmatter with a forgiving schema."""
skill_name = skill_dir.name
skill_file = skill_dir / "SKILL.md"
if not skill_file.exists():
return SkillMetadata(
skill_name=skill_name,
path=skill_dir,
source=source,
name=skill_name,
description="",
)
raw = skill_file.read_text(encoding="utf-8").strip()
frontmatter = {}
body = raw
if raw.startswith("---"):
parts = raw.split("---", 2)
if len(parts) >= 3:
try:
frontmatter = yaml.safe_load(parts[1].strip()) or {}
except yaml.YAMLError:
frontmatter = {}
body = parts[2].strip()
if not isinstance(frontmatter, dict):
frontmatter = {}
description = str(frontmatter.get("description") or "").strip()
if not description and body:
description = body.splitlines()[0].strip().lstrip("#").strip()
return SkillMetadata(
skill_name=skill_name,
path=skill_dir,
source=source,
name=str(frontmatter.get("name") or skill_name).strip() or skill_name,
description=description,
version=str(frontmatter.get("version") or "").strip(),
tools=_string_list(frontmatter.get("tools")),
allowed_tools=_string_list(frontmatter.get("allowed_tools")),
denied_tools=_string_list(frontmatter.get("denied_tools")),
)
def _string_list(value) -> List[str]:
if isinstance(value, str):
item = value.strip()
return [item] if item else []
if not isinstance(value, list):
return []
seen: List[str] = []
for item in value:
if not isinstance(item, str):
continue
normalized = item.strip()
if normalized and normalized not in seen:
seen.append(normalized)
return seen