ramco-brain / query /app.py
VizuaraAI's picture
deploy PO brain
519cffb verified
Raw
History Blame Contribute Delete
7.67 kB
"""Read-only web app for the PO brain — stdlib http.server, zero install.
GET / chat UI
GET /ask?q=... JSON grounded answer (+ rendered html)
GET /wiki/<type>/<slug>.md rendered wiki page
GET /wiki/index.md the catalog
This app NEVER mutates anything. It only reads the compiled graph + wiki.
"""
from __future__ import annotations
import json
import urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import config
from query import md as mdlib
from query.answer import answer
from query.retriever import get_brain
PAGE = """<!doctype html><html><head><meta charset="utf-8">
<title>ramco-brain · PO</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root{--bg:#0f1115;--panel:#171a21;--line:#262b36;--ink:#e7e9ee;--mut:#9aa3b2;--accent:#7c9cff;--chip:#1f2530}
*{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink);
font:15px/1.55 -apple-system,BlinkMacSystemFont,Inter,Segoe UI,sans-serif}
header{padding:18px 22px;border-bottom:1px solid var(--line);display:flex;gap:12px;align-items:baseline}
header b{font-size:18px} header span{color:var(--mut);font-size:13px}
.wrap{display:grid;grid-template-columns:1fr 1fr;gap:0;height:calc(100vh - 59px)}
.col{overflow:auto;padding:22px}
.col.left{border-right:1px solid var(--line)}
form{display:flex;gap:8px} input{flex:1;background:var(--panel);border:1px solid var(--line);
color:var(--ink);border-radius:10px;padding:11px 13px;font-size:15px}
button{background:var(--accent);border:0;color:#0b0e14;font-weight:600;border-radius:10px;padding:0 16px;cursor:pointer}
.chips{margin:12px 0 4px;display:flex;flex-wrap:wrap;gap:7px}
.chip{background:var(--chip);border:1px solid var(--line);color:var(--mut);border-radius:999px;
padding:5px 11px;font-size:12.5px;cursor:pointer}
.chip:hover{color:var(--ink)}
#ans{margin-top:18px} #ans a{color:var(--accent);text-decoration:none} #ans a:hover{text-decoration:underline}
#ans h1,#ans h2,#ans h3{font-size:15px;margin:16px 0 6px;color:var(--mut);font-weight:600;text-transform:uppercase;letter-spacing:.04em}
#ans ul{margin:6px 0;padding-left:18px} #ans li{margin:3px 0}
#ans code{background:#0c0f15;padding:1px 5px;border-radius:5px;font-size:13px}
#ans blockquote{border-left:3px solid var(--accent);margin:8px 0;padding:4px 12px;color:var(--mut)}
table{border-collapse:collapse;margin:8px 0;font-size:13px} th,td{border:1px solid var(--line);padding:4px 9px;text-align:left}
.fm{background:#0c0f15;border:1px solid var(--line);border-radius:8px;padding:10px;color:var(--mut);font-size:12px;white-space:pre-wrap;overflow:auto}
.src{color:var(--mut);font-size:12px;margin-top:10px}
.ph{color:var(--mut)} h2.t{margin-top:0}
</style></head><body>
<header><b>ramco-brain</b><span>read-only queryable wiki · Purchase Order · grounded in Ramco artifacts</span></header>
<div class="wrap">
<div class="col left">
<form onsubmit="ask(event)">
<input id="q" placeholder="Ask the brain… e.g. which SPs write PO_POMAS_PUR_ORDER_HDR" autofocus>
<button>Ask</button></form>
<div class="chips" id="examples"></div>
<div id="ans"><p class="ph">Ask a structural question about the PO domain. Answers are grounded in the graph and link to wiki pages on the right.</p></div>
</div>
<div class="col right" id="page"><h2 class="t ph">Wiki page</h2><p class="ph">Click any link in an answer to open its page here.</p></div>
</div>
<script>
const EX=["what is the API for creating a PO","which SPs write PO_POMAS_PUR_ORDER_HDR",
"what screens are in PoCrt","how do I amend a purchase order","tell me about CreatePO",
"which errors does poemn_sp_aprgrd raise","what does pocrmn_sp_podbyr read"];
const ec=document.getElementById('examples');
EX.forEach(t=>{const c=document.createElement('span');c.className='chip';c.textContent=t;
c.onclick=()=>{document.getElementById('q').value=t;ask();};ec.appendChild(c);});
async function ask(e){if(e)e.preventDefault();const q=document.getElementById('q').value.trim();
if(!q)return;const a=document.getElementById('ans');a.innerHTML='<p class="ph">…</p>';
const r=await fetch('/ask?q='+encodeURIComponent(q));const d=await r.json();
a.innerHTML=d.html; a.querySelectorAll('a[href^="/wiki/"]').forEach(el=>{
el.onclick=(ev)=>{ev.preventDefault();openPage(el.getAttribute('href'));};});}
async function openPage(href){const p=document.getElementById('page');
const r=await fetch(href);const d=await r.json();p.innerHTML=d.html;
p.querySelectorAll('a[href^="/wiki/"]').forEach(el=>{
el.onclick=(ev)=>{ev.preventDefault();openPage(el.getAttribute('href'));};});
p.scrollTop=0;}
</script></body></html>"""
def _resolve_wiki_href(href: str) -> str:
"""Normalise relative wiki links (../table/x.md) to absolute /wiki/table/x.md."""
href = href.replace("../", "")
if href.endswith(".md") and not href.startswith("/wiki/"):
href = "/wiki/" + href.lstrip("/")
return href
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, *args):
pass
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
path = urllib.parse.unquote(parsed.path)
qs = urllib.parse.parse_qs(parsed.query)
if path == "/" or path == "/index.html":
return self._send(200, PAGE, "text/html; charset=utf-8")
if path == "/ask":
q = (qs.get("q", [""])[0]).strip()
res = answer(q) if q else {"answer": "", "citations": []}
res["html"] = _answer_html(res)
return self._send(200, json.dumps(res))
if path.startswith("/wiki/"):
return self._serve_wiki(path)
return self._send(404, json.dumps({"error": "not found"}))
def _serve_wiki(self, path):
rel = path[len("/wiki/"):]
if rel in ("", "index", "index.md"):
rel = "index.md"
if not rel.endswith(".md"):
rel += ".md"
target = (config.WIKI_DIR / rel).resolve()
# path-traversal guard
if config.WIKI_DIR.resolve() not in target.parents and target != config.WIKI_DIR.resolve():
return self._send(403, json.dumps({"error": "forbidden"}))
if not target.exists():
return self._send(404, json.dumps({"html": "<p class='ph'>Page not found.</p>"}))
html_body = mdlib.to_html(target.read_text(encoding="utf-8"))
# rewrite relative .md links to absolute /wiki/ ones
import re
html_body = re.sub(r'href="([^"]+\.md)"',
lambda m: f'href="{_resolve_wiki_href(m.group(1))}"', html_body)
return self._send(200, json.dumps({"html": html_body}))
def _answer_html(res: dict) -> str:
body = mdlib.to_html(res.get("answer", ""))
import re
body = re.sub(r'href="([^"]+\.md)"',
lambda m: f'href="{_resolve_wiki_href(m.group(1))}"', body)
return body
def main(port: int = 8800):
get_brain() # warm the index
srv = ThreadingHTTPServer(("127.0.0.1", port), Handler)
print(f"ramco-brain serving at http://127.0.0.1:{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)