fix(rag): breite Mehrfachsuche + mehr Treffer fuer Uebersichten

- wide_recall: bis 16 ES-Runden mit Sparten-/Gesellschafts-Queries, Merge
  nach Dedup-Key, bis 25 distinct Treffer, groessere Snippets.
- Normale Suche: top_k bis 15, ES bis 150.
- Forciertes RAG: top_k 25, Tool-Payload 32k Zeichen.
- Hinweis: 100% Vollstaendigkeit haengt von Index/OCR ab.
This commit is contained in:
Homelab Cursor 2026-03-26 17:12:13 +01:00
parent a9d1069728
commit dcf70b087b
2 changed files with 162 additions and 21 deletions

View file

@ -450,7 +450,7 @@ def ask_with_tools(question: str, tool_handlers: dict, session_id: str = None, d
question
+ " Versicherung Beitrag Beitragsrechnung Jahresbetrag"
)
_rag_res = _rag_fn(query=_rag_q, top_k=10)
_rag_res = _rag_fn(query=_rag_q, top_k=25)
if _rag_res and not _rag_res.startswith("Keine"):
log.info("RAG-Pflicht: %d Zeichen — loesche Session-History", len(str(_rag_res)))
messages = [
@ -467,9 +467,9 @@ def ask_with_tools(question: str, tool_handlers: dict, session_id: str = None, d
{"role": "assistant", "content": None,
"tool_calls": [{"id": "forced_rag", "type": "function",
"function": {"name": "rag_search",
"arguments": json.dumps({"query": _rag_q, "top_k": 10})}}]},
"arguments": json.dumps({"query": _rag_q, "top_k": 25})}}]},
{"role": "tool", "tool_call_id": "forced_rag",
"content": str(_rag_res)[:12000]},
"content": str(_rag_res)[:32000]},
{"role": "user", "content": question},
]
except Exception as e:

View file

@ -21,6 +21,10 @@ OLLAMA_EMBED_URL = "http://100.84.255.83:11434/api/embeddings"
EMBED_MODEL = "nomic-embed-text"
MIN_TOP_K = 5
# Breite Übersichten: mehr ES-Runden, mehr distinct Treffer
MAX_TOP_K_NORMAL = 15
MAX_TOP_K_WIDE = 25
ES_SIZE_CAP = 150
TOOLS = [
{
@ -33,8 +37,8 @@ TOOLS = [
"Anleitungen, Buecher, persoenliche Unterlagen). "
"Nutze dieses Tool wenn der User nach einem bestimmten Dokument, "
"Vertrag, Brief oder persoenlicher Information fragt. "
"Bei breiten Fragen ('welche Versicherungen', 'alle Vertraege') "
"immer top_k=10 verwenden."
"Bei breiten Fragen ('welche Versicherungen', Jahreskosten, Listen) "
"top_k=15 oder hoeher setzen."
),
"parameters": {
"type": "object",
@ -48,8 +52,8 @@ TOOLS = [
},
"top_k": {
"type": "integer",
"description": "Anzahl Ergebnisse (5-10, Standard 8)",
"default": 8,
"description": "Anzahl Ergebnisse (5-25, Standard 10)",
"default": 10,
},
},
"required": ["query"],
@ -73,7 +77,7 @@ SUCHANFRAGE: Kurze Keywords, KEINE ganzen Saetze. Beispiele:
- "Familienbuch" / "Grundsteuer Erklaerung" / "Haftpflicht" / "Kindergeld" / "Mietvertrag" / "Arbeitsvertrag" / "Reisepass"
ERGEBNISSE AUSWERTEN:
- Bei breiten Fragen ("welche Versicherungen", "alle Vertraege"): top_k=10
- Bei breiten Fragen ("welche Versicherungen", Jahreskosten, Listen): top_k=15-25, ALLE Treffer aus der Tool-Antwort abarbeiten
- Liste die gefundenen Dokumente mit Ordner und kurzem Inhalt auf
- ERFINDE KEINE Details die nicht im Ergebnis stehen
- Der Ordnerpfad (vor dem Dateinamen, getrennt durch __) zeigt die Kategorie
@ -147,6 +151,7 @@ def _es_hybrid_search(query: str, es_size: int) -> dict:
if not qvec:
return {"_error": "Embedding fehlgeschlagen (Ollama nicht erreichbar?)."}
es_size = min(ES_SIZE_CAP, max(es_size, 20))
kb_filter = {"term": {"kb_id": KB_ID}}
body = {
"size": es_size,
@ -154,7 +159,7 @@ def _es_hybrid_search(query: str, es_size: int) -> dict:
"field": "q_768_vec",
"query_vector": qvec,
"k": es_size,
"num_candidates": min(500, max(es_size * 5, 120)),
"num_candidates": min(500, max(es_size * 5, 150)),
"filter": [kb_filter],
},
"query": {
@ -191,25 +196,152 @@ def _es_hybrid_search(query: str, es_size: int) -> dict:
return {"_error": str(e)}
def _is_wide_recall_query(q: str) -> bool:
"""Übersichts-/Listen-/Kostenfragen: mehrfach suchen und mergen."""
ql = (q or "").lower()
if any(x in ql for x in ("welche versicherung", "alle versicherung", "versicherungen habe")):
return True
if "versicherung" in ql and any(
x in ql
for x in (
"welche",
"alle",
"liste",
"überblick",
"ueberblick",
"kosten",
"beitrag",
"jähr",
"jaehr",
"jahres",
"gesamt",
"summe",
"übersicht",
"uebersicht",
)
):
return True
costish = any(
x in ql
for x in (
"kosten",
"beitrag",
"beiträge",
"beitraege",
"eur",
"euro",
"jähr",
"jaehr",
"jahreskosten",
"prämie",
"praemie",
)
)
broad = any(
x in ql
for x in (
"liste",
"übersicht",
"uebersicht",
"alle",
"gesamt",
"summe",
"jährlich",
"jaehrlich",
)
)
return costish and broad
# Zusatzanfragen decken Sparten + Gesellschaften ab (Recall)
_WIDE_SUBQUERIES = [
"Versicherung Beitragsrechnung Jahresbeitrag",
"Wohngebäudeversicherung Gebäude Beitrag",
"Hausratversicherung Beitrag Ergo",
"Haftpflichtversicherung Beitrag GARANTA",
"Kfz Kasko Haftpflicht Beitragsrechnung",
"Rechtsschutzversicherung Beitrag",
"Lebensversicherung Beitrag",
"Krankenversicherung PKV Beitrag",
"Sachversicherung LVM Beitrag",
"LVM AutoPlus Versicherungsschein",
"Allianz Versicherung Police",
"Nürnberger Versicherung Beitrag",
"Ergo Versicherung Police",
"Unfallversicherung Berufsunfähigkeit",
"Bausparvertrag Bauspar",
]
def _merge_hits_from_queries(queries: list[str], es_size: int, pool_cap: int) -> tuple[list, str | None]:
"""Führt mehrere Hybrid-Suchen aus; pro Dokument höchster Score."""
best: dict[str, dict] = {}
last_err: str | None = None
def absorb(hits: list) -> None:
for h in hits:
src = h.get("_source") or {}
dn = src.get("docnm_kwd") or "?"
dk = _dedup_key(dn)
sc = float(h.get("_score") or 0.0)
old = best.get(dk)
if old is None or sc > float(old.get("_score") or 0.0):
best[dk] = h
for q in queries:
q = (q or "").strip()
if not q:
continue
data = _es_hybrid_search(q, es_size)
if "_error" in data:
last_err = str(data["_error"])
log.warning("wide_recall subquery fail %s: %s", q[:40], last_err)
continue
absorb((data.get("hits") or {}).get("hits") or [])
merged = sorted(best.values(), key=lambda h: float(h.get("_score") or 0.0), reverse=True)
return merged[:pool_cap], last_err
def handle_rag_search(query: str, top_k: int = 8, **kw):
if not query or not query.strip():
return "rag_search: query fehlt."
top_k = max(MIN_TOP_K, min(int(top_k or 8), 10))
es_size = min(120, max(top_k * 12, 50))
qstrip = query.strip()
wide = _is_wide_recall_query(qstrip)
cap = MAX_TOP_K_WIDE if wide else MAX_TOP_K_NORMAL
top_k = max(MIN_TOP_K, min(int(top_k or 10), cap))
es_size = min(ES_SIZE_CAP, max(top_k * 10, 70))
data = _es_hybrid_search(query.strip(), es_size)
if wide:
subqs = [qstrip]
for sq in _WIDE_SUBQUERIES:
if sq.lower() not in qstrip.lower():
subqs.append(sq)
pool_cap = max(top_k * 3, 45)
hits, err = _merge_hits_from_queries(subqs[:16], es_size, pool_cap=pool_cap)
if err and not hits:
return f"Fehler bei der Dokumentensuche: {err}"
header = (
f"**Breitensuche ({len(subqs[:16])} Anfragen gemerged) fuer '{qstrip}'"
f"{len(hits)} Kandidaten, zeige bis {top_k} distinct:**\n"
)
snip_len = 750
else:
data = _es_hybrid_search(qstrip, es_size)
if "_error" in data:
return f"Fehler bei der Dokumentensuche: {data['_error']}"
hits = (data.get("hits") or {}).get("hits") or []
header = f"**Dokumente fuer '{qstrip}' (bis {top_k}):**\n"
snip_len = 650
if not hits:
return f"Keine Ergebnisse fuer '{query}' in der Wissensbasis gefunden."
return f"Keine Ergebnisse fuer '{qstrip}' in der Wissensbasis gefunden."
seen_docs: set[str] = set()
lines: list[str] = []
count = 0
for h in hits:
if count >= top_k:
break
@ -222,7 +354,7 @@ def handle_rag_search(query: str, top_k: int = 8, **kw):
score = h.get("_score") or 0.0
raw = src.get("content_with_weight") or src.get("content_de") or ""
content = raw[:600].strip()
content = raw[:snip_len].strip()
ocr = _ocr_note(raw)
folder = _folder_from_docname(doc_name)
filename = doc_name.rsplit("__", 1)[-1] if "__" in doc_name else doc_name
@ -236,10 +368,19 @@ def handle_rag_search(query: str, top_k: int = 8, **kw):
count += 1
if count == 0:
return f"Keine Dokumente fuer '{query}' gefunden."
return f"Keine Dokumente fuer '{qstrip}' gefunden."
lines.insert(0, f"**{count} verschiedene Dokumente fuer '{query}':**\n")
lines.append("\n---\n(Ende der Ergebnisse. Nur diese Dokumente wurden gefunden.)")
lines.insert(0, header)
tail = (
"\n---\n(Ende der Ergebnisse. Nur diese Dokumente in dieser Runde. "
+ (
"Bei Summen/Zahlen: alle Treffer prüfen; OCR kann unvollständig sein."
if wide
else ""
)
+ ")"
)
lines.append(tail)
return "\n".join(lines)