ramco-brain / query /web.py
VizuaraAI's picture
deploy PO brain
519cffb verified
Raw
History Blame Contribute Delete
16.9 kB
"""Ramco Intelligence — the PO brain (answer-first UI).
White, minimal, SF system font, soft pink/violet. The answer renders directly below the
question as sectioned paragraphs; the screen->service->API->SP->tables chain is a compact
left-to-right pipeline below it; citations are cards that reveal their exact passage.
stdlib only.
GET / UI
GET /ask?q=... grounded answer JSON (sections, chain, citations, handoff)
GET /cite?source=&loc= the cited passage text
GET /questions the 100-question catalog (grouped)
"""
from __future__ import annotations
import json
import urllib.parse
from functools import lru_cache
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import config
from query.ask import answer
@lru_cache(maxsize=1)
def _chunk_index():
idx = {}
try:
for c in json.loads(config.DOC_CHUNKS_JSON.read_text()):
idx[(c["source"], c["loc"])] = c["text"]
except Exception:
pass
return idx
PAGE = r"""<!doctype html><html lang="en"><head><meta charset="utf-8">
<title>Ramco Intelligence · PO Brain</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root{
--bg:#ffffff; --ink:#1d1d1f; --mut:#86868b; --line:#ececf1;
--pink:#f7d6e4; --pink-deep:#c85f93; --violet:#efe9fd; --violet-deep:#6f5cd0;
--surface:#fcfcfe; --chip:#f6f4fc;
}
*{box-sizing:border-box}
html,body{margin:0}
body{background:var(--bg);color:var(--ink);
font:16px/1.6 -apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Helvetica Neue",Inter,sans-serif;
-webkit-font-smoothing:antialiased}
/* top bar */
.topbar{display:flex;align-items:center;justify-content:space-between;
padding:16px 26px;border-bottom:1px solid var(--line);position:sticky;top:0;background:rgba(255,255,255,.85);
backdrop-filter:saturate(180%) blur(12px);z-index:5}
.brand{display:flex;align-items:center;gap:10px;font-weight:600;font-size:16px;letter-spacing:-.01em}
.brand svg{width:26px;height:26px}
.browse{background:none;border:1px solid var(--line);border-radius:999px;color:var(--violet-deep);
font-weight:600;font-size:13.5px;padding:7px 15px;cursor:pointer;transition:.15s}
.browse:hover{background:var(--violet);border-color:var(--violet-deep)}
/* layout */
.wrap{max-width:740px;margin:0 auto;padding:30px 24px 110px}
.hero{text-align:center;margin:18px 0 22px}
.hero h1{font-size:34px;font-weight:600;letter-spacing:-.02em;margin:0}
.hero .sub{color:var(--mut);font-size:16.5px;margin-top:8px}
.answered .hero{margin:6px 0 16px}
.answered .hero h1{font-size:22px}
.answered .hero .sub{display:none}
/* search */
form{position:relative}
#q{width:100%;border:1px solid var(--line);border-radius:16px;padding:17px 56px 17px 18px;
font-size:17px;color:var(--ink);background:var(--surface);outline:none;transition:.18s}
#q:focus{border-color:var(--violet-deep);box-shadow:0 0 0 4px var(--violet)}
#go{position:absolute;right:8px;top:8px;bottom:8px;border:0;border-radius:12px;padding:0 18px;
background:linear-gradient(135deg,var(--pink-deep),var(--violet-deep));color:#fff;font-weight:600;
font-size:15px;cursor:pointer}
.chips{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin:14px 0 0}
.chip{background:var(--chip);border:1px solid var(--line);color:#5b5b66;border-radius:999px;
padding:7px 13px;font-size:13px;cursor:pointer;transition:.15s}
.chip:hover{border-color:var(--violet-deep);color:var(--ink)}
.answered .chips{display:none}
/* explorer (top-triggered) */
#explorer{display:none;margin:16px 0 0}
#explorer.open{display:block}
#explorer .intro{color:var(--mut);font-size:13px;margin-bottom:12px}
.cat{border:1px solid var(--line);border-radius:14px;margin:0 0 10px;overflow:hidden;background:#fff}
.cat>summary{list-style:none;cursor:pointer;padding:13px 16px;font-weight:600;font-size:14.5px;
display:flex;justify-content:space-between;align-items:center}
.cat>summary::-webkit-details-marker{display:none}
.cat>summary .ct{font-size:12px;color:var(--mut);font-weight:500;background:var(--chip);border-radius:999px;padding:3px 9px}
.cat[open]>summary{border-bottom:1px solid var(--line)}
.qlist{padding:8px 16px 14px}
.qitem{display:block;width:100%;text-align:left;background:none;border:0;border-radius:8px;
padding:8px 10px;font-size:14px;color:#3a3a3c;cursor:pointer}
.qitem:hover{background:var(--chip);color:var(--ink)}
.qitem::before{content:"›";color:var(--pink-deep);margin-right:9px}
/* answer */
#out{margin-top:22px}
.tag{display:inline-block;font-size:11px;letter-spacing:.07em;text-transform:uppercase;
color:var(--violet-deep);background:var(--violet);border-radius:7px;padding:4px 10px;margin-bottom:14px}
.sec{margin:0 0 20px}
.sec h3{font-size:13px;letter-spacing:.04em;text-transform:uppercase;color:var(--pink-deep);
margin:0 0 6px;font-weight:700}
.sec p{margin:0;font-size:16.5px;line-height:1.72;color:#2a2a2e}
.sec code,.sec .m{font-family:"SF Mono",ui-monospace,Menlo,monospace;font-size:14px}
.lead p{font-size:18px;line-height:1.7}
/* handoff */
.handoff{margin:22px 0 4px}
.handoff a{display:inline-flex;align-items:center;gap:9px;background:#111114;color:#fff;
text-decoration:none;border-radius:999px;padding:12px 22px;font-size:15px;font-weight:600;transition:.15s}
.handoff a:hover{background:#000;transform:translateY(-1px)}
.handoff a .pl{color:#f7a8cf}
.handoff .note{color:var(--mut);font-size:12.5px;margin-top:8px}
/* chain pipeline (left to right) */
.pipe-wrap{margin:26px 0 0;border-top:1px solid var(--line);padding-top:20px}
.pipe-h{font-size:12px;letter-spacing:.07em;text-transform:uppercase;color:var(--mut);margin:0 0 12px}
.pipe{display:flex;align-items:stretch;flex-wrap:wrap;gap:0}
.pstage{border:1px solid var(--line);border-radius:13px;padding:10px 14px;cursor:pointer;background:#fff;
transition:.15s;min-width:92px}
.pstage:hover,.pstage.active{border-color:var(--violet-deep);background:#fbfaff}
.pstage .k{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--mut)}
.pstage .v{font-size:15px;font-weight:600;margin-top:2px}
.parrow{display:flex;align-items:center;color:#c9c7d2;padding:0 8px;font-size:16px}
.pitems{margin-top:14px;display:none}
.pitems.open{display:block}
.pill{display:inline-block;background:var(--chip);border:1px solid var(--line);border-radius:8px;
padding:5px 10px;margin:0 6px 6px 0;font-size:12.5px;color:#46414f;font-family:"SF Mono",ui-monospace,Menlo,monospace}
.pill.tbl{background:#fdeef5;border-color:#f6d6e4;color:#9c4a72}
.subc{border:1px solid var(--line);border-radius:11px;padding:10px 12px;margin:0 0 8px;background:#fff}
.subc .name{font-weight:600;font-size:13.5px}
.subc .sub{color:var(--mut);font-size:12px;margin:2px 0 6px}
/* sources */
.src{margin:26px 0 0;border-top:1px solid var(--line);padding-top:20px}
.src .h{font-size:12px;letter-spacing:.07em;text-transform:uppercase;color:var(--mut);margin:0 0 12px}
.srcgrid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.scard{border:1px solid var(--line);border-radius:13px;padding:11px 13px;cursor:pointer;background:#fff;
transition:.15s;display:flex;gap:11px;align-items:flex-start}
.scard:hover{border-color:var(--violet-deep);background:#fcfbff}
.dot{width:9px;height:9px;border-radius:50%;margin-top:5px;flex:0 0 auto;background:var(--violet-deep)}
.dot.code{background:var(--pink-deep)} .dot.data{background:#e0a93b}
.scard .t{font-size:13px;font-weight:600;line-height:1.35}
.scard .l{font-size:11.5px;color:var(--mut);margin-top:2px}
#cite{margin-top:12px;border-left:3px solid var(--pink-deep);background:#fff;border-radius:0 12px 12px 0;
padding:13px 16px;font-size:14px;color:#46414f;display:none;white-space:pre-wrap;line-height:1.55}
#cite.open{display:block}
.empty{color:var(--mut);font-size:15px;text-align:center;margin-top:26px}
@media(max-width:560px){.srcgrid{grid-template-columns:1fr}.hero h1{font-size:28px}}
</style></head><body>
<div class="topbar">
<div class="brand"><svg viewBox="0 0 100 100"><g transform="translate(50,50)" id="petals"></g></svg>Ramco Intelligence</div>
<button class="browse" onclick="toggleExplorer()">Browse 100 questions</button>
</div>
<main class="wrap" id="main">
<div class="hero">
<h1>Ask the Purchase Order brain</h1>
<div class="sub">Plain-English answers about how the PO system works — grounded in the source truth.</div>
</div>
<form onsubmit="ask(event)">
<input id="q" placeholder="What happens when someone creates a purchase order?" autofocus>
<button id="go">Ask</button>
</form>
<div class="chips" id="ex"></div>
<div id="explorer"></div>
<div id="out"><p class="empty">Ask about a process, the tables it populates, an API, a stored procedure,
a validation, or what something means. Every answer links back to its source.</p></div>
</main>
<script>
(function(){const cols=['#e0218a','#4aa3e0','#f2a93b','#e0218a','#4aa3e0','#f2a93b'];let s='';
for(let i=0;i<6;i++)s+=`<ellipse cx="0" cy="-22" rx="12" ry="22" fill="${cols[i]}" transform="rotate(${i*60})"/>`;
document.getElementById('petals').innerHTML=s;})();
const EX=["What happens when someone creates a purchase order?",
"Which tables get populated when I create a PO?","Which API fires when I save a PO?",
"What are the variants of creating a purchase order?","What validations run when I save a PO?",
"What does PO_POSHD_SCHEDULE_DTL mean?"];
const ex=document.getElementById('ex');
EX.forEach(t=>{const c=document.createElement('span');c.className='chip';c.textContent=t;
c.onclick=()=>{document.getElementById('q').value=t;ask();};ex.appendChild(c);});
function esc(s){return (s||'').replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
// wrap TABLE_NAMES and code-ish tokens in <code> for readability
function code(s){return esc(s).replace(/\b([A-Z][A-Z0-9]+(?:_[A-Z0-9]+)+)\b/g,'<span class="m">$1</span>');}
const STAGES=[["screen","Screens"],["section","Sections"],["api","API"],["sp","Save SPs"],["table","Tables"]];
// ---- explorer ----
let QLOADED=false;
async function toggleExplorer(){
const e=document.getElementById('explorer');
if(!QLOADED){const cats=await (await fetch('/questions')).json();
let h='<p class="intro">100 questions this brain can answer — click any to run it.</p>';
cats.forEach((c,i)=>{h+=`<details class="cat"${i===0?' open':''}><summary>${esc(c.category)}<span class="ct">${c.questions.length}</span></summary><div class="qlist">`;
c.questions.forEach(q=>{h+=`<button class="qitem" onclick='runQ(${JSON.stringify(q).replace(/'/g,"&#39;")})'>${esc(q)}</button>`;});
h+='</div></details>';});
e.innerHTML=h;QLOADED=true;}
e.classList.toggle('open');
}
function runQ(q){document.getElementById('q').value=q;document.getElementById('explorer').classList.remove('open');ask();}
// ---- chain pipeline ----
let CUR_CHAIN=null;
function pipelineHTML(chain){
if(!chain||!chain.length)return '';
CUR_CHAIN=chain;const byStep={};chain.forEach(s=>byStep[s.step]=s);
const present=STAGES.filter(([k])=>byStep[k]);
let h='<div class="pipe-wrap"><p class="pipe-h">How it works in the system — screen → service → API → SP → tables</p><div class="pipe">';
present.forEach(([k,label],i)=>{const st=byStep[k];const v=k==='api'?esc(st.items[0]||'—'):st.items.length;
h+=`<div class="pstage" onclick="pick('${k}')"><div class="k">${label}</div><div class="v">${v}</div></div>`;
if(i<present.length-1)h+='<div class="parrow">→</div>';});
h+='</div><div id="pitems" class="pitems"></div></div>';
return h;
}
function pick(k){
const st=(CUR_CHAIN||[]).find(s=>s.step===k);if(!st)return;
document.querySelectorAll('.pstage').forEach(n=>n.classList.remove('active'));
const present=STAGES.filter(([kk])=>(CUR_CHAIN||[]).find(s=>s.step===kk)).map(s=>s[0]);
const ns=document.querySelectorAll('.pstage');const pos=present.indexOf(k);if(ns[pos])ns[pos].classList.add('active');
let body='';
if(k==='section'){st.items.forEach(s=>{body+=`<div class="subc"><div class="name">${esc(s.name)}</div>
<div class="sub">${s.api?('sub-API '+esc(s.api)+' · '):''}${s.sps.length} save SP(s) · writes ${s.tables.length} table(s)</div>
${s.tables.map(t=>`<span class="pill tbl">${esc(t)}</span>`).join('')}</div>`;});}
else{body=st.items.map(x=>`<span class="pill ${k==='table'?'tbl':''}">${esc(typeof x==='string'?x:x.name)}</span>`).join('');}
const lbl=STAGES.find(s=>s[0]===k)[1];
const box=document.getElementById('pitems');box.innerHTML=`<p class="pipe-h">${lbl} (${st.items.length})</p>${body}`;box.classList.add('open');
}
// ---- sources ----
function srcKind(src){const f=(src||'').toLowerCase();
if(f.endsWith('.sql'))return 'code';if(f.endsWith('.csv')||f.endsWith('.json')||f.endsWith('.xlsx'))return 'data';return 'doc';}
function srcTitle(src){return (src||'').split('/').pop().replace(/\.(pdf|docx|pptx|xlsx|csv|json|sql)$/i,'');}
function sourcesHTML(cites){
if(!cites||!cites.length)return '';
let h='<div class="src"><p class="h">Where this comes from</p><div class="srcgrid">';
cites.forEach(c=>{h+=`<div class="scard" onclick='showCite(${JSON.stringify(c).replace(/'/g,"&#39;")})'>
<span class="dot ${srcKind(c.source)}"></span><div><div class="t">${esc(srcTitle(c.source))}</div>
<div class="l">${c.loc?esc(c.loc):'source artifact'}</div></div></div>`;});
h+='</div><div id="cite"></div></div>';return h;
}
async function showCite(c){const box=document.getElementById('cite');box.classList.add('open');box.textContent='…';
const r=await fetch('/cite?source='+encodeURIComponent(c.source||'')+'&loc='+encodeURIComponent(c.loc||''));
const d=await r.json();
box.textContent=(d.source||'')+(c.loc?(' · '+c.loc):'')+'\n\n'+(d.text||'(source artifact — open it in the Ramco files to verify)');}
function handoffHTML(h){if(!h)return '';
return `<div class="handoff"><a href="${esc(h.url)}" target="_blank" rel="noopener"><span class="pl">▶</span> ${esc(h.label)}</a>
<div class="note">This activity is a live journey in the assistant — it picks up where the brain leaves off.</div></div>`;}
function answerHTML(d){
const secs=d.sections;
let body;
if(secs&&secs.length){body=secs.map((s,i)=>`<section class="sec ${i===0?'lead':''}"><h3>${esc(s.heading)}</h3><p>${code(s.body)}</p></section>`).join('');}
else{body=`<section class="sec lead"><p>${code(d.answer_polished||d.answer||'No answer.')}</p></section>`;}
const tag=d.target?esc((d.target.type||'')+' · '+(d.target.name||'')):esc(d.intent||'');
return `<div class="tag">${tag}</div>${body}`+handoffHTML(d.handoff)+pipelineHTML(d.chain)+sourcesHTML(d.citations);
}
async function ask(e){if(e)e.preventDefault();
const q=document.getElementById('q').value.trim();if(!q)return;
document.getElementById('main').classList.add('answered');
const out=document.getElementById('out');out.innerHTML='<p class="empty">Thinking…</p>';
const r=await fetch('/ask?q='+encodeURIComponent(q));const d=await r.json();
out.innerHTML=answerHTML(d);
out.scrollIntoView({behavior:'smooth',block:'nearest'});
}
</script></body></html>"""
class Handler(BaseHTTPRequestHandler):
def _send(self, code, body, ctype="application/json"):
data = body.encode("utf-8") if isinstance(body, str) else body
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *a):
pass
def do_GET(self):
u = urllib.parse.urlparse(self.path)
qs = urllib.parse.parse_qs(u.query)
if u.path in ("/", "/index.html"):
return self._send(200, PAGE, "text/html; charset=utf-8")
if u.path == "/ask":
q = (qs.get("q", [""])[0]).strip()
res = answer(q) if q else {"answer": "", "chain": [], "citations": []}
return self._send(200, json.dumps(res))
if u.path == "/cite":
src = qs.get("source", [""])[0]
loc = qs.get("loc", [""])[0]
text = _chunk_index().get((src, loc), "")
return self._send(200, json.dumps({"source": src, "loc": loc, "text": text}))
if u.path == "/questions":
try:
return self._send(200, config.QUESTIONS_JSON.read_text())
except Exception:
return self._send(200, "[]")
return self._send(404, json.dumps({"error": "not found"}))
def main(port: int = 8800):
import os
host = os.environ.get("HOST", "127.0.0.1")
port = int(os.environ.get("PORT", port))
answer("warmup")
srv = ThreadingHTTPServer((host, port), Handler)
print(f"Ramco Intelligence · PO Brain at http://{host}:{port} (Ctrl-C to stop)")
try:
srv.serve_forever()
except KeyboardInterrupt:
srv.shutdown()
if __name__ == "__main__":
import sys
main(int(sys.argv[1]) if len(sys.argv) > 1 else 8800)