diff --git a/tools/skill_writer.py b/tools/skill_writer.py new file mode 100644 index 0000000..7ecaddb --- /dev/null +++ b/tools/skill_writer.py @@ -0,0 +1,165 @@ +""" +Skill Writer — creates and updates teacher skill directories. +Adapted from colleague-skill's skill_writer.py for Buddhist teacher context. +""" + +import json +import os +import shutil +from datetime import datetime +from typing import Optional + +try: + from pypinyin import lazy_pinyin, Style + HAS_PYPINYIN = True +except ImportError: + HAS_PYPINYIN = False + + +SKILL_MD_TEMPLATE = """--- +name: teacher_{slug} +description: 依据{name}({tradition}{school})的教学风格与教义体系 +user-invocable: true +--- + +# {name} + +{disclaimer} + +--- + +## PART A — 教义体系 + +{teaching_content} + +## PART B — 说法风格 + +{voice_content} + +## 运行规则 + +1. 收到提问后,先依据 voice.md Layer 0 硬规则检查 +2. 依据 voice.md Layer 1-3 确定回答的风格和方式 +3. 依据 teaching.md 检索相关教义内容 +4. 以该法师的风格组织回答 +5. 必须附经文出处,格式:【《经名》卷N】→ https://fojin.app/texts/{text_id} +6. 遇到超出范围的问题,坦诚说明并建议查阅相关传承 +""" + +DISCLAIMER = "本内容依据历史佛教文献生成,仅供参考学习。如需正式修行指导,请亲近善知识。所有回答均附经文出处,可通过 FoJin (fojin.app) 查阅原文。" + + +def slugify(name: str) -> str: + """Convert teacher name to URL-safe slug.""" + if HAS_PYPINYIN: + pinyin_list = lazy_pinyin(name, style=Style.NORMAL) + slug = "-".join(pinyin_list) + else: + slug = name.lower().replace(" ", "-") + slug = "".join(c for c in slug if c.isalnum() or c == "-") + slug = slug.strip("-") + return slug + + +def create_teacher( + base_dir: str, + name: str, + tradition: str, + school: str, + era: str, + languages: list, + teaching_content: str, + voice_content: str, + fojin_entity_id: Optional[str] = None, + sources: Optional[list] = None, +) -> str: + """Create a new teacher skill directory.""" + slug = slugify(name) + teacher_dir = os.path.join(base_dir, slug) + os.makedirs(teacher_dir, exist_ok=True) + os.makedirs(os.path.join(teacher_dir, "versions"), exist_ok=True) + + with open(os.path.join(teacher_dir, "teaching.md"), "w", encoding="utf-8") as f: + f.write(teaching_content) + + with open(os.path.join(teacher_dir, "voice.md"), "w", encoding="utf-8") as f: + f.write(voice_content) + + skill_content = SKILL_MD_TEMPLATE.format( + slug=slug, name=name, tradition=tradition, school=school, + disclaimer=DISCLAIMER, teaching_content=teaching_content, + voice_content=voice_content, + ) + with open(os.path.join(teacher_dir, "SKILL.md"), "w", encoding="utf-8") as f: + f.write(skill_content) + + meta = { + "name": name, "slug": slug, "tradition": tradition, "school": school, + "era": era, "languages": languages, "fojin_entity_id": fojin_entity_id, + "sources": sources or [], "version": "1.0.0", + "created_at": datetime.now().strftime("%Y-%m-%d"), + "updated_at": datetime.now().strftime("%Y-%m-%d"), + "disclaimer": DISCLAIMER, + } + with open(os.path.join(teacher_dir, "meta.json"), "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + + return teacher_dir + + +def update_teacher(teacher_dir: str, teaching_patch: Optional[str] = None, voice_patch: Optional[str] = None) -> str: + """Update an existing teacher skill with new content. Archives current version before updating.""" + meta_path = os.path.join(teacher_dir, "meta.json") + with open(meta_path, "r", encoding="utf-8") as f: + meta = json.load(f) + + version = meta.get("version", "1.0.0") + version_dir = os.path.join(teacher_dir, "versions", f"v{version}") + os.makedirs(version_dir, exist_ok=True) + for fname in ["SKILL.md", "teaching.md", "voice.md", "meta.json"]: + src = os.path.join(teacher_dir, fname) + if os.path.exists(src): + shutil.copy2(src, version_dir) + + if teaching_patch: + with open(os.path.join(teacher_dir, "teaching.md"), "a", encoding="utf-8") as f: + f.write("\n\n" + teaching_patch) + + if voice_patch: + with open(os.path.join(teacher_dir, "voice.md"), "a", encoding="utf-8") as f: + f.write("\n\n" + voice_patch) + + parts = version.split(".") + parts[1] = str(int(parts[1]) + 1) + new_version = ".".join(parts) + + meta["version"] = new_version + meta["updated_at"] = datetime.now().strftime("%Y-%m-%d") + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + + teaching_content = open(os.path.join(teacher_dir, "teaching.md"), encoding="utf-8").read() + voice_content = open(os.path.join(teacher_dir, "voice.md"), encoding="utf-8").read() + skill_content = SKILL_MD_TEMPLATE.format( + slug=meta["slug"], name=meta["name"], tradition=meta["tradition"], + school=meta["school"], disclaimer=DISCLAIMER, + teaching_content=teaching_content, voice_content=voice_content, + ) + with open(os.path.join(teacher_dir, "SKILL.md"), "w", encoding="utf-8") as f: + f.write(skill_content) + + return new_version + + +def list_teachers(base_dir: str) -> list: + """List all teacher skills in a directory.""" + teachers = [] + if not os.path.exists(base_dir): + return teachers + for entry in sorted(os.listdir(base_dir)): + meta_path = os.path.join(base_dir, entry, "meta.json") + if os.path.isfile(meta_path): + with open(meta_path, "r", encoding="utf-8") as f: + meta = json.load(f) + teachers.append(meta) + return teachers