lumen-rag / lumen_rag /cli.py
WickTech's picture
Space README + Gradio entrypoint
fd3cc1a verified
Raw
History Blame Contribute Delete
2.74 kB
"""Command-line interface: ingest, ask, eval, serve."""
from __future__ import annotations
import json
import sys
from pathlib import Path
import typer
# Ensure Unicode output (arrows, box-drawing) doesn't crash on Windows' cp1252 console.
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8") # type: ignore[union-attr]
except (AttributeError, ValueError):
pass
from .config import settings
from .engine import RagEngine
from .eval import evaluate
from .eval.harness import load_cases
from .ingestion.loaders import _LOADERS
from .retrieval import Retriever, RetrievalMode
app = typer.Typer(help="Lumen RAG — ingest, ask, and evaluate a RAG pipeline.")
@app.command()
def ingest(
paths: list[str] = typer.Argument(..., help="Files or directories (.txt/.md)."),
chunk_size: int = 120,
overlap: int = 20,
) -> None:
"""Index documents into the persistent vector store."""
files: list[Path] = []
for p in paths:
path = Path(p)
if path.is_dir():
for ext in _LOADERS:
files.extend(path.rglob(f"*{ext}"))
else:
files.append(path)
engine = (
RagEngine.load() if Path(settings.index_dir, "chunks.json").exists() else RagEngine()
)
docs = [
{"id": f.stem, "text": f.read_text(encoding="utf-8"), "metadata": {"source": str(f)}}
for f in files
]
total = engine.add_documents(docs, chunk_size=chunk_size, overlap=overlap)
engine.save()
typer.echo(f"Indexed {len(files)} file(s) → {total} chunks in {settings.index_dir}/")
@app.command()
def ask(
question: str,
k: int = 5,
mode: str = typer.Option("hybrid", help="Retrieval mode: vector | bm25 | hybrid"),
) -> None:
"""Query the index and print an answer with citations."""
engine = RagEngine.load()
result = engine.query(question, k=k, mode=mode) # type: ignore[arg-type]
typer.echo("\n" + result.text + "\n")
typer.echo("Sources:")
for c in result.citations:
typer.echo(f" [{c['n']}] {c['doc_id']} (score={c['score']})")
@app.command(name="eval")
def run_eval(dataset: str, k: int = 5) -> None:
"""Score the retriever against a JSONL of labelled questions."""
engine = RagEngine.load()
cases = load_cases(dataset)
report = evaluate(Retriever(engine.store, engine.embedder), cases, k=k)
typer.echo("\n" + report.pretty() + "\n")
typer.echo(json.dumps(report.as_dict()))
@app.command()
def serve(host: str = "0.0.0.0", port: int = 8000) -> None:
"""Run the FastAPI server."""
import uvicorn
uvicorn.run("lumen_rag.api.app:app", host=host, port=port, reload=False)
if __name__ == "__main__":
app()