"""CLI des PV RAG Agent. python -m agent.cli ingest [--no-embed] Index (neu) aufbauen python -m agent.cli ask "Frage?" [--top-k N] [--json] python -m agent.cli eval [--answers] [--limit N] [--k 8] [--json-out FILE] python -m agent.cli serve [--host 0.0.0.0] """ from __future__ import annotations import argparse import json import sys from .config import Config def _cmd_ingest(args: argparse.Namespace, cfg: Config) -> int: import dataclasses from .ingest import build_index client = None if args.no_embed: cfg = dataclasses.replace(cfg, embed_off=True) elif not cfg.embed_off: from .ollama_client import OllamaClient client = OllamaClient( cfg.ollama_url, embed_timeout_s=cfg.embed_timeout_s, chat_timeout_s=cfg.chat_timeout_s, ) if not client.is_up(): print( f"[warn] Ollama unter {cfg.ollama_url} nicht erreichbar — " "Index wird BM25-only aufgebaut.", file=sys.stderr, ) try: stats = build_index(cfg, client=client) finally: if client is not None: client.close() print(json.dumps(stats.as_dict(), indent=2, ensure_ascii=False)) if stats.embed_error: print(f"[warn] {stats.embed_error}", file=sys.stderr) return 0 def _cmd_ask(args: argparse.Namespace, cfg: Config) -> int: from .generate import answer_question try: result = answer_question(args.question, cfg, top_k=args.top_k) except Exception as e: print( f"[Fehler] Antwortgenerierung fehlgeschlagen: " f"{type(e).__name__}: {e}\n" f"Ollama erreichbar unter {cfg.ollama_url}? 'curl {cfg.ollama_url}/api/tags'", file=sys.stderr, ) return 2 if args.json: print(json.dumps(result, indent=2, ensure_ascii=False)) return 0 print(result["answer"]) if result["sources"]: print("\nQuellen:") for s in result["sources"]: print(f" - {s['id']} — {s['title']} ({s['stand']})") status = "VERWEIGERT" if result["refused"] else ( "OK" if result["verified"] else "UNVERIFIZIERT" ) print( f"\n[{status} · {result['model']} · {result['latency_ms']} ms · " f"{result['n_context']} Kontextblöcke]" ) return 0 def _cmd_eval(args: argparse.Namespace, cfg: Config) -> int: from .eval.evaluate import run_eval return run_eval(cfg, args) def _cmd_serve(args: argparse.Namespace, cfg: Config) -> int: import uvicorn uvicorn.run("agent.api:app", host=args.host, port=cfg.port, log_level="info") return 0 def main(argv: list[str] | None = None) -> int: cfg = Config.from_env() parser = argparse.ArgumentParser(prog="agent.cli", description=__doc__) sub = parser.add_subparsers(dest="cmd", required=True) p_ingest = sub.add_parser("ingest", help="Index (neu) aufbauen") p_ingest.add_argument("--no-embed", action="store_true", help="Keine Embeddings erzeugen (BM25-only)") p_ask = sub.add_parser("ask", help="Frage stellen") p_ask.add_argument("question") p_ask.add_argument("--top-k", type=int, default=None) p_ask.add_argument("--json", action="store_true") p_eval = sub.add_parser("eval", help="Goldset-Evaluation") p_eval.add_argument("--answers", action="store_true", help="inkl. Antwortgenerierung (benötigt Ollama)") p_eval.add_argument("--limit", type=int, default=None) p_eval.add_argument("--k", type=int, default=8, help="K für Recall@k") p_eval.add_argument("--json-out", default=None) p_serve = sub.add_parser("serve", help="HTTP-API starten") p_serve.add_argument("--host", default="127.0.0.1") args = parser.parse_args(argv) handlers = { "ingest": _cmd_ingest, "ask": _cmd_ask, "eval": _cmd_eval, "serve": _cmd_serve, } return handlers[args.cmd](args, cfg) if __name__ == "__main__": raise SystemExit(main())