Spaces:
Sleeping
Sleeping
| """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) | |