From 59e53a27501748e3736e8216f4e52058ac2e8b84 Mon Sep 17 00:00:00 2001 From: Homelab Cursor Date: Thu, 26 Mar 2026 15:25:52 +0100 Subject: [PATCH] =?UTF-8?q?rag:=2019/20=20E2E-Tests=20bestanden=20?= =?UTF-8?q?=E2=80=94=20Dedup=20+=20Anti-Halluzination=20+=20Pflicht-Prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dedup auf Dateinamen-Ebene (Extension + Kopie-Marker ignorieren) - docnm_kwd boost 1.5→3.0 fuer bessere Ordner-Treffer - SYSTEM_PROMPT_EXTRA verschaerft: IMMER rag_search bei Dokument-Fragen - Expliziter Ende-Marker gegen LLM-Halluzination - MIN_TOP_K=5, Default top_k=8 - Content-Snippet 400→600 Zeichen Ref: Issue #51 --- homelab-ai-bot/tools/rag.py | 60 +++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/homelab-ai-bot/tools/rag.py b/homelab-ai-bot/tools/rag.py index b80126eb..8c428987 100644 --- a/homelab-ai-bot/tools/rag.py +++ b/homelab-ai-bot/tools/rag.py @@ -58,20 +58,27 @@ TOOLS = [ }, ] -SYSTEM_PROMPT_EXTRA = """RAG DOKUMENTENSUCHE: -Du hast Zugriff auf eine private Wissensbasis mit >21.000 Dokumenten (Vertraege, Versicherungen, Rente, Finanzamt, Familiendokumente, Anleitungen, Buecher, persoenliche Unterlagen). -Nutze rag_search wenn der User nach Dokumenten, Vertraegen, persoenlichen Unterlagen oder Informationen aus seinen Dateien fragt. -Die Suchanfrage sollte kurze Keywords sein, KEINE ganzen Saetze. Beispiele: -- "Familienbuch Opa Oma" -- "Grundsteuer Erklaerung" -- "Nuernberger Versicherung" -- "Allianz Beitraege" -Bei schlechten Ergebnissen: andere Keywords versuchen oder Dokumentnamen direkt suchen. -WICHTIG: -- Bei breiten Kategorie-Fragen ("welche Versicherungen", "alle Vertraege"): top_k=10 -- Liste NUR die Dokumente auf die rag_search zurueckliefert. ERFINDE KEINE Details die nicht im Ergebnis stehen. -- Der Ordnerpfad im Dokumentnamen (vor dem Dateinamen, getrennt durch __) zeigt die Kategorie. -- Wenn das Ergebnis Dokumente zeigt, liste sie auf — auch wenn du den Inhalt nicht vollstaendig kennst.""" +SYSTEM_PROMPT_EXTRA = """RAG DOKUMENTENSUCHE — PFLICHT-REGELN: +Du hast Zugriff auf eine private Wissensbasis mit >21.000 Dokumenten (Vertraege, Versicherungen, Rente, Finanzamt, Familiendokumente, Anleitungen, Buecher, persoenliche Unterlagen, Arbeitsvertraege, Kindergeld, Reisepass, Personalausweis, KFZ, Mietvertraege, Bausparvertraege, Rechnungen). + +WANN rag_search AUFRUFEN — IMMER bei diesen Fragen: +- "habe ich..." / "gibt es..." / "wo ist..." / "finde..." / "zeig mir..." + Dokument/Vertrag/Versicherung/Bescheid +- Jede Frage nach persoenlichen Unterlagen, Vertraegen, Versicherungen, Rechnungen, Bescheiden +- AUCH wenn du glaubst die Antwort zu kennen — das Gedaechtnis ist NICHT die Wissensbasis! +- AUCH wenn das Thema im Gedaechtnis steht — trotzdem rag_search aufrufen fuer vollstaendige Antwort + +WANN NICHT: Nur bei reinen Homelab/IT-Fragen, Smalltalk, oder wenn der User explizit NICHT nach Dokumenten fragt. + +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 +- 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 +- Wenn rag_search Treffer liefert: IMMER auflisten, auch wenn Inhalt unvollstaendig +- Antworte NIEMALS "keine gefunden" oder "nicht gespeichert" OHNE vorher rag_search aufgerufen zu haben""" def _basic_auth_header() -> str: @@ -121,6 +128,19 @@ def _folder_from_docname(name: str) -> str: return "" +def _dedup_key(name: str) -> str: + """Normalisiert Dokumentnamen fuer Deduplizierung. + + Extrahiert nur den Dateinamen (nach letztem __), ignoriert + Dateiendung und Kopie-Marker wie (1), (2). + 'Ordner__Foo(1).pdf' und 'Anderer__Foo.txt' werden als gleich behandelt. + """ + fname = name.rsplit("__", 1)[-1] if "__" in name else name + key = re.sub(r"\.(pdf|txt|docx?|xlsx?|csv|png|jpg|jpeg)$", "", fname, flags=re.IGNORECASE) + key = re.sub(r"\s*\(\d+\)\s*$", "", key).rstrip() + return key.lower() + + def _es_hybrid_search(query: str, es_size: int) -> dict: qvec = _ollama_embed(query) if not qvec: @@ -187,7 +207,6 @@ def handle_rag_search(query: str, top_k: int = 8, **kw): seen_docs: set[str] = set() lines: list[str] = [] - lines.append(f"**{len(hits)} Treffer fuer '{query}'** (Top {top_k} Dokumente):\n") count = 0 for h in hits: @@ -195,10 +214,10 @@ def handle_rag_search(query: str, top_k: int = 8, **kw): break src = h.get("_source") or {} doc_name = src.get("docnm_kwd") or "?" - doc_key = str(doc_name) - if doc_key in seen_docs: + dk = _dedup_key(doc_name) + if dk in seen_docs: continue - seen_docs.add(doc_key) + seen_docs.add(dk) score = h.get("_score") or 0.0 raw = src.get("content_with_weight") or src.get("content_de") or "" @@ -216,7 +235,10 @@ def handle_rag_search(query: str, top_k: int = 8, **kw): count += 1 if count == 0: - return f"Keine eindeutigen Dokumente fuer '{query}' (nach Deduplizierung)." + return f"Keine Dokumente fuer '{query}' gefunden." + + lines.insert(0, f"**{count} verschiedene Dokumente fuer '{query}':**\n") + lines.append("\n---\n(Ende der Ergebnisse. Nur diese Dokumente wurden gefunden.)") return "\n".join(lines)