From 97985371cac339029a471debe03bc19a8e42ee61 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 9 Dec 2025 09:58:35 +0100 Subject: [PATCH] neue engine mit mehreren typen --- app/routers/chat.py | 73 ++++++++++++++++++++++++++++--------- config/decision_engine.yaml | 38 +++++++++---------- 2 files changed, 72 insertions(+), 39 deletions(-) diff --git a/app/routers/chat.py b/app/routers/chat.py index 81ae964..9afcf59 100644 --- a/app/routers/chat.py +++ b/app/routers/chat.py @@ -1,10 +1,10 @@ """ -app/routers/chat.py — RAG Endpunkt (WP-06 Decision Engine - Late Binding Refactor) +app/routers/chat.py — RAG Endpunkt (WP-06 Decision Engine - Full Config Refactor) Zweck: Verbindet Retrieval mit LLM-Generation. WP-06: Implementiert Intent Detection und Strategic Retrieval. - Update: Konfiguration via decision_engine.yaml (Late Binding). + Update: Konfiguration via decision_engine.yaml (Late Binding) mit 'Best Match' Logik. """ from fastapi import APIRouter, HTTPException, Depends @@ -25,14 +25,23 @@ logger = logging.getLogger(__name__) # --- Helper: Config Loader --- +# Cache für die Config (damit wir nicht bei jedem Request lesen) +_DECISION_CONFIG_CACHE = None + def _load_decision_config() -> Dict[str, Any]: """Lädt die Decision-Engine Konfiguration (Late Binding).""" settings = get_settings() path = Path(settings.DECISION_CONFIG_PATH) + + # Default Fallback, falls YAML kaputt/weg default_config = { "strategies": { - "FACT": {"inject_types": [], "prompt_template": "rag_template"}, - "DECISION": {"inject_types": ["value", "principle"], "prompt_template": "decision_template"} + "FACT": {"trigger_keywords": []}, + "DECISION": { + "trigger_keywords": ["soll ich", "meinung"], + "inject_types": ["value", "principle"], + "prompt_template": "decision_template" + } } } @@ -47,15 +56,17 @@ def _load_decision_config() -> Dict[str, Any]: logger.error(f"Failed to load decision config: {e}") return default_config -# Cache für die Config (damit wir nicht bei jedem Request lesen) -_DECISION_CONFIG_CACHE = None - -def get_decision_strategy(intent: str) -> Dict[str, Any]: +def get_full_config() -> Dict[str, Any]: + """Gibt die ganze Config zurück (für Intent Detection).""" global _DECISION_CONFIG_CACHE if _DECISION_CONFIG_CACHE is None: _DECISION_CONFIG_CACHE = _load_decision_config() - - strategies = _DECISION_CONFIG_CACHE.get("strategies", {}) + return _DECISION_CONFIG_CACHE + +def get_decision_strategy(intent: str) -> Dict[str, Any]: + """Gibt die Strategie für einen spezifischen Intent zurück.""" + config = get_full_config() + strategies = config.get("strategies", {}) # Fallback auf FACT, wenn Intent unbekannt return strategies.get(intent, strategies.get("FACT", {})) @@ -74,6 +85,8 @@ def get_retriever(): def _build_enriched_context(hits: List[QueryHit]) -> str: """ Baut einen 'Rich Context' String. + Statt nur Text, injizieren wir Metadaten (Typ, Tags), damit das LLM + die semantische Rolle des Schnipsels versteht. """ context_parts = [] @@ -105,14 +118,38 @@ def _build_enriched_context(hits: List[QueryHit]) -> str: async def _classify_intent(query: str, llm: LLMService) -> str: """ - WP-06: Intent Detection (Simple Keyword Heuristic for Speed). - TODO: Move keywords to config if needed later. + WP-06: Intent Detection (Best Match / Longest Keyword Wins). + + Prüft Keywords aus der YAML gegen die Query. + Wenn mehrere Strategien passen, gewinnt die mit dem längsten Keyword (Spezifität). """ - # Performance-Optimierung: Keywords statt LLM Call - keywords = ["soll ich", "meinung", "besser", "empfehlung", "strategie", "entscheidung", "wert", "prinzip"] - if any(k in query.lower() for k in keywords): - return "DECISION" - return "FACT" + config = get_full_config() + strategies = config.get("strategies", {}) + + query_lower = query.lower() + + best_intent = "FACT" + max_match_length = 0 + + # Iteriere über alle Strategien + for intent_name, strategy in strategies.items(): + if intent_name == "FACT": + continue + + keywords = strategy.get("trigger_keywords", []) + + # Prüfe jedes Keyword + for k in keywords: + # Wenn Keyword im Text ist... + if k.lower() in query_lower: + # ... prüfen wir, ob es spezifischer (länger) ist als der bisherige Favorit + current_len = len(k) + if current_len > max_match_length: + max_match_length = current_len + best_intent = intent_name + # Wir brechen hier NICHT ab, sondern suchen weiter nach noch längeren Matches + + return best_intent @router.post("/", response_model=ChatResponse) async def chat_endpoint( @@ -126,7 +163,7 @@ async def chat_endpoint( logger.info(f"Chat request [{query_id}]: {request.message[:50]}...") try: - # 1. Intent Detection + # 1. Intent Detection (Config-Driven & Best Match) intent = await _classify_intent(request.message, llm) logger.info(f"[{query_id}] Detected Intent: {intent}") diff --git a/config/decision_engine.yaml b/config/decision_engine.yaml index 6145ad1..09fce63 100644 --- a/config/decision_engine.yaml +++ b/config/decision_engine.yaml @@ -1,25 +1,21 @@ -# config/decision_engine.yaml -# Steuerung der Decision Engine (WP-06) -# Hier wird definiert, wie auf verschiedene Intents reagiert wird. - version: 1.0 strategies: - # 1. Fakten-Abfrage (Standard) - FACT: - description: "Reine Wissensabfrage." - inject_types: [] # Keine speziellen Typen erzwingen - prompt_template: "rag_template" - prepend_instruction: null # Keine spezielle Anweisung im Context - - # 2. Entscheidungs-Frage (WP-06) + # Strategie 1: Der Berater (Das haben wir gebaut) DECISION: - description: "Der User sucht Rat, Strategie oder Abwägung." - # HIER definierst du, was das 'Gewissen' ausmacht: - # Aktuell: Werte & Prinzipien. - # Später einfach ergänzen um: "goal", "experience", "belief" - inject_types: ["value", "principle", "goal"] - prompt_template: "decision_template" - prepend_instruction: | - !!! ENTSCHEIDUNGS-MODUS !!! - BITTE WÄGE FAKTEN GEGEN FOLGENDE WERTE/PRINZIPIEN AB: \ No newline at end of file + trigger_keywords: ["soll ich", "empfehlung", "strategie"] + inject_types: ["value", "principle", "goal"] + prompt_template: "decision_template" # Nutzt das "Abwägen"-Template + + # Strategie 2: Der empathische Zuhörer (NEU - Konzept) + EMPATHY: + trigger_keywords: ["ich fühle", "traurig", "gestresst", "angst"] + inject_types: ["belief", "experience"] # Lädt Glaubenssätze & eigene Erfahrungen + prompt_template: "empathy_template" # Ein Template, das auf "Zuhören" getrimmt ist + prepend_instruction: "SEI EMPATHISCH. SPIEGEL DIE GEFÜHLE." + + # Strategie 3: Der Coder (NEU - Konzept) + CODING: + trigger_keywords: ["code", "python", "funktion", "bug"] + inject_types: ["snippet", "reference"] # Lädt nur technische Schnipsel + prompt_template: "technical_template" # Ein Template, das Codeblöcke erzwingt \ No newline at end of file