ERP-DocIQ / gradio_app.py
kenmandal's picture
App heading → ERP-DocIQ (on-brand for hackathon)
3752979 verified
Raw
History Blame Contribute Delete
10 kB
"""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