Spaces:
Sleeping
Sleeping
| """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 | |
| 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=>({'&':'&','<':'<','>':'>'}[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,"'")})'>${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,"'")})'> | |
| <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) | |