homelab-brain/homelab-ai-bot/rag_mode.py
root 10f3c8323a rag_mode: RAG-first als Default + Persistenz, Router haertet persoenliche Fragen
rag_mode.py:
 - Default fuer neue/unbekannte Chats: RAG-first (True statt False)
 - State wird auf Disk geschrieben (/opt/homelab-ai-bot/data/rag_mode.json),
   ueberlebt Bot-Restarts; threadsicher.

llm.py:
 - _LOCAL_OVERRIDES erweitert um persoenliche Possessiv-/Besitz-Marker:
   wohnung(en), apartment, condo/kondo, immobilie, kambodscha/cambodia,
   phnom penh, arakawa, gekostet, kaufpreis, bezahlt, ausgegeben, ueberweisung,
   meine/mein/meines/..., was haben, wie viel habe ich, ich fuer/für.
   Damit werden klar persoenliche Fragen nie mehr faelschlich an Sonar
   geroutet, selbst wenn Web-Trigger wie "wie viel" im Text vorkommen.

Hintergrund: Eine Frage der Form "wie viel habe ich fuer die Wohnungen in
Kambodscha bezahlt" wurde an Perplexity/Sonar geroutet (Websuche) statt an
RAG, weil der Mode-Schalter durch einen Bot-Restart im RAM verloren ging und
der Router bei "wie viel" sofort MODEL_ONLINE waehlte.
2026-04-17 21:57:42 +02:00

97 lines
2.7 KiB
Python

"""Pro-Chat: Betriebsart Unterlagen zuerst (RAG vor Web).
Persistenz: /opt/homelab-ai-bot/data/rag_mode.json (ueberlebt Bot-Restarts).
Default fuer neue Chats: True (RAG-first, keine Web-Suche ohne Zutun).
"""
from __future__ import annotations
import json
import logging
import os
import threading
from pathlib import Path
from typing import Optional, Tuple
log = logging.getLogger("rag_mode")
_STATE_PATH = Path(os.environ.get("RAG_MODE_STATE", "/opt/homelab-ai-bot/data/rag_mode.json"))
_DEFAULT_ON = True
_lock = threading.Lock()
_active: dict[str, bool] = {}
_loaded = False
BTN_OFF = "📁 Unterlagen: AUS"
BTN_ON = "📁 Unterlagen: AN"
def _load() -> None:
global _loaded, _active
if _loaded:
return
try:
if _STATE_PATH.exists():
data = json.loads(_STATE_PATH.read_text(encoding="utf-8") or "{}")
if isinstance(data, dict):
_active = {str(k): bool(v) for k, v in data.items()}
log.info("rag_mode geladen: %s Eintraege aus %s", len(_active), _STATE_PATH)
except Exception as e:
log.warning("rag_mode konnte State nicht laden: %s", e)
_loaded = True
def _save() -> None:
try:
_STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
_STATE_PATH.write_text(json.dumps(_active, ensure_ascii=False, indent=2), encoding="utf-8")
except Exception as e:
log.warning("rag_mode konnte State nicht schreiben: %s", e)
def is_document_mode(channel_key: str) -> bool:
with _lock:
_load()
return _active.get(channel_key, _DEFAULT_ON)
def set_document_mode(channel_key: str, on: bool) -> None:
with _lock:
_load()
_active[channel_key] = bool(on)
_save()
def toggle_document_mode(channel_key: str) -> bool:
cur = is_document_mode(channel_key)
set_document_mode(channel_key, not cur)
return not cur
def keyboard_label(channel_key: str) -> str:
return BTN_ON if is_document_mode(channel_key) else BTN_OFF
def is_mode_button(text: str) -> bool:
t = (text or "").strip()
return t in (BTN_ON, BTN_OFF)
def handle_mode_button(text: str, channel_key: str) -> Optional[bool]:
"""Returns True if turned ON, False if OFF, None if not a mode button."""
t = (text or "").strip()
if t == BTN_OFF:
set_document_mode(channel_key, True)
return True
if t == BTN_ON:
set_document_mode(channel_key, False)
return False
return None
def strip_document_prefix(text: str) -> Tuple[str, bool]:
t = (text or "").strip()
low = t.lower()
for p in ("doku:", "rag:", "#doku"):
if low.startswith(p):
return t[len(p) :].lstrip(), True
return t, False