Spaces:
Sleeping
Sleeping
Download app.py from BonusLockSMith/diy-creator: direct link, hf CLI and curl.
- Browser
- Download file 16 kB
-
https://huggingface.co/spaces/BonusLockSMith/diy-creator/resolve/main/app.py
- Command line
-
hf download hf://spaces/BonusLockSMith/diy-creator/app.py
-
curl -L -o app.py https://huggingface.co/spaces/BonusLockSMith/diy-creator/resolve/main/app.py
16 kB
| #!/usr/bin/env python | |
| """DIY Creator (#17 of the 30-in-15) — a multi-agent team you can watch work. | |
| A 🔍 Researcher, an ✍️ Writer, and a 🧐 Critic hand off to each other to produce a vetted build guide. | |
| The critic scores the draft and sends it back until it passes — a real critique->revise loop, shown live. | |
| The lesson is multi-agent orchestration: role-specialized agents, explicit handoffs, and an | |
| independent critic (the "diamond") gating the result. Each step is its own endpoint so the browser | |
| renders the team working in real time. | |
| pip install -r requirements.txt | |
| python app.py # http://127.0.0.1:7860 (local Ollama by default; ANTHROPIC_API_KEY for Claude) | |
| """ | |
| import os | |
| from flask import Flask, request, render_template_string, jsonify | |
| import agents | |
| import llm | |
| app = Flask(__name__) | |
| MAX_TOPIC = 160 | |
| PAGE = r"""<!doctype html><html lang="en"><head> | |
| <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>GritAI · DIY Creator</title> | |
| <link rel="stylesheet" href="/static/gritai.css"> | |
| <style> | |
| .wrap{max-width:1000px;margin:0 auto;padding:22px 20px 40px} | |
| .lede{color:var(--dim);font-size:14.5px;line-height:1.6;margin:6px 0 18px;max-width:70ch} | |
| .roles{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 18px} | |
| .rolechip{font-family:var(--mono);font-size:11px;letter-spacing:.04em;border:1px solid var(--line2); | |
| border-radius:20px;padding:5px 11px;color:var(--dim);background:var(--panel)} | |
| .composer{display:flex;gap:10px} | |
| .composer input{flex:1;background:var(--panel);border:1px solid var(--line2);border-radius:3px;color:var(--ink); | |
| font-family:var(--body);font-size:16px;padding:14px 16px;outline:none} | |
| .composer input:focus{border-color:var(--grit)} | |
| .go{border:0;background:var(--grit);color:#140a06;font-family:var(--mono);font-weight:600;font-size:12px; | |
| letter-spacing:.12em;text-transform:uppercase;padding:0 22px;cursor:pointer;border-radius:3px;white-space:nowrap} | |
| .go:hover{background:var(--grit2)} .go:disabled{opacity:.5;cursor:default} | |
| .examples{margin:10px 0 0;display:flex;gap:7px;flex-wrap:wrap} | |
| .ex{font-size:12px;color:var(--dim2);border:1px dashed var(--line2);border-radius:3px;padding:4px 9px;cursor:pointer} | |
| .ex:hover{color:var(--grit);border-color:var(--grit)} | |
| #lane{margin-top:26px;display:flex;flex-direction:column;gap:14px} | |
| .card{border:1px solid var(--line);border-radius:4px;background:var(--panel);overflow:hidden; | |
| animation:rise .35s ease} | |
| @keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}} | |
| .card .hd{display:flex;align-items:center;gap:10px;padding:11px 15px;border-bottom:1px solid var(--line); | |
| background:var(--panel2)} | |
| .card .av{font-size:18px} | |
| .card .role{font-family:var(--disp);font-size:13px;letter-spacing:.05em;text-transform:uppercase} | |
| .card.researcher .role{color:#6fb6ff} .card.writer .role{color:var(--grit)} .card.critic .role{color:#c58cff} | |
| .card .st{margin-left:auto;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim2)} | |
| .card .bd{padding:14px 16px;font-size:14px;line-height:1.65} | |
| .working .st{color:var(--grit)} | |
| .dots::after{content:'';animation:dots 1.2s steps(4,end) infinite} | |
| @keyframes dots{0%{content:''}25%{content:'.'}50%{content:'..'}75%{content:'...'}} | |
| .brief{display:grid;grid-template-columns:repeat(3,1fr);gap:8px 18px;margin-bottom:10px} | |
| @media(max-width:680px){.brief{grid-template-columns:1fr 1fr}} | |
| .brief .m{font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim2)} | |
| .brief .v{font-size:14px;color:var(--ink)} | |
| .cols{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px} | |
| @media(max-width:760px){.cols{grid-template-columns:1fr}} | |
| .col h5{font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);margin:0 0 6px} | |
| .col ul{margin:0;padding-left:16px} .col li{font-size:13px;margin:3px 0;color:var(--ink2,#cfd3dc)} | |
| .gate{display:flex;align-items:center;gap:12px;margin-bottom:12px} | |
| .score{font-family:var(--disp);font-size:26px;font-weight:700} .score small{font-size:13px;color:var(--dim2)} | |
| .badge{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;padding:4px 10px;border-radius:3px} | |
| .badge.pass{background:rgba(95,208,138,.14);color:var(--ok);border:1px solid var(--ok)} | |
| .badge.revise{background:rgba(255,90,42,.13);color:var(--grit);border:1px solid var(--grit)} | |
| .issues{list-style:none;padding:0;margin:0} | |
| .issues li{display:flex;gap:9px;align-items:flex-start;padding:7px 0;border-top:1px solid var(--line);font-size:13.5px} | |
| .sev{font-family:var(--mono);font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;padding:2px 7px;border-radius:3px;margin-top:1px;white-space:nowrap} | |
| .sev.high{background:rgba(255,107,107,.15);color:var(--bad)} .sev.medium{background:rgba(255,176,32,.15);color:#ffb020} | |
| .sev.low{background:var(--panel2);color:var(--dim)} | |
| .final{border:1px solid var(--grit);border-radius:4px;overflow:hidden;margin-top:8px} | |
| .final .hd{background:linear-gradient(90deg,rgba(255,90,42,.16),transparent);border-bottom:1px solid var(--grit)} | |
| .guide h2{font-family:var(--disp);font-size:17px;color:var(--ink);margin:16px 0 7px;letter-spacing:.01em} | |
| .guide h2:first-child{margin-top:0} | |
| .guide p{margin:7px 0} .guide ul,.guide ol{margin:7px 0;padding-left:22px} .guide li{margin:4px 0} | |
| .guide strong{color:var(--grit)} .guide code{font-family:var(--mono);font-size:.9em;background:var(--panel2);padding:1px 5px;border-radius:3px} | |
| .flow{text-align:center;color:var(--line2);font-size:16px;margin:-6px 0 -4px} | |
| .err{color:var(--bad);font-size:13.5px} | |
| .muted{color:var(--dim2);font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;margin-top:22px} | |
| </style></head><body> | |
| <div class="wrap"> | |
| <header style="display:flex;align-items:center;gap:13px"> | |
| <svg width="34" height="34" viewBox="0 0 38 38" fill="none"><rect x="1" y="1" width="36" height="36" rx="2" stroke="#2b303c"/><path d="M12 26l14-14M12 12h14v14" stroke="#ff5a2a" stroke-width="2.4" stroke-linecap="square"/></svg> | |
| <div class="brand"><div class="co">Grit<b>AI</b> · DIY Creator</div> | |
| <div class="sub">A research → write → critique team that builds you a vetted guide</div></div> | |
| <div style="flex:1"></div><div class="tag hot">#17 · 30-in-15</div> | |
| </header> | |
| <p class="lede">Name something you want to build. A <b style="color:#6fb6ff">Researcher</b> gathers the | |
| materials, tools and safety; a <b style="color:var(--grit)">Writer</b> drafts a step-by-step guide; then an | |
| independent <b style="color:#c58cff">Critic</b> scores it and sends it back for revisions until it passes. | |
| You watch the whole team work.</p> | |
| <div class="composer"> | |
| <input id="topic" maxlength="160" placeholder="e.g. a raised garden bed, a floating bookshelf, a cold frame…" autofocus> | |
| <button class="go" id="go" onclick="run()">Assemble the team</button> | |
| </div> | |
| <div class="examples" id="ex"></div> | |
| <div id="lane"></div> | |
| <p class="muted">Backend: {{ backend }} · max {{ max_rev }} critic rounds · GritAI Solutions</p> | |
| </div> | |
| <script> | |
| const MAX_ROUNDS = {{ max_rev }}; | |
| const EX = ["a raised garden bed from cedar boards","a floating bookshelf","a cold frame for winter greens", | |
| "a pegboard tool wall","a simple wooden phone stand","a worm composting bin"]; | |
| const lane=document.getElementById('lane'), goBtn=document.getElementById('go'), topicEl=document.getElementById('topic'); | |
| const exBox=document.getElementById('ex'); | |
| EX.forEach(t=>{const s=document.createElement('span');s.className='ex';s.textContent=t; | |
| s.onclick=()=>{topicEl.value=t;run();};exBox.appendChild(s);}); | |
| function esc(s){return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');} | |
| function post(url,body){return fetch(url,{method:'POST',headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify(body)}).then(r=>r.json());} | |
| // tiny, safe markdown -> html for the guide (headings, bold, code, ordered/unordered lists) | |
| function md(src){ | |
| const lines=esc(src).split(/\r?\n/); let html='', list=null; | |
| const close=()=>{ if(list){html+=`</${list}>`; list=null;} }; | |
| for(let ln of lines){ | |
| let m; | |
| if(/^\s*##\s+/.test(ln)){close(); html+='<h2>'+inline(ln.replace(/^\s*##\s+/,''))+'</h2>'; continue;} | |
| if((m=ln.match(/^\s*\d+\.\s+(.*)/))){ if(list!=='ol'){close();list='ol';html+='<ol>';} html+='<li>'+inline(m[1])+'</li>'; continue;} | |
| if((m=ln.match(/^\s*[-*]\s+(.*)/))){ if(list!=='ul'){close();list='ul';html+='<ul>';} html+='<li>'+inline(m[1])+'</li>'; continue;} | |
| close(); if(ln.trim()) html+='<p>'+inline(ln)+'</p>'; | |
| } | |
| close(); return html; | |
| } | |
| function inline(s){return s.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/`(.+?)`/g,'<code>$1</code>');} | |
| function card(cls,av,role,statusHtml,bodyHtml){ | |
| const d=document.createElement('div'); d.className='card '+cls; | |
| d.innerHTML=`<div class="hd"><span class="av">${av}</span><span class="role">${role}</span> | |
| <span class="st">${statusHtml}</span></div><div class="bd">${bodyHtml}</div>`; | |
| lane.appendChild(d); d.scrollIntoView({behavior:'smooth',block:'end'}); return d; | |
| } | |
| function flow(){const d=document.createElement('div');d.className='flow';d.textContent='↓';lane.appendChild(d);} | |
| function setStatus(c,html,working){c.querySelector('.st').innerHTML=html; c.classList.toggle('working',!!working);} | |
| function setBody(c,html){c.querySelector('.bd').innerHTML=html;} | |
| function briefHtml(b){ | |
| const list=(t,a)=>`<div class="col"><h5>${t}</h5><ul>${(a||[]).map(x=>'<li>'+esc(x)+'</li>').join('')||'<li>—</li>'}</ul></div>`; | |
| return `<div class="brief"> | |
| <div><div class="m">Difficulty</div><div class="v">${esc(b.difficulty||'—')}</div></div> | |
| <div><div class="m">Time</div><div class="v">${esc(b.time_estimate||'—')}</div></div> | |
| <div><div class="m">Est. cost</div><div class="v">${esc(b.cost_estimate||'—')}</div></div> | |
| </div> | |
| <div class="cols">${list('Materials',b.materials)}${list('Tools',b.tools)}${list('Safety',b.safety)}</div>`; | |
| } | |
| function critiqueHtml(v){ | |
| const cls=v.pass?'pass':'revise'; | |
| const issues=(v.issues||[]).map(i=>`<li><span class="sev ${esc(i.severity||'low')}">${esc(i.severity||'?')}</span> | |
| <span>${esc(i.issue||'')}</span></li>`).join(''); | |
| return `<div class="gate"><div class="score">${v.score.toFixed(0)}<small>/10</small></div> | |
| <span class="badge ${cls}">${v.pass?'✓ passes':'↻ send back'}</span> | |
| <span style="color:var(--dim);font-size:13px">${esc(v.summary||'')}</span></div> | |
| <ul class="issues">${issues||'<li style="border:0;color:var(--dim2)">No issues found.</li>'}</ul>`; | |
| } | |
| let busy=false; | |
| async function run(){ | |
| if(busy) return; const topic=topicEl.value.trim(); if(!topic) return; | |
| busy=true; goBtn.disabled=true; lane.innerHTML=''; | |
| try{ | |
| // 1) RESEARCH | |
| const rc=card('researcher','🔍','Researcher','<span class="dots">researching</span>', | |
| '<span style="color:var(--dim2)">gathering materials, tools, safety and pitfalls…</span>'); | |
| const r1=await post('/api/research',{topic}); | |
| if(r1.error) throw r1.error; | |
| setStatus(rc,'brief ready'); setBody(rc,briefHtml(r1.brief)); | |
| let brief=r1.brief; | |
| // 2) WRITE | |
| flow(); | |
| const wc=card('writer','✍️','Writer','<span class="dots">drafting</span>', | |
| '<span style="color:var(--dim2)">turning the brief into a step-by-step guide…</span>'); | |
| let gw=await post('/api/write',{topic,brief}); | |
| if(gw.error) throw gw.error; | |
| let guide=gw.guide; | |
| setStatus(wc,'draft ready'); setBody(wc,'<div class="guide">'+md(guide)+'</div>'); | |
| // 3) CRITIQUE -> REVISE loop | |
| let passed=false, round=0, lastVerdict=null; | |
| while(round<MAX_ROUNDS){ | |
| round++; flow(); | |
| const cc=card('critic','🧐','Critic'+(MAX_ROUNDS>1?` · round ${round}`:''),'<span class="dots">reviewing</span>', | |
| '<span style="color:var(--dim2)">checking steps, materials and safety…</span>'); | |
| const cv=await post('/api/critique',{topic,brief,guide}); | |
| if(cv.error) throw cv.error; | |
| lastVerdict=cv.verdict; setStatus(cc,lastVerdict.pass?'passed':'sent back'); | |
| setBody(cc,critiqueHtml(lastVerdict)); | |
| if(lastVerdict.pass){passed=true;break;} | |
| if(round>=MAX_ROUNDS) break; | |
| flow(); | |
| const rv=card('writer','✍️','Writer · revising','<span class="dots">revising</span>', | |
| '<span style="color:var(--dim2)">addressing every issue the critic raised…</span>'); | |
| const rr=await post('/api/revise',{topic,brief,guide,verdict:lastVerdict}); | |
| if(rr.error) throw rr.error; | |
| guide=rr.guide; setStatus(rv,'revised'); setBody(rv,'<div class="guide">'+md(guide)+'</div>'); | |
| } | |
| // 4) FINAL | |
| flow(); | |
| const label=passed?'✓ Vetted by the team':'Shipped after review'+(lastVerdict&&!lastVerdict.pass?' · minor notes remain':''); | |
| const fc=card('final writer','🛠️','Final Build Guide',label,'<div class="guide">'+md(guide)+'</div>'); | |
| fc.classList.add('final'); | |
| }catch(e){ | |
| card('critic','⚠️','Error','', '<span class="err">'+esc(typeof e==='string'?e:'Something went wrong — is the model backend running?')+'</span>'); | |
| }finally{ busy=false; goBtn.disabled=false; } | |
| } | |
| topicEl.addEventListener('keydown',e=>{if(e.key==='Enter')run();}); | |
| </script></body></html>""" | |
| def index(): | |
| return render_template_string(PAGE, backend=llm.backend_label(), max_rev=agents.MAX_REVISIONS) | |
| def _topic(): | |
| d = request.get_json(force=True, silent=True) or {} | |
| return (d.get("topic") or "").strip()[:MAX_TOPIC], d | |
| def api_research(): | |
| topic, _ = _topic() | |
| if not topic: | |
| return jsonify({"error": "Tell me what you want to build."}) | |
| try: | |
| return jsonify({"brief": agents.research(topic)}) | |
| except Exception as e: | |
| print(f"research error: {type(e).__name__}") | |
| return jsonify({"error": "The researcher couldn't reach the model backend."}) | |
| def api_write(): | |
| topic, d = _topic() | |
| brief = d.get("brief") or {} | |
| if not topic: | |
| return jsonify({"error": "Missing project."}) | |
| try: | |
| return jsonify({"guide": agents.write(topic, brief)}) | |
| except Exception as e: | |
| print(f"write error: {type(e).__name__}") | |
| return jsonify({"error": "The writer couldn't reach the model backend."}) | |
| def api_critique(): | |
| topic, d = _topic() | |
| brief, guide = d.get("brief") or {}, d.get("guide") or "" | |
| if not (topic and guide): | |
| return jsonify({"error": "Missing guide to review."}) | |
| try: | |
| return jsonify({"verdict": agents.critique(topic, brief, guide)}) | |
| except Exception as e: | |
| print(f"critique error: {type(e).__name__}") | |
| return jsonify({"error": "The critic couldn't reach the model backend."}) | |
| def api_revise(): | |
| topic, d = _topic() | |
| brief, guide, verdict = d.get("brief") or {}, d.get("guide") or "", d.get("verdict") or {} | |
| if not (topic and guide): | |
| return jsonify({"error": "Missing guide to revise."}) | |
| try: | |
| return jsonify({"guide": agents.revise(topic, brief, guide, verdict)}) | |
| except Exception as e: | |
| print(f"revise error: {type(e).__name__}") | |
| return jsonify({"error": "The writer couldn't reach the model backend."}) | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", "7860")) | |
| app.run(host="0.0.0.0", port=port, debug=False) | |