Spaces:
Sleeping
Sleeping
File size: 9,803 Bytes
a5c7816 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | #!/usr/bin/env python
"""The DIY Creator team (#17 of the 30-in-15) — three role-specialized agents that hand off to
each other to produce a vetted build guide.
🔍 Researcher topic -> a structured brief (materials, tools, steps, safety, pitfalls)
✍️ Writer topic + brief -> a clean step-by-step DIY guide (markdown)
🧐 Critic topic + guide -> a scored verdict + concrete issues (the "diamond": an
INDEPENDENT reviewer, ideally a different model)
✍️ Writer + critique -> a revised guide addressing every issue
The lesson is multi-agent orchestration: specialized roles, explicit handoffs, and a
critique -> revise LOOP that keeps going until the critic passes (or we hit the round cap).
Same idea as a real editorial team — nobody ships their own first draft.
"""
import json
import llm
PASS_SCORE = 7 # critic score (0-10) at or above which the guide ships
MAX_REVISIONS = 2 # how many times the writer may revise before we ship the best we have
# ---- role system prompts -------------------------------------------------------------------
RESEARCHER_SYS = (
"You are the RESEARCHER on a DIY build team. Given a project the user wants to make, produce a "
"concise, PRACTICAL research brief the writer will turn into a guide. Draw on real maker knowledge: "
"correct materials with rough quantities, the actual tools needed, the core techniques, honest "
"difficulty and time, a rough cost range, the SAFETY hazards that genuinely apply, and the mistakes "
"beginners really make. Be specific and grounded — no filler. "
'Reply ONLY JSON: {"project":"...","difficulty":"beginner|intermediate|advanced",'
'"time_estimate":"...","cost_estimate":"...","materials":["qty + item", ...],"tools":["...", ...],'
'"techniques":["...", ...],"safety":["...", ...],"common_mistakes":["...", ...],'
'"key_considerations":["...", ...]}'
)
WRITER_SYS = (
"You are the WRITER on a DIY build team. Using the RESEARCH BRIEF, write a clear, encouraging, "
"start-to-finish build guide a motivated beginner could actually follow. Use this markdown structure:\n"
"## Overview (1-2 sentences: what they'll build and why it's worth it)\n"
"**Difficulty:** … · **Time:** … · **Cost:** …\n"
"## Materials (bullet list, with quantities)\n"
"## Tools (bullet list)\n"
"## Steps (numbered; each step is a concrete action with the how, not just the what; fold the "
"relevant safety note INTO the step it applies to)\n"
"## Tips & Common Mistakes (bullets)\n"
"Ground everything in the brief; do not invent materials or tools that weren't researched. Write the "
"guide itself — no preamble, no 'here is your guide'. Output MARKDOWN only."
)
CRITIC_SYS = (
"You are the CRITIC on a DIY build team — an INDEPENDENT reviewer, not the author. Your job is to catch "
"what would make a beginner fail, get hurt, or give up. Review the GUIDE against the RESEARCH BRIEF and "
"real-world build sense. Look hard for: missing or out-of-order steps, materials/tools used in a step but "
"not listed, unsafe or missing safety guidance, vague instructions, and skipped prep or cleanup. "
"Be strict but fair; do not invent problems, and do NOT flag mere style or nice-to-haves as serious. "
"Tag every issue by severity, using these definitions precisely:\n"
" • HIGH = would cause injury or a failed/unsafe build (missing critical safety gear, a structural "
"mistake, or a genuinely missing essential step)\n"
" • MEDIUM = would actually block or seriously confuse a beginner (an essential measurement, material, "
"or tool that a step needs but isn't given)\n"
" • LOW = polish only (a helpful diagram, extra clarity, a nice-to-have) — the build still succeeds "
"without it\n"
"A guide PASSES when a beginner could build it safely and successfully — i.e. no HIGH or MEDIUM issues "
"remain. LOW issues are fine to ship with. Prefer LOW unless an issue truly blocks or endangers the build. "
'Reply ONLY JSON: {"score":0-10,"pass":true/false,"summary":"one honest sentence",'
'"issues":[{"severity":"high|medium|low","issue":"what is wrong and the fix"}, ...]}'
)
REVISER_SYS = (
"You are the WRITER revising your DIY guide after an INDEPENDENT critic reviewed it. Produce an improved "
"guide that fixes EVERY issue raised — add missing steps, list every material/tool a step uses, work in "
"the safety guidance, and clarify vague instructions — while keeping the same clean markdown structure "
"(Overview / Difficulty·Time·Cost / Materials / Tools / Steps / Tips & Common Mistakes). Keep what already "
"worked; change what the critique flagged. Output the full revised guide as MARKDOWN only — no commentary."
)
# ---- the four handoffs ---------------------------------------------------------------------
def research(topic: str) -> dict:
prompt = f"Project the user wants to build: {topic}"
# generous token budget so the full JSON brief isn't truncated (truncation -> parse fail -> empty brief)
brief = llm.json_call(RESEARCHER_SYS, prompt, role="researcher", max_tokens=1800)
if not (isinstance(brief, dict) and brief.get("materials")):
brief = llm.json_call(RESEARCHER_SYS, prompt, role="researcher", max_tokens=1800, temperature=0.35)
if not isinstance(brief, dict):
brief = {"project": topic, "_note": "researcher returned unstructured output"}
for k in ("materials", "tools", "techniques", "safety", "common_mistakes", "key_considerations"):
brief.setdefault(k, [])
return brief
def write(topic: str, brief: dict) -> str:
user = f"PROJECT: {topic}\n\nRESEARCH BRIEF:\n{json.dumps(brief, indent=2)}\n\nWrite the build guide."
return llm.chat(WRITER_SYS, user, role="writer", temperature=0.55, max_tokens=1700)
def critique(topic: str, brief: dict, guide: str) -> dict:
user = (f"PROJECT: {topic}\n\nRESEARCH BRIEF:\n{json.dumps(brief, indent=2)}\n\n"
f"GUIDE TO REVIEW:\n{guide}")
verdict = llm.json_call(CRITIC_SYS, user, role="critic", max_tokens=1100)
if not isinstance(verdict, dict):
verdict = {"summary": "critic returned unstructured output", "issues": []}
issues = verdict.get("issues") if isinstance(verdict.get("issues"), list) else []
verdict["issues"] = [i for i in issues if isinstance(i, dict) and i.get("issue")]
# The GATE is deterministic from the issues the critic found — not the model's own number, which
# small models score inconsistently. Score follows severity; a guide only PASSES with no high/medium.
highs = sum(1 for i in verdict["issues"] if i.get("severity") == "high")
meds = sum(1 for i in verdict["issues"] if i.get("severity") == "medium")
lows = sum(1 for i in verdict["issues"] if i.get("severity") == "low")
if highs:
score = max(2, 4 - (highs - 1))
elif meds:
score = max(5, 6 - (meds - 1))
elif lows:
score = 7 if lows > 2 else 8
else:
score = 10
verdict["score"] = float(score)
verdict["pass"] = (highs == 0 and meds == 0)
verdict["counts"] = {"high": highs, "medium": meds, "low": lows}
return verdict
def revise(topic: str, brief: dict, guide: str, verdict: dict) -> str:
issues = "\n".join(f"- [{i.get('severity','?')}] {i.get('issue','')}" for i in verdict.get("issues", []))
user = (f"PROJECT: {topic}\n\nRESEARCH BRIEF:\n{json.dumps(brief, indent=2)}\n\n"
f"YOUR PREVIOUS GUIDE:\n{guide}\n\nCRITIC SCORE: {verdict.get('score')}/10 — {verdict.get('summary','')}\n"
f"ISSUES TO FIX:\n{issues or '(none listed)'}\n\nRewrite the guide fixing every issue.")
return llm.chat(REVISER_SYS, user, role="writer", temperature=0.5, max_tokens=1800)
# ---- CLI orchestrator (for local testing; the web app drives the same steps step-by-step) --
def run(topic: str, verbose: bool = True):
def say(*a):
if verbose:
print(*a)
say(f"\n=== DIY CREATOR TEAM ===\nPROJECT: {topic}\n")
say("🔍 Researcher — gathering the brief …")
brief = research(topic)
say(f" materials: {len(brief.get('materials', []))} · tools: {len(brief.get('tools', []))} · "
f"safety: {len(brief.get('safety', []))} · difficulty: {brief.get('difficulty','?')}")
say("✍️ Writer — drafting the guide …")
guide = write(topic, brief)
rounds = []
for r in range(1, MAX_REVISIONS + 1):
say(f"🧐 Critic — reviewing (round {r}) …")
verdict = critique(topic, brief, guide)
rounds.append(verdict)
say(f" score {verdict['score']:.1f}/10 — {'PASS' if verdict['pass'] else 'REVISE'} — "
f"{verdict.get('summary','')}")
for i in verdict.get("issues", [])[:6]:
say(f" · [{i.get('severity','?')}] {i.get('issue','')[:90]}")
if verdict["pass"]:
break
if r < MAX_REVISIONS:
say("✍️ Writer — revising to address the critique …")
guide = revise(topic, brief, guide, verdict)
say("\n=== FINAL GUIDE ===\n")
say(guide)
return {"brief": brief, "guide": guide, "rounds": rounds}
if __name__ == "__main__":
import sys
try:
sys.stdout.reconfigure(encoding="utf-8") # so the emoji role labels print on any console
except Exception:
pass
topic = " ".join(sys.argv[1:]) or "a simple wooden phone stand"
run(topic)
|