""" app/services/llm_service.py — LLM Client (Ollama) Version: 0.2.0 (WP-06 Hybrid Router Support) """ import httpx import yaml import logging import os from pathlib import Path from app.config import get_settings logger = logging.getLogger(__name__) class LLMService: def __init__(self): self.settings = get_settings() self.prompts = self._load_prompts() # Timeout aus Config nutzen (Default 120s) self.client = httpx.AsyncClient( base_url=self.settings.OLLAMA_URL, timeout=self.settings.LLM_TIMEOUT ) def _load_prompts(self) -> dict: """Lädt Prompts aus der konfigurierten YAML-Datei.""" path = Path(self.settings.PROMPTS_PATH) if not path.exists(): logger.warning(f"Prompt config not found at {path}, using defaults.") return {} try: with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) except Exception as e: logger.error(f"Failed to load prompts: {e}") return {} async def generate_raw_response(self, prompt: str) -> str: """ Führt einen direkten LLM Call ohne RAG-Template aus. Ideal für Classification/Routing. """ payload = { "model": self.settings.LLM_MODEL, "prompt": prompt, "stream": False, "options": { "temperature": 0.0, # Deterministisch für Routing! "num_ctx": 512 # Kleines Fenster reicht für Classification } } try: response = await self.client.post("/api/generate", json=payload) if response.status_code != 200: logger.error(f"Ollama Error ({response.status_code}): {response.text}") return "FACT" # Fallback bei Fehler data = response.json() return data.get("response", "").strip() except Exception as e: logger.error(f"LLM Raw Gen Error: {e}") return "FACT" async def generate_rag_response(self, query: str, context_str: str) -> str: """ Generiert eine Antwort basierend auf Query und Kontext (RAG). """ # Template hier ist nur Fallback, falls im Router nichts übergeben wird. # Im Normalfall formatiert der Router den context_str bereits vor oder übergibt das Template. # Hier nutzen wir simple substitution, da der Prompt meist schon vom Router aufbereitet ist # oder wir nutzen das Standard-Template aus der YAML. # HINWEIS: In der neuen Architektur (chat.py) wird das Template bereits VOR diesem Aufruf # geladen und formatiert, und als 'prompt' übergeben? # Nein, chat.py ruft generate_rag_response(query, context_str) auf. # Wir müssen sicherstellen, dass wir das *richtige* Template nutzen. # Da generate_rag_response aktuell KEINEN 'template_key' Parameter hat, # gehen wir davon aus, dass 'context_str' bereits Instruktionen enthält ODER # dass chat.py den Prompt komplett baut. # Um die API sauber zu halten: chat.py übergibt jetzt den FERTIGEN Prompt als context_str? # Nein, chat.py baut den Prompt mit replace(). # Wir ändern diese Methode leicht ab, um flexibler zu sein: # Wir erwarten, dass der Aufrufer (chat.py) die volle Kontrolle hat. # Damit es 100% zusammenpasst mit dem chat.py unten: # chat.py baut den finalen Prompt selbst zusammen! # Wir nutzen daher generate_raw_response eigentlich auch für RAG, # ODER wir passen generate_rag_response an, dass es "dumm" ist. # Legacy Support für generate_rag_response: # Wir bauen den Prompt zusammen, falls noch nicht geschehen. # ABER: chat.py in meiner Version unten macht das Template-Handling. # Daher ist der sauberste Weg: chat.py ruft generate_raw_response auf für die Antwort! # FIX für Kompatibilität: Wir leiten rag_response intern auf raw um, # bauen aber vorher den Prompt, falls context_str übergeben wird. # Da wir chat.py kontrollieren (siehe unten), ändern wir chat.py so, # dass es generate_raw_response nutzt! Das ist viel sauberer. # Diese Methode bleibt für Backward Compatibility. system_prompt = self.prompts.get("system_prompt", "") rag_template = self.prompts.get("rag_template", "{context_str}\n\n{query}") final_prompt = rag_template.format(context_str=context_str, query=query) payload = { "model": self.settings.LLM_MODEL, "system": system_prompt, "prompt": final_prompt, "stream": False, "options": { "temperature": 0.7, "num_ctx": 2048 } } try: response = await self.client.post("/api/generate", json=payload) if response.status_code != 200: return f"Error: {response.text}" return response.json().get("response", "") except Exception as e: return f"Error: {str(e)}" async def close(self): await self.client.aclose()