| """Gradio front-end for the Lumen RAG Space (free-tier CPU, no Docker needed). |
| |
| Wraps `lumen_rag.RagEngine` directly in-process — no HTTP layer. |
| """ |
| from __future__ import annotations |
|
|
| from pathlib import Path |
|
|
| import spaces |
| import gradio as gr |
|
|
| from lumen_rag.engine import RagEngine |
| from lumen_rag.eval import evaluate |
| from lumen_rag.eval.harness import load_cases |
| from lumen_rag.ingestion.loaders import _LOADERS, load_file |
| from lumen_rag.retrieval import Retriever |
|
|
| def _find_repo_root() -> Path: |
| |
| |
| here = Path(__file__).resolve().parent |
| for candidate in (here, here.parent.parent.parent): |
| if (candidate / "data" / "docs").is_dir(): |
| return candidate |
| return here |
|
|
|
|
| _REPO_ROOT = _find_repo_root() |
| _SAMPLE_DIR = _REPO_ROOT / "data" / "docs" |
| _EVAL_PATH = _REPO_ROOT / "data" / "eval.jsonl" |
|
|
| engine = RagEngine() |
|
|
|
|
| def load_sample() -> str: |
| files = sorted(p for p in _SAMPLE_DIR.iterdir() if p.suffix.lower() in _LOADERS) |
| docs = [load_file(p) for p in files] |
| total = engine.add_documents(docs) |
| return f"Indexed {len(docs)} sample files, {total} chunks." |
|
|
|
|
| def upload_files(files: list[str]) -> str: |
| docs = [] |
| for f in files: |
| suffix = Path(f).suffix.lower() |
| if suffix not in _LOADERS: |
| continue |
| doc = load_file(f) |
| doc["id"] = Path(f).stem |
| docs.append(doc) |
| if not docs: |
| return f"No supported files. Supported: {sorted(_LOADERS)}" |
| total = engine.add_documents(docs) |
| return f"Indexed {len(docs)} files, {total} chunks." |
|
|
|
|
| def reset_index() -> str: |
| global engine |
| engine = RagEngine() |
| return "Index reset." |
|
|
|
|
| @spaces.GPU(duration=30) |
| def ask(question: str, k: int, mode: str): |
| if len(engine.store) == 0: |
| return "Index is empty — load the sample corpus or upload files first.", "" |
| result = engine.query(question, k=int(k), mode=mode) |
| citations = "\n".join( |
| f"[{c['n']}] {c['doc_id']} — {c['source']} (score={c['score']:.3f})" |
| for c in result.citations |
| ) |
| return result.text, citations |
|
|
|
|
| def run_eval(k: int) -> str: |
| if not _EVAL_PATH.exists(): |
| return "Bundled eval set not found." |
| if len(engine.store) == 0: |
| return "Index is empty — load the sample corpus first." |
| cases = load_cases(_EVAL_PATH) |
| report = evaluate(Retriever(engine.store, engine.embedder), cases, k=int(k)) |
| d = report.as_dict() |
| return "\n".join(f"{key}: {value}" for key, value in d.items()) |
|
|
|
|
| with gr.Blocks(title="Lumen RAG") as demo: |
| gr.Markdown( |
| "# Lumen RAG\n" |
| "Transparent, evaluated RAG: ingest documents, retrieve with hybrid " |
| "vector+BM25 search, answer with citations. Runs 100% offline on a " |
| "deterministic hashing embedder — no API key required." |
| ) |
|
|
| with gr.Row(): |
| sample_btn = gr.Button("Load sample corpus") |
| reset_btn = gr.Button("Reset index") |
| upload = gr.File(label="Or upload documents", file_count="multiple") |
| status = gr.Textbox(label="Index status", interactive=False) |
|
|
| sample_btn.click(load_sample, outputs=status) |
| reset_btn.click(reset_index, outputs=status) |
| upload.upload(upload_files, inputs=upload, outputs=status) |
|
|
| gr.Markdown("---") |
|
|
| question = gr.Textbox(label="Question") |
| with gr.Row(): |
| k = gr.Slider(1, 20, value=5, step=1, label="k") |
| mode = gr.Dropdown(["hybrid", "vector", "bm25"], value="hybrid", label="Retrieval mode") |
| ask_btn = gr.Button("Ask", variant="primary") |
| answer_box = gr.Textbox(label="Answer", lines=4) |
| citations_box = gr.Textbox(label="Citations", lines=4) |
|
|
| ask_btn.click(ask, inputs=[question, k, mode], outputs=[answer_box, citations_box]) |
|
|
| gr.Markdown("---") |
|
|
| with gr.Row(): |
| eval_k = gr.Slider(1, 20, value=5, step=1, label="eval k") |
| eval_btn = gr.Button("Run retrieval eval (recall@k, MRR, nDCG@k)") |
| eval_box = gr.Textbox(label="Eval report", lines=6) |
| eval_btn.click(run_eval, inputs=eval_k, outputs=eval_box) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|