Spaces:
Sleeping
Sleeping
Download app.py from BonusLockSMith/web-research-agent: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/spaces/BonusLockSMith/web-research-agent/resolve/main/app.py
- Command line
-
hf download hf://spaces/BonusLockSMith/web-research-agent/app.py
-
curl -L -o app.py https://huggingface.co/spaces/BonusLockSMith/web-research-agent/resolve/main/app.py
15.6 kB
| #!/usr/bin/env python | |
| """Web Research Agent (#13) β Flask UI. | |
| Golden-standard research UX (inline domain chips, visible research steps, source passages on click) | |
| plus our wedge: 'dig deeper' on any citation, selection, or source β with YOUR own steering text β | |
| spawns a scoped research pass appended as an expandable, recursive branch. | |
| pip install -r requirements.txt | |
| python app.py # http://127.0.0.1:7860 | |
| """ | |
| import os | |
| from flask import Flask, request, jsonify, render_template_string | |
| import agent | |
| import llm | |
| app = Flask(__name__) | |
| AREAS = ["web", "opportunity", "technical", "competitive", "client"] | |
| PAGE = r"""<!doctype html><html lang="en"><head> | |
| <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>GritAI Β· Research</title> | |
| <link rel="stylesheet" href="/static/gritai.css"> | |
| <style> | |
| .wrap{max-width:1240px;margin:0 auto;padding:22px 20px 90px} | |
| .bar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:14px} | |
| .bar input#q{flex:1;min-width:260px;background:var(--panel);border:1px solid var(--line2);border-radius:3px; | |
| color:var(--ink);font-family:var(--body);font-size:15.5px;padding:14px 16px;outline:none} | |
| .bar select{background:var(--panel);border:1px solid var(--line2);border-radius:3px;color:var(--ink); | |
| font-family:var(--mono);font-size:12px;padding:13px 10px;text-transform:uppercase;letter-spacing:.08em} | |
| .cols{display:grid;grid-template-columns:1fr 320px;gap:20px;margin-top:22px} | |
| @media(max-width:860px){.cols{grid-template-columns:1fr}} | |
| .muted{color:var(--dim2);font-family:var(--mono);font-size:10.5px;letter-spacing:.2em;text-transform:uppercase} | |
| .steps{border-left:2px solid var(--grit-dim);padding:6px 0 6px 14px;margin:6px 0 20px} | |
| .steps div{color:var(--dim);font-family:var(--mono);font-size:12px;padding:2px 0} | |
| .report{font-size:15.5px;line-height:1.75} | |
| .report h4{font-family:var(--disp);color:var(--grit);font-size:14px;letter-spacing:.05em;text-transform:uppercase;margin:20px 0 8px} | |
| .report p{margin:11px 0}.report li{margin:5px 0} | |
| .chip{display:inline-flex;align-items:baseline;gap:4px;background:var(--panel2);border:1px solid var(--line2); | |
| border-radius:3px;padding:1px 6px;margin:0 2px;font-family:var(--mono);font-size:11px;color:var(--dim); | |
| cursor:pointer;vertical-align:baseline;transition:.14s;white-space:nowrap} | |
| .chip:hover{color:var(--grit);border-color:var(--grit-dim)} | |
| .chip b{color:var(--grit);font-weight:600} | |
| .card{border:1px solid var(--line2);border-radius:3px;background:var(--panel);padding:13px 14px;margin-bottom:10px} | |
| .card .dom{font-family:var(--mono);font-size:10.5px;color:var(--grit);letter-spacing:.06em} | |
| .card .ti{font-size:13.5px;margin:4px 0 6px;color:var(--ink);line-height:1.35} | |
| .card .ex{font-size:12px;color:var(--dim);line-height:1.5;max-height:56px;overflow:hidden} | |
| .dig{border:0;background:transparent;color:var(--dim);border:1px solid var(--line2);border-radius:3px; | |
| font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;padding:5px 9px; | |
| margin-top:8px;cursor:pointer;transition:.14s} | |
| .dig:hover{color:var(--grit);border-color:var(--grit-dim)} | |
| #pop{position:fixed;z-index:60;max-width:340px;background:var(--panel2);border:1px solid var(--line2); | |
| border-radius:4px;padding:12px 13px;box-shadow:0 10px 40px -8px #000;display:none} | |
| #pop .dom{font-family:var(--mono);font-size:10.5px;color:var(--grit)} | |
| #pop .ti{font-size:13px;color:var(--ink);margin:4px 0 6px}#pop .ex{font-size:11.5px;color:var(--dim);line-height:1.5} | |
| #pop a{color:var(--grit2);font-size:11px;font-family:var(--mono);text-decoration:none} | |
| #selbtn{position:absolute;z-index:70;display:none} | |
| .digbox{border:1px solid var(--grit-dim);border-radius:4px;background:var(--panel2);padding:14px;margin:12px 0} | |
| .digbox .fx{font-size:12.5px;color:var(--dim);border-left:2px solid var(--grit);padding-left:9px;margin-bottom:9px} | |
| .digbox textarea{width:100%;box-sizing:border-box;background:var(--panel);border:1px solid var(--line2);border-radius:3px; | |
| color:var(--ink);font-family:var(--body);font-size:14px;padding:10px;min-height:56px;resize:vertical;outline:none} | |
| .branch{border:1px solid var(--line);border-left:3px solid var(--grit);border-radius:3px;background:var(--bg2); | |
| padding:14px 16px;margin:14px 0} | |
| .branch .bh{font-family:var(--mono);font-size:11px;color:var(--grit);letter-spacing:.06em;margin-bottom:8px} | |
| .branch .srcline{font-size:11px;color:var(--dim);font-family:var(--mono);margin-top:8px} | |
| .spin{display:inline-block;color:var(--grit);font-family:var(--mono);font-size:12px} | |
| .run{border:0;background:var(--grit);color:#140a06;font-family:var(--mono);font-weight:600;font-size:12px; | |
| letter-spacing:.12em;text-transform:uppercase;padding:14px 20px;cursor:pointer;border-radius:3px} | |
| .run:hover{background:var(--grit2)} | |
| </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="M27 12.5A9 9 0 1 0 28 22H19" stroke="#ff5a2a" stroke-width="2.4" stroke-linecap="square"/><rect x="10.5" y="10.5" width="3" height="3" fill="#ff5a2a"/></svg> | |
| <div class="brand"><div class="co">Grit<b>AI</b> Β· Research</div> | |
| <div class="sub">Cited web research Β· dig deeper into anything</div></div> | |
| <div style="flex:1"></div><div class="tag hot">#13 Β· 30-in-15</div> | |
| </header> | |
| <div class="bar"> | |
| <input id="q" placeholder="Ask a research questionβ¦" autofocus> | |
| <select id="area">__AREAS__</select> | |
| <select id="mode"><option value="quick">Quick</option><option value="deep" selected>Deep</option></select> | |
| <button class="run" onclick="ask()">Research βΈ</button> | |
| </div> | |
| <div class="cols"> | |
| <div> | |
| <div id="status"></div> | |
| <div id="steps" class="steps" style="display:none"></div> | |
| <div id="report" class="report"></div> | |
| <div id="branches"></div> | |
| </div> | |
| <div> | |
| <div class="muted" id="srchead" style="display:none;margin-bottom:10px">Sources</div> | |
| <div id="sources"></div> | |
| </div> | |
| </div> | |
| <p class="muted" style="margin-top:34px">Backend: __BACKEND__ Β· local fleet by default Β· GritAI Solutions</p> | |
| </div> | |
| <div id="pop"></div> | |
| <button id="selbtn" class="dig" onmousedown="event.preventDefault()">β dig deeper</button> | |
| <script> | |
| const BLOCKS = {}; // blockId -> sources[] | |
| let curSel = null; // {text, blockId, question} | |
| let bseq = 0; | |
| function esc(s){return (s||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");} | |
| function mdChips(text, blockId, sources){ | |
| // minimal markdown -> html, then citation tokens -> domain chips | |
| const lines=(text||"").split("\n"); let out=[], ul=false; | |
| const flush=()=>{if(ul){out.push("</ul>");ul=false;}}; | |
| for(let raw of lines){ | |
| let h=esc(raw.trim()); | |
| let mh=raw.match(/^\s*#{1,6}\s*(.+?):?\s*$/); | |
| let mli=raw.match(/^\s*[-*]\s+(.+)$/); | |
| if(mh){flush();out.push("<h4>"+esc(mh[1])+"</h4>");} | |
| else if(mli){if(!ul){out.push("<ul>");ul=true;}out.push("<li>"+esc(mli[1])+"</li>");} | |
| else if(h){flush();out.push("<p>"+h+"</p>");} | |
| } | |
| flush(); | |
| let html=out.join("\n").replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>"); | |
| // citations [n] [n,m] [n-m] | |
| html=html.replace(/\[([\d\s,β-]+)\]/g,(m,grp)=>{ | |
| let nums=[]; grp.split(",").forEach(p=>{p=p.trim().replace("β","-"); | |
| if(p.includes("-")){let[a,b]=p.split("-").map(x=>parseInt(x));for(let i=a;i<=b;i++)nums.push(i);} | |
| else if(/^\d+$/.test(p))nums.push(parseInt(p));}); | |
| return nums.map(n=>{let s=sources[n-1]; if(!s)return ""; | |
| return `<span class="chip" data-b="${blockId}" data-n="${n}"><span>${esc(s.domain)}</span><b>${n}</b></span>`; | |
| }).join(""); | |
| }); | |
| return html; | |
| } | |
| function renderSources(sources){ | |
| const box=document.getElementById("sources"); box.innerHTML=""; | |
| document.getElementById("srchead").style.display=sources.length?"block":"none"; | |
| sources.forEach(s=>{ | |
| const d=document.createElement("div"); d.className="card"; | |
| d.innerHTML=`<div class="dom">${esc(s.domain)} Β· [${s.n}]</div> | |
| <div class="ti">${esc(s.title)}</div><div class="ex">${esc(s.excerpt)}</div> | |
| <button class="dig">β dig deeper</button>`; | |
| d.querySelector(".dig").onclick=()=>openDig(s.title, "root", document.getElementById("q").dataset.q||""); | |
| box.appendChild(d); | |
| }); | |
| } | |
| function popover(chip){ | |
| const s=BLOCKS[chip.dataset.b][parseInt(chip.dataset.n)-1]; if(!s)return; | |
| const p=document.getElementById("pop"); | |
| p.innerHTML=`<div class="dom">${esc(s.domain)}</div><div class="ti">${esc(s.title)}</div> | |
| <div class="ex">${esc(s.excerpt)}β¦</div><a href="${esc(s.url)}" target="_blank" rel="noopener">open source β</a>`; | |
| const r=chip.getBoundingClientRect(); | |
| p.style.display="block"; | |
| p.style.left=Math.min(r.left, window.innerWidth-360)+"px"; | |
| p.style.top=(r.bottom+6)+"px"; | |
| } | |
| document.addEventListener("click",e=>{ | |
| const chip=e.target.closest(".chip"); | |
| if(chip){popover(chip);e.stopPropagation();return;} | |
| if(!e.target.closest("#pop")) document.getElementById("pop").style.display="none"; | |
| }); | |
| // selection -> floating dig button | |
| document.addEventListener("mouseup",e=>{ | |
| if(e.target.closest("#selbtn"))return; | |
| const sel=window.getSelection(); const t=sel.toString().trim(); | |
| const btn=document.getElementById("selbtn"); | |
| const block=e.target.closest("[data-question]"); | |
| if(t.length>8 && block){ | |
| const r=sel.getRangeAt(0).getBoundingClientRect(); | |
| btn.style.display="block"; | |
| btn.style.left=(window.scrollX+r.left)+"px"; | |
| btn.style.top=(window.scrollY+r.bottom+6)+"px"; | |
| curSel={text:t, blockId:block.dataset.block, question:block.dataset.question}; | |
| } else if(!e.target.closest(".digbox")) btn.style.display="none"; | |
| }); | |
| document.getElementById("selbtn").onclick=()=>{ | |
| document.getElementById("selbtn").style.display="none"; | |
| if(curSel) openDig(curSel.text, curSel.blockId, curSel.question); | |
| }; | |
| function openDig(focus, blockId, question){ | |
| const box=document.createElement("div"); box.className="digbox"; | |
| box.innerHTML=`<div class="fx">Dig deeper into: β${esc(focus.slice(0,160))}β</div> | |
| <textarea placeholder="What do you want explored? (optional β e.g. 'focus on the cost figures' or 'find counter-evidence')"></textarea> | |
| <div style="margin-top:9px;display:flex;gap:8px"> | |
| <button class="run">Research βΈ</button> | |
| <button class="dig cancel">cancel</button></div>`; | |
| const anchor=(blockId==="root")?document.getElementById("branches"): | |
| (document.querySelector(`[data-block="${blockId}"] .branch-slot`)||document.getElementById("branches")); | |
| anchor.prepend(box); | |
| box.querySelector(".cancel").onclick=()=>box.remove(); | |
| box.querySelector(".run").onclick=()=>{ | |
| const instr=box.querySelector("textarea").value; | |
| const target=anchor; box.remove(); | |
| dig(focus, instr, question, target); | |
| }; | |
| box.scrollIntoView({behavior:"smooth",block:"center"}); | |
| } | |
| async function post(url,body){ | |
| const r=await fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}); | |
| return r.json(); | |
| } | |
| async function ask(){ | |
| const q=document.getElementById("q").value.trim(); if(!q)return; | |
| document.getElementById("q").dataset.q=q; | |
| const area=document.getElementById("area").value, mode=document.getElementById("mode").value; | |
| document.getElementById("steps").style.display="none"; | |
| document.getElementById("report").innerHTML=""; document.getElementById("branches").innerHTML=""; | |
| document.getElementById("sources").innerHTML=""; document.getElementById("srchead").style.display="none"; | |
| const st=document.getElementById("status"); | |
| st.innerHTML=`<span class="spin">β researching (${mode})β¦ this runs live searches + reads pages</span>`; | |
| let dots=0; const iv=setInterval(()=>{dots=(dots+1)%4; st.querySelector(".spin").textContent="β researching ("+mode+")"+".".repeat(dots)+" β live searches + page reads";},500); | |
| try{ | |
| const res=await post("/research",{question:q,mode,area}); | |
| clearInterval(iv); st.innerHTML=""; | |
| const blockId="root"; BLOCKS[blockId]=res.sources||[]; | |
| const rep=document.getElementById("report"); | |
| rep.setAttribute("data-question",q); rep.setAttribute("data-block",blockId); | |
| const steps=document.getElementById("steps"); | |
| steps.style.display="block"; steps.innerHTML="<div class='muted'>Research steps</div>"+ | |
| (res.trace||[]).map(t=>"<div>Β· "+esc(t)+"</div>").join(""); | |
| rep.innerHTML=mdChips(res.report,blockId,res.sources||[]); | |
| renderSources(res.sources||[]); | |
| }catch(e){clearInterval(iv);st.innerHTML="<span style='color:var(--bad)'>Research failed β try again.</span>";} | |
| } | |
| async function dig(focus, instruction, parent, targetEl){ | |
| const area=document.getElementById("area").value, mode=document.getElementById("mode").value; | |
| const br=document.createElement("div"); br.className="branch"; | |
| const bid="b"+(++bseq); | |
| br.setAttribute("data-question",parent); br.setAttribute("data-block",bid); | |
| br.innerHTML=`<div class="bh">β digging: ${esc((instruction||focus).slice(0,120))} <span class="spin">ββ¦</span></div>`; | |
| targetEl.prepend(br); | |
| try{ | |
| const res=await post("/dig",{focus,instruction,parent,area,mode}); | |
| BLOCKS[bid]=res.sources||[]; | |
| br.innerHTML=`<div class="bh">β ${esc((instruction||focus).slice(0,120))}</div> | |
| <div class="report">${mdChips(res.report,bid,res.sources||[])}</div> | |
| <div class="srcline">${(res.sources||[]).map(s=>`[${s.n}] ${esc(s.domain)}`).join(" Β· ")}</div> | |
| <div class="branch-slot"></div>`; | |
| }catch(e){br.innerHTML="<div class='bh' style='color:var(--bad)'>dig failed β try again</div>";} | |
| } | |
| document.getElementById("q").addEventListener("keydown",e=>{if(e.key==="Enter")ask();}); | |
| </script></body></html>""" | |
| def index(): | |
| opts = "".join(f'<option value="{a}">{a.title()}</option>' for a in AREAS) | |
| html = PAGE.replace("__AREAS__", opts).replace("__BACKEND__", llm.backend_label()) | |
| return render_template_string(html) | |
| def do_research(): | |
| d = request.get_json(force=True) | |
| q = (d.get("question") or "").strip()[:500] | |
| if not q: | |
| return jsonify({"report": "Ask a question.", "sources": [], "trace": []}) | |
| mode = d.get("mode", "deep") if d.get("mode") in ("quick", "deep", "swarm") else "deep" | |
| area = d.get("area", "web") | |
| try: | |
| res = agent.research(q, mode=mode, area=area) | |
| return jsonify(res) | |
| except Exception as e: | |
| print(f"research error: {type(e).__name__}") | |
| return jsonify({"report": "Research failed β try again.", "sources": [], "trace": []}), 200 | |
| def do_dig(): | |
| d = request.get_json(force=True) | |
| focus = (d.get("focus") or "").strip()[:600] | |
| instruction = (d.get("instruction") or "").strip()[:400] | |
| parent = (d.get("parent") or "").strip()[:500] | |
| area = d.get("area", "web") | |
| mode = d.get("mode", "deep") if d.get("mode") in ("quick", "deep", "swarm") else "deep" | |
| try: | |
| res = agent.dig(focus, parent, instruction=instruction, area=area, mode=mode) | |
| return jsonify(res) | |
| except Exception as e: | |
| print(f"dig error: {type(e).__name__}") | |
| return jsonify({"report": "Dig failed β try again.", "sources": [], "trace": []}), 200 | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", "7860")) | |
| app.run(host="0.0.0.0", port=port, debug=False) | |