"""Aperture — Gradio app. A lightweight alternative UI to the React/Vercel dashboard, for analysts and quick demos. Runs the SAME in-process pipeline (OCR backends, categorization, hybrid extract, DB, RAG, KPIs) — no separate API needed. pip install gradio python gradio_app.py # http://localhost:7860 Docs: https://www.gradio.app/guides/quickstart """ from __future__ import annotations import json import sys from pathlib import Path BACKEND = Path(__file__).resolve().parent / "backend" sys.path.insert(0, str(BACKEND)) import gradio as gr # noqa: E402 from app.categories import list_categories # noqa: E402 from app.config import get_settings # noqa: E402 from app.db import Database # noqa: E402 from app.metrics import MetricsStore # noqa: E402 from app.observability import business_kpis # noqa: E402 from app.ocr.backends import build_ocr_registry, list_backends # noqa: E402 from app.pipeline import process_document # noqa: E402 from app.providers import build_registry # noqa: E402 from app.rag_store import VectorStore # noqa: E402 from app.router import ModelRouter # noqa: E402 S = get_settings() METRICS = MetricsStore(S.metrics_db_path) ROUTER = ModelRouter(build_registry(S), S, METRICS) OCR = build_ocr_registry(S) DB = Database(S.app_db_path) RAG = VectorStore(S.rag_db_path) CATS = [c["id"] for c in list_categories()] SAMPLES = sorted(p.name[:-len(".gt.json")] for p in S.evals_dataset_dir.glob("*.gt.json")) def _sample_path(sample_id: str): for ext in (".pdf", ".png", ".jpg", ".jpeg"): p = S.evals_dataset_dir / f"{sample_id}{ext}" if p.exists(): return p return None def run_sample(sample_id: str, ocr_backend: str): path = _sample_path(sample_id) if not path: return {"error": "sample not found"}, "", _kpis_md() return _run(path, sample_id, ocr_backend) def run_upload(file, ocr_backend: str): if not file: return {"error": "upload a PDF/PNG"}, "", _kpis_md() return _run(Path(file), Path(file).stem, ocr_backend) def _run(path, doc_id, ocr_backend): run = process_document(path, router=ROUTER, settings=S, metrics=METRICS, ocr_registry=OCR, db=DB, rag_store=RAG, doc_id=doc_id, ocr_backend=ocr_backend or "auto") st = run["_state"] summary = ( f"**Category:** {st['category']} · **Type:** {st['doc_type']} · " f"**Confidence:** {st['confidence']:.0%} · **OCR:** {st.get('ocr_backend')}\n\n" f"**Outcome:** {'✅ auto-posted ' + str(run['result'].get('post_id')) if run['result']['posted'] else '⚠ routed to human review'} · " f"**Cost:** ${run['total_cost_usd']:.5f} · **Latency:** {run['latency_ms']:.0f} ms\n\n" f"**OCR strategy:** {st['ocr'].get('strategy','')}" ) return st["extracted"], summary, _kpis_md() def _kpis_md() -> str: k = business_kpis(DB, METRICS) if not k.get("total_documents"): return "_No documents processed yet._" return ( f"### Live KPIs\n" f"- Documents: **{k['total_documents']}**\n" f"- Straight-through (auto-post): **{k['straight_through_rate']:.0%}**\n" f"- Human-in-the-loop: **{k['hitl_rate']:.0%}**\n" f"- OCR completion: **{k['ocr_completion_rate']:.0%}**\n" f"- Avg confidence: **{(k['avg_confidence'] or 0):.0%}**\n" f"- Cost / document: **${k['cost_per_document_usd']:.5f}**\n" f"- By category: `{json.dumps(k['by_category'])}`" ) def search(query: str): if not query: return [] return [[r["ref"], round(r["score"], 3), r["text"][:120]] for r in RAG.search(query, k=8)] def erp_ask(question: str): """ERP DocIQ: NLQ / analytics / summary / 'why' over the simulated ERP knowledgebase.""" from app.erp import ErpChat, get_warehouse if not (question or "").strip(): return "Ask about spend, vendors, late payments, inventory or returns.", [] chat = ErpChat(S, router=ROUTER, warehouse=get_warehouse(S), metrics=METRICS) r = chat.answer(question) md = (f"**{r['intent']}** · {r['engine']} · {r['model']} · {r['latency_ms']} ms\n\n" f"{r['answer']}\n\n" + (f"```sql\n{r['sql']}\n```" if r.get("sql") else "")) rows = r.get("rows") or [] table = [[*(str(v) for v in row)] for row in rows[:12]] if rows else [] return md, table def _erp_finetune_md() -> str: import json as _json from pathlib import Path p = Path(__file__).resolve().parent / "backend" / "finetune" / "erp_finetune_report.json" if not p.exists(): return "_Run `python scripts/finetune_erp.py` to populate fine-tune metrics._" d = _json.loads(p.read_text()); od = d.get("offline_demo") or d return ("### ERP-domain fine-tuning\n" f"- **Production target:** OpenBMB **MiniCPM3-4B** (LoRA recipe emitted)\n" f"- **Offline demo (CPU):** before **{od['before_test_accuracy']*100:.1f}%** → " f"after **{od['after_test_accuracy']*100:.1f}%** " f"(**+{od['accuracy_gain']*100:.0f} pts**) on {od['dataset_size']} examples; " f"routed-SQL exec {od['routed_sql_exec_rate']*100:.0f}%") def run_complex_web_automation(): """Intricate multi-step browser automation: ERP dashboard → Procurement → +Create Order → read the complex order-form fields.""" from app.browser import run_browser_agent # The Gradio app doesn't host the ERP portal, so drive the simulated browser # (the same multi-step flow runs on real Chromium in the full web app). res = run_browser_agent( "Open the ERP dashboard, click Procurement, click '+ Create Order', and read all order fields", router=ROUTER, settings=S, metrics=METRICS, scenario="complex_order", base_url="https://portal.local/portal", prefer_simulated=True) trace_md = f"**{res['backend']} browser · {res['agent_mode']} · {res['steps']} steps**\n\n" for t in res["trace"]: trace_md += f"- **step {t['step']}** `{t['tool']}` {t.get('args') or ''} — {t.get('note','')[:90]}\n" return trace_md, res.get("result") with gr.Blocks(title="ERP-DocIQ — Agentic Document Intelligence + ERP NLQ") as demo: gr.Markdown("# 📄 ERP-DocIQ — Agentic Document Intelligence + ERP NLQ, on small models\n" "Read documents (OCR + IDP), ask your ERP in plain English (NLQ → SQL), and automate the clicks — " "all on open models ≤32B, with **OpenBMB MiniCPM** (vision + reasoning) doing the heavy lifting. " "Built for the **Build Small Hackathon**.") with gr.Tab("Process a document"): with gr.Row(): with gr.Column(): backend = gr.Dropdown(["auto", *list_backends()], value="auto", label="OCR backend") sample = gr.Dropdown(SAMPLES, value=(SAMPLES[0] if SAMPLES else None), label="Sample document") run_btn = gr.Button("▶ Run sample", variant="primary") gr.Markdown("— or —") upload = gr.File(label="Upload PDF/PNG", file_types=[".pdf", ".png", ".jpg", ".jpeg"]) upload_btn = gr.Button("▶ Run upload") with gr.Column(): summary = gr.Markdown() extracted = gr.JSON(label="Extracted fields (multi-layer)") kpis = gr.Markdown(_kpis_md()) run_btn.click(run_sample, [sample, backend], [extracted, summary, kpis]) upload_btn.click(run_upload, [upload, backend], [extracted, summary, kpis]) with gr.Tab("ERP DocIQ (chat)"): gr.Markdown("### Ask your ERP reports — NLQ · analytics · summary · reasons\n" "Natural-language questions over a simulated retail ERP (vendors, POs, invoices, " "GL, inventory, returns). Figures come from **real SQL**; OpenBMB **MiniCPM3-4B** " "phrases summaries & explanations and never invents numbers.") erp_q = gr.Textbox(label="Question", placeholder="e.g. Why did spend rise in Q2 2026?") gr.Examples(["Who are the top 5 vendors by spend?", "What is the late-payment rate overall?", "Why did spend rise in Q2 2026?", "Summarize accounts payable health.", "Top return reasons by refund amount?"], inputs=erp_q) erp_btn = gr.Button("💬 Ask ERP DocIQ", variant="primary") erp_answer = gr.Markdown() erp_rows = gr.Dataframe(label="Query result (real SQL over the warehouse)") erp_btn.click(erp_ask, [erp_q], [erp_answer, erp_rows]) gr.Markdown(_erp_finetune_md()) with gr.Tab("Search (RAG)"): q = gr.Textbox(label="Query", placeholder="e.g. POS Cloud subscription renewal") search_btn = gr.Button("🔍 Search") results = gr.Dataframe(headers=["ref", "score", "text"], label="Results") search_btn.click(search, [q], [results]) with gr.Tab("Web Automation"): gr.Markdown("### Complex multi-step browser automation\n" "Replaces UiPath Studio Web: navigate the ERP dashboard → click the **Procurement** " "tile → click **+ Create Order** → open the order-form modal → **read the complex " "nested fields** (vendor & terms, ship-to, line items, totals, approver). Runs on real " "Chromium when Playwright is installed; replays on the simulated browser here.") cx_btn = gr.Button("▶ Run: Procurement → Create Order → read fields", variant="primary") with gr.Row(): cx_trace = gr.Markdown() cx_result = gr.JSON(label="Order fields read from the form") cx_btn.click(run_complex_web_automation, [], [cx_trace, cx_result]) if __name__ == "__main__": try: demo.launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft()) except TypeError: demo.launch(server_name="0.0.0.0", server_port=7860) # older gradio