Files
yao-meta-skill/scripts/render_intent_dialogue.py
T
2026-04-19 11:47:24 +08:00

280 lines
11 KiB
Python

#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover
yaml = None
def parse_frontmatter(text: str) -> tuple[dict, str]:
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return {}, text
try:
end_index = lines[1:].index("---") + 1
except ValueError:
return {}, text
frontmatter = "\n".join(lines[1:end_index])
body = "\n".join(lines[end_index + 1 :]).lstrip()
if yaml is not None:
payload = yaml.safe_load(frontmatter) or {}
return payload if isinstance(payload, dict) else {}, body
data = {}
for line in frontmatter.splitlines():
if ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip().strip('"')
return data, body
def extract_title(body: str, fallback: str) -> str:
for line in body.splitlines():
if line.startswith("# "):
return line[2:].strip()
return fallback
def classify_focus(description: str) -> str:
lowered = description.lower()
if any(token in lowered for token in ["review", "audit", "incident", "risk", "govern"]):
return "quality-and-boundary"
if any(token in lowered for token in ["export", "package", "adapter", "client", "portable"]):
return "portability-and-contract"
if any(token in lowered for token in ["workflow", "coordinate", "orchestrate", "process"]):
return "execution-and-assets"
return "trigger-and-output"
def build_questions(focus: str) -> list[dict]:
base = [
{
"question": "If this skill worked beautifully, what recurring job would it reliably handle for the user every time?",
"why": "This reveals the real job-to-be-done and gives the package a humane center instead of a guessed prompt shape.",
},
{
"question": "When someone reaches for this skill in the real world, what materials will they actually hand to it?",
"why": "Input shape decides whether references, scripts, or templates are needed.",
},
{
"question": "What finished output should it hand back so the user can immediately keep moving?",
"why": "Outputs should drive the package structure before extra guidance is added.",
},
{
"question": "Which nearby requests should this skill politely refuse so the boundary stays clean?",
"why": "The exclusion list is the fastest route to better trigger quality.",
},
{
"question": "What matters most here: speed, consistency, auditability, portability, governance, or tone/style fit?",
"why": "Constraints decide how much structure, packaging, and review the skill actually needs.",
},
{
"question": "Do you already have any references you want this skill to learn from, such as a repo, product, page, workflow, or prompt example?",
"why": "A good reference can raise the quality bar quickly, but the skill should only borrow patterns and standards, never copy wording or confidential material.",
},
]
if focus == "quality-and-boundary":
base.append(
{
"question": "What failure would make this skill untrustworthy in practice?",
"why": "The answer usually reveals the first evaluation gate worth adding.",
}
)
elif focus == "portability-and-contract":
base.append(
{
"question": "Which environments or clients must be able to consume this skill?",
"why": "This sets the minimum metadata and degradation strategy.",
}
)
else:
base.append(
{
"question": "What repeated manual step should become a deterministic asset first?",
"why": "This usually reveals whether a script or reference should be created next.",
}
)
return base
def build_opening_styles() -> list[dict]:
return [
{
"label": "温柔陪伴型",
"best_when": "用户想法还散、还在试探,或者需要先被接住。",
"message": "我们先不急着把它说成一个很完整的 skill。你就像跟我聊天一样,先说说你最想让它以后稳稳接住哪类重复工作;如果它做得很理想,最后应该交回你一个什么结果。",
},
{
"label": "专业教练型",
"best_when": "用户目标比较明确,希望被高效带着走。",
"message": "我们先把这件事讲清楚,再决定 skill 怎么设计。你先告诉我三件事:它要接住的重复任务是什么,别人通常会给它什么材料,最后你希望它交付什么结果。",
},
{
"label": "共创伙伴型",
"best_when": "用户已经有一些想法,希望一起打磨,不想被填表。",
"message": "我们把这次当成一次共创。你先给我一个粗糙版本就行,我先帮你看它真正的核心任务是什么,再一起决定边界、结构和接下来最值的一步。",
},
]
def build_summary(skill_dir: Path) -> dict:
skill_text = (skill_dir / "SKILL.md").read_text(encoding="utf-8")
frontmatter, body = parse_frontmatter(skill_text)
name = frontmatter.get("name", skill_dir.name)
description = frontmatter.get("description", "No description found.")
title = extract_title(body, name.replace("-", " ").title())
focus = classify_focus(description)
questions = build_questions(focus)
opening_styles = build_opening_styles()
output = {
"capability_sentence": f"{title} should turn a recurring request into a reliable reusable output without widening the boundary unnecessarily.",
"required_capture": [
"recurring job",
"real inputs",
"required outputs",
"exclusions",
"constraints",
"reference preferences",
"first evaluation target",
],
"recommended_first_gate": "trigger and boundary" if focus != "portability-and-contract" else "portability and contract",
}
return {
"skill_name": name,
"title": title,
"description": description,
"focus": focus,
"opening_frame": "Let's start from the real work, the result you care about, and the standards that matter here. We can make the structure clearer after that.",
"reference_note": "If you already have examples you admire, bring them in. We will learn the pattern, not copy the source.",
"conversation_path": [
"Start with the user's own words, not package vocabulary.",
"Reflect the job, output, and non-goals back in one clean sentence.",
"Only then offer a tiny scaffold if it would help the user move faster.",
],
"opening_styles": opening_styles,
"optional_scaffold": [
"The repeated job it should reliably handle",
"The real inputs people will hand to it",
"The useful output it should hand back",
"What it should clearly refuse",
],
"questions": questions,
"output": output,
}
def render_markdown(summary: dict) -> str:
lines = [
"# Intent Dialogue",
"",
f"Skill: `{summary['skill_name']}`",
"",
"## Opening Frame",
"",
summary["opening_frame"],
"",
"## Opening Tone Options",
"",
]
for item in summary["opening_styles"]:
lines.extend(
[
f"### {item['label']}",
"",
f"- Best when: {item['best_when']}",
f"- Example: {item['message']}",
"",
]
)
lines.extend(
[
"## Conversation Path",
"",
]
)
for idx, item in enumerate(summary["conversation_path"], start=1):
lines.append(f"{idx}. {item}")
lines.extend(
[
"",
"## Why Start Here",
"",
"Use this short dialogue before deep authoring. The goal is to learn the real job, output, exclusions, and constraints so the first package is small but accurate.",
"",
"## Current Anchor",
"",
f"- Title: `{summary['title']}`",
f"- Description: {summary['description']}",
f"- Focus: `{summary['focus']}`",
f"- Reference note: {summary['reference_note']}",
"",
"## Questions To Ask",
"",
]
)
for idx, item in enumerate(summary["questions"], start=1):
lines.extend(
[
f"{idx}. {item['question']}",
f" Why: {item['why']}",
]
)
lines.extend(
[
"",
"## Capture Before Drafting",
"",
f"- Capability sentence: {summary['output']['capability_sentence']}",
f"- Recommended first gate: `{summary['output']['recommended_first_gate']}`",
"- Tiny optional scaffold:",
]
)
for item in summary["optional_scaffold"]:
lines.append(f" - {item}")
for item in summary["output"]["required_capture"]:
lines.append(f"- Capture: `{item}`")
return "\n".join(lines).strip() + "\n"
def render_intent_dialogue(skill_dir: Path, output_md: Path | None = None, output_json: Path | None = None) -> dict:
skill_dir = skill_dir.resolve()
reports_dir = skill_dir / "reports"
reports_dir.mkdir(parents=True, exist_ok=True)
output_md = output_md or reports_dir / "intent-dialogue.md"
output_json = output_json or reports_dir / "intent-dialogue.json"
summary = build_summary(skill_dir)
output_md.write_text(render_markdown(summary), encoding="utf-8")
output_json.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
return {
"ok": True,
"skill_dir": str(skill_dir),
"artifacts": {
"markdown": str(output_md),
"json": str(output_json),
},
"summary": summary,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Render an intent dialogue guide for a skill package.")
parser.add_argument("skill_dir", nargs="?", default=".")
parser.add_argument("--output-md")
parser.add_argument("--output-json")
args = parser.parse_args()
result = render_intent_dialogue(
Path(args.skill_dir),
output_md=Path(args.output_md).resolve() if args.output_md else None,
output_json=Path(args.output_json).resolve() if args.output_json else None,
)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()