062e010d7b
- Query-Planer (agent/query_planner.py): Heuristik-Gate (Jahr, Vergleichs-/ Aggregationsmarker, Laenge) entscheidet ueber einen kleinen LLM-Call, der komplexe Fragen in 1-3 Sub-Queries zerlegt (JSON, temp 0; Fehler -> Original als Einzel-Query). Je Sub-Query optional stand_year und scope. - Multi-Query-Retrieval: je Sub-Query BM25+Dense mit RRF-Summe; Per-Query-Slots (2 je Sub-Query) sichern jeden Frageaspekt im Kontext (sonst dominieren Eintraege, die in mehreren Sub-Queries mittelgut matchen — q-113-Befund). Scope-Filter: 'gesetz' (nur Lexis/WIKU/RIS) und 'kv' (nur Branchen-KV) mit Fallback auf unscoped bei leerem Ergebnis. Temporal-Intent: stand_year + temporal_boost (default 0, FTS-jahr-Tag-Signal reichte). - Grounding unveraendert: eine Retrieved-Menge (Union), eine Antwort, Post-Validierung ueber die Union, Verweigerungspflicht unveraendert. - Goldset 42 -> 46: q-110 (Temporal 2023; 2024er-Lohnordnung existiert im Korpus nicht - Mantelvertrag ohne Lohntabelle, korrekt verweigert), q-111 (2025), q-112 (Abfertigung-Vergleich), q-113 (Gesetz+KV). - Eval (46 Fragen): Zitier-Praezision 97,8 %, Verweigerung 97,8 % (Gate >94,3 % erfuellt), erwartete Quelle 90,2 %, Latenz mean 33,5 s. - Tests 50 -> 57. Reports lokal: data/eval-qwen38-stage1*.json. API-first festgehalten (D13-Vorbereitung): Odoo bleibt duenner Client; Lohndaten-Zugriff in M4 erfordert Privacy-Neubewertung.
120 lines
5.7 KiB
Python
120 lines
5.7 KiB
Python
"""Konfiguration des PV RAG Agent (alle Werte per Umgebungsvariable übersteuerbar)."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
|
|
|
|
def _env_str(name: str, default: str) -> str:
|
|
v = os.environ.get(name)
|
|
return v if v not in (None, "") else default
|
|
|
|
|
|
def _env_int(name: str, default: int) -> int:
|
|
try:
|
|
return int(os.environ.get(name, default))
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
def _env_float(name: str, default: float) -> float:
|
|
try:
|
|
return float(os.environ.get(name, default))
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
def _env_bool(name: str, default: bool) -> bool:
|
|
v = os.environ.get(name)
|
|
if v is None:
|
|
return default
|
|
return v.strip().lower() in ("1", "true", "yes", "on")
|
|
|
|
|
|
@dataclass
|
|
class Config:
|
|
# Pfade (relativ zum Repo-Root, sofern nicht absolut)
|
|
kb_dir: str = "wissensbasis"
|
|
db_path: str = "data/index.db"
|
|
|
|
# Ollama — Ziel-Instanz ist die Remote-GPU-Maschine im Tailscale-Netz
|
|
# (Entwicklungsumgebung, Radeon AI Pro R9700). Nicht auf localhost bzw.
|
|
# Port 11434 "korrigieren" — 11434 ist ggf. ein lokaler Ollama auf der
|
|
# Dev-Maschine, nicht die Ziel-Instanz.
|
|
ollama_url: str = "http://100.103.83.12:11435"
|
|
embed_model: str = "bge-m3"
|
|
answer_model: str = "qwen3.8:27b" # provisorisch bis Bake-off (M3)
|
|
|
|
# Generierung
|
|
temperature: float = 0.1
|
|
num_ctx: int = 32768 # qwen3.8: 256k-faehig; 16k trunciert lange KV-Prompts
|
|
# (KV-Cache @32k ~6-8 GB -> gesamt ~21 GB, Budget ok)
|
|
max_context_chars: int = 90_000 # Budget fuer User-Content; niedrig gerankte
|
|
# Bloecke werden weggelassen statt trunciert
|
|
num_predict: int = 1024
|
|
think: bool = False # Thinking per Request abschalten (Latenz)
|
|
chat_timeout_s: float = 300.0
|
|
embed_timeout_s: float = 240.0
|
|
|
|
# Retrieval
|
|
embed_off: bool = False # True = BM25-only (ohne Dense-Index/-Suche)
|
|
candidate_pool: int = 150 # Kandidaten je Liste vor der Fusion
|
|
# (KV/RIS-Erweiterung: Longtail-Spezialisten
|
|
# liegen sonst außerhalb der Kandidatur)
|
|
context_blocks: int = 8 # Kontext-Blöcke im Prompt
|
|
cross_ref_expand: int = 6 # Top-Eintraege, deren cross_refs ergaenzt
|
|
# (KV/RIS-Erweiterung: die Top-3 sind oft
|
|
# Branchen-KV-Bloecke mit leeren cross_refs —
|
|
# kuratierte Nachbarn kommen sonst nie nach)
|
|
cross_ref_max_extra: int = 6 # Obergrenze der Ergänzungen
|
|
rrf_k: int = 20 # RRF-Konstante (erweiterter Korpus: Top-Ränge
|
|
# müssen dominanter zählen)
|
|
dense_weight: float = 2.0 # RRF-Gewicht der Dense-Liste relativ zu BM25
|
|
# (BM25 ist durch KV-§-Titel-Matches inflationiert)
|
|
recency_boost: float = 0.005 # additiv auf RRF-Score, gewichtet nach Stand
|
|
|
|
# Query-Planer (Stufe 1, M6): komplexe Fragen -> 1-3 Sub-Queries
|
|
planner_enabled: bool = True # Heuristik-Gate entscheidet je Frage
|
|
planner_max_queries: int = 3
|
|
planner_num_predict: int = 220
|
|
planner_model: str = "" # leer = Antwortmodell
|
|
per_query_slots: int = 2 # Multi-Query: garantierte Kontext-Slots
|
|
# je Sub-Query (Multi-Hop-Abdeckung)
|
|
temporal_boost: float = 0.0 # Bonus fuer kv-Eintraege im gefragten
|
|
# Geltungsjahr (0 = nur FTS-Tag-Signal)
|
|
|
|
# Service
|
|
port: int = 8080
|
|
|
|
@classmethod
|
|
def from_env(cls) -> "Config":
|
|
d = cls()
|
|
return cls(
|
|
kb_dir=_env_str("PV_KB_DIR", d.kb_dir),
|
|
db_path=_env_str("PV_DB_PATH", d.db_path),
|
|
ollama_url=_env_str("OLLAMA_URL", d.ollama_url),
|
|
embed_model=_env_str("PV_EMBED_MODEL", d.embed_model),
|
|
answer_model=_env_str("PV_ANSWER_MODEL", d.answer_model),
|
|
temperature=_env_float("PV_TEMPERATURE", d.temperature),
|
|
num_ctx=_env_int("PV_NUM_CTX", d.num_ctx),
|
|
max_context_chars=_env_int("PV_MAX_CONTEXT_CHARS", d.max_context_chars),
|
|
num_predict=_env_int("PV_NUM_PREDICT", d.num_predict),
|
|
think=_env_bool("PV_THINK", d.think),
|
|
chat_timeout_s=_env_float("PV_CHAT_TIMEOUT_S", d.chat_timeout_s),
|
|
embed_timeout_s=_env_float("PV_EMBED_TIMEOUT_S", d.embed_timeout_s),
|
|
embed_off=_env_bool("PV_EMBED_OFF", d.embed_off),
|
|
candidate_pool=_env_int("PV_CANDIDATE_POOL", d.candidate_pool),
|
|
context_blocks=_env_int("PV_CONTEXT_BLOCKS", d.context_blocks),
|
|
cross_ref_expand=_env_int("PV_CROSS_REF_EXPAND", d.cross_ref_expand),
|
|
cross_ref_max_extra=_env_int("PV_CROSS_REF_MAX_EXTRA", d.cross_ref_max_extra),
|
|
rrf_k=_env_int("PV_RRF_K", d.rrf_k),
|
|
dense_weight=_env_float("PV_DENSE_WEIGHT", d.dense_weight),
|
|
recency_boost=_env_float("PV_RECENCY_BOOST", d.recency_boost),
|
|
planner_enabled=_env_bool("PV_QUERY_PLANNER", d.planner_enabled),
|
|
planner_max_queries=_env_int("PV_PLANNER_MAX_QUERIES", d.planner_max_queries),
|
|
planner_num_predict=_env_int("PV_PLANNER_NUM_PREDICT", d.planner_num_predict),
|
|
planner_model=_env_str("PV_PLANNER_MODEL", d.planner_model),
|
|
per_query_slots=_env_int("PV_PER_QUERY_SLOTS", d.per_query_slots),
|
|
temporal_boost=_env_float("PV_TEMPORAL_BOOST", d.temporal_boost),
|
|
port=_env_int("PV_PORT", d.port),
|
|
) |