Skills › Scientific Agent Skills › autoskill › synthesize.py
synthesize.py
79 lines · skills/autoskill/scripts/synthesize.py at 5f2bc03
1import json
2import re
4VALID_VERDICTS = {"reuse", "compose", "novel"}
7class SynthesisError(Exception):
8 pass
11def _build_prompt(cluster, top_k_skills):
12 apps = ", ".join(cluster["apps"])
13 titles = "; ".join(cluster.get("example_titles", []))
14 candidates = "\n".join(
15 f"- {s['name']} (score={s['score']:.2f}): {s['description']}"
16 for s in top_k_skills
17 )
18 return f"""You are classifying an observed user workflow against an existing skill library.
20Cluster:
21- apps: {apps}
22- sessions: {cluster['session_count']}
23- total_duration_seconds: {cluster['total_duration_seconds']}
24- example titles: {titles}
26Candidate existing skills (ranked by semantic similarity):
27{candidates}
29Decide one of:
30- "reuse": an existing skill already covers this workflow.
31- "compose": no single skill covers it, but chaining existing skills does. Draft a thin SKILL.md that invokes them in order.
32- "novel": not covered; draft a full new SKILL.md.
34Respond with a single JSON object:
35- reuse: {{"verdict": "reuse", "target": "<skill-name>"}}
36- compose: {{"verdict": "compose", "name": "<new-skill-name>", "skill_body": "<SKILL.md body>"}}
37- novel: {{"verdict": "novel", "name": "<new-skill-name>", "skill_body": "<SKILL.md body>"}}
38"""
41def _extract_json(text: str) -> dict:
42 fence = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
43 if fence:
44 candidate = fence.group(1)
45 else:
46 start = text.find("{")
47 end = text.rfind("}")
48 if start == -1 or end == -1 or end <= start:
49 raise SynthesisError(f"no JSON object found in response: {text!r}")
50 candidate = text[start:end + 1]
51 try:
52 return json.loads(candidate)
53 except json.JSONDecodeError as e:
54 raise SynthesisError(f"invalid JSON in response: {e}") from e
57def synthesize(cluster, top_k_skills, backend):
58 prompt = _build_prompt(cluster, top_k_skills)
59 response = backend(prompt)
60 payload = _extract_json(response)
62 verdict = payload.get("verdict")
63 if verdict not in VALID_VERDICTS:
64 raise SynthesisError(f"unknown verdict: {verdict!r}")
66 result = {"verdict": verdict, "skill_body": None}
67 if verdict == "reuse":
68 result["target"] = payload.get("target")
69 else:
70 name = payload.get("name")
71 body = payload.get("skill_body")
72 if (not isinstance(name, str) or not 1 <= len(name) <= 64
73 or re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name) is None):
74 raise SynthesisError("draft name must be a valid skill name, not a path")
75 if not isinstance(body, str) or not body.strip():
76 raise SynthesisError("draft skill_body must be a non-empty string")
77 result["name"] = name
78 result["skill_body"] = body
79 return result