M1+M2: RAG-Pipeline mit verbindlichem Grounding
agent/-Paket: Ingest (601 Layer-2-Eintraege -> 3005 Chunks, FTS5-BM25 + Vektoren-Cache), Hybrid-Retrieval (RRF, Stand-Boost, cross_ref-Erweiterung), Ollama-Client (embed/chat, think-Flag-Fallback, kurzes Connect-Budget), Systemprompt mit Zitierpflicht, Post-Validierung (zitierte IDs gemaess Retrieved-Set, 1x Regenerierung, dann Verweigerung), FastAPI (/ask, /health, /reindex), CLI, Goldset (31 Fragen, IDs gegen kb.json verifiziert, inkl. ATZ-Konfliktfall + 4 Verweigerungsfaelle), Eval-Suite, Test-Chat. 41 Offline-Tests gruen. Baseline BM25-only: Hit-Rate 0,871 / Recall@8 0,855 / MRR 0,476. Hybrid-Messung, Antwortmodus-Eval und Modell-Bake-off (M3) auf dem Host ausstaendig (Ollama aus der Zed-Sandbox nicht erreichbar). MEMORY.md und planung.md Umsetzungsstand aktualisiert.
This commit is contained in:
+124
@@ -0,0 +1,124 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user