"""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 = 2048 # Output-Budget; 1024 schnitt lange belegte # Antworten ab (done_reason=length, q-024-Fall) # -> unvollstaendige Zitationen 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) survey_blocks: int = 16 # Map-Reduce (Stufe 2): breiteres # Retrieval fuer Survey-Fragen 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), survey_blocks=_env_int("PV_SURVEY_BLOCKS", d.survey_blocks), temporal_boost=_env_float("PV_TEMPORAL_BOOST", d.temporal_boost), port=_env_int("PV_PORT", d.port), )