BonusLockSMith's picture
Upload folder using huggingface_hub
3e0f245 verified
Raw History Blame Contribute Delete
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");}
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>"""
@app.route("/")
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)
@app.route("/research", methods=["POST"])
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
@app.route("/dig", methods=["POST"])
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)