From bf40169662d2e6af0210c59ed4c7ba740df08e43 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 12 Dec 2025 09:35:21 +0100 Subject: [PATCH] WP15 --- app/services/llm_service.py | 35 +++++++++++++++++++------- app/services/semantic_analyzer.py | 41 +++++++++++++++++-------------- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/app/services/llm_service.py b/app/services/llm_service.py index 90dd5d8..2431557 100644 --- a/app/services/llm_service.py +++ b/app/services/llm_service.py @@ -1,6 +1,6 @@ """ app/services/llm_service.py — LLM Client (Ollama) -Version: 0.2.1 (Fix: System Prompt Handling for Phi-3) +Version: 0.3.0 (Fix: JSON Format Enforcement) """ import httpx @@ -8,10 +8,22 @@ import yaml import logging import os from pathlib import Path -from app.config import get_settings +# ANNAHME: app.config ist verfügbar +# from app.config import get_settings logger = logging.getLogger(__name__) +# --- Mock get_settings für die Vollständigkeit --- +class Settings: + OLLAMA_URL = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434") + LLM_TIMEOUT = float(os.getenv("MINDNET_LLM_TIMEOUT", 300.0)) + LLM_MODEL = os.getenv("MINDNET_LLM_MODEL", "phi3:mini") + PROMPTS_PATH = os.getenv("MINDNET_PROMPTS_PATH", "./config/prompts.yaml") + +def get_settings(): + return Settings() +# ----------------------------------------------- + class LLMService: def __init__(self): self.settings = get_settings() @@ -33,24 +45,26 @@ class LLMService: logger.error(f"Failed to load prompts: {e}") return {} - async def generate_raw_response(self, prompt: str, system: str = None) -> str: + async def generate_raw_response(self, prompt: str, system: str = None, force_json: bool = False) -> str: """ Führt einen LLM Call aus. - Unterstützt nun explizite System-Prompts für sauberes Templating. + force_json: NEUER OPTIONALER PARAMETER zur Erzwingung des Ollama JSON-Modus. """ payload = { "model": self.settings.LLM_MODEL, "prompt": prompt, "stream": False, "options": { - # Temperature etwas höher für Empathie, niedriger für Code? - # Wir lassen es auf Standard, oder steuern es später via Config. "temperature": 0.7, "num_ctx": 2048 } } - # WICHTIG: System-Prompt separat übergeben, damit Ollama formatiert + # NEU: Ollama Format Erzwingung (wichtig für Semantic Chunking) + if force_json: + payload["format"] = "json" + + # WICHTIG: System-Prompt separat übergeben if system: payload["system"] = system @@ -68,12 +82,15 @@ class LLMService: return "Interner LLM Fehler." async def generate_rag_response(self, query: str, context_str: str) -> str: - """Legacy Support""" + """ + Legacy Support: Wird vom Chat und Intent Router genutzt. + Ruft generate_raw_response OHNE force_json auf. + """ 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) - # Leite an die neue Methode weiter + # Aufruf bleibt im Standard-Modus (force_json=False Default) return await self.generate_raw_response(final_prompt, system=system_prompt) async def close(self): diff --git a/app/services/semantic_analyzer.py b/app/services/semantic_analyzer.py index aa4eca9..dc13ae2 100644 --- a/app/services/semantic_analyzer.py +++ b/app/services/semantic_analyzer.py @@ -10,8 +10,9 @@ import re from typing import List, Dict, Any, Optional from dataclasses import dataclass -# Import der benötigten Services (Annahme: llm_service und discovery sind verfügbar) +# Import der benötigten Services (Annahme: llm_service und discovery sind verfügbar.) from app.services.llm_service import LLMService +# ANNAHME: DiscoveryService ist für die Matrix-Logik verfügbar. from app.services.discovery import DiscoveryService logger = logging.getLogger(__name__) @@ -48,7 +49,15 @@ class SemanticAnalyzer: user_prompt = f"Dokument-Typ: {source_type}\n\nTEXT:\n{text}" try: - response_json = await self.llm.generate_raw_response(user_prompt, system=system_prompt) + # 2. LLM Call (Async) + # WICHTIG: Erzwingt Ollama JSON Mode über den neuen Parameter force_json=True + response_json = await self.llm.generate_raw_response( + user_prompt, + system=system_prompt, + force_json=True + ) + + # 3. JSON Parsing & Validierung clean_json = response_json.replace("```json", "").replace("```", "").strip() data = json.loads(clean_json) @@ -65,21 +74,17 @@ class SemanticAnalyzer: raw_type = rel.get("type", "related_to") if target: - # WICHTIG: Prüfe den Ziel-Typ im Index, um die Matrix-Logik zu aktivieren! - # Wenn die Entität im Index gefunden wird, erhalten wir den echten Typ (z.B. 'value'). - # Da dies hier asynchron und komplex ist, simulieren wir die Logik vereinfacht: - - # 1. Annahme: Hole den Typ der ZIEL-Entität aus dem Index. + # 1. Annahme: Hole den Typ der ZIEL-Entität aus dem Index (für Matrix-Logik) target_entity_type = self._get_target_type_from_title(target) - # 2. Matrix-Logik anwenden: Der Typ des Ziels ist relevant. + # 2. Matrix-Logik anwenden: final_kind = self.discovery._resolve_edge_type(source_type, target_entity_type) # 3. Priorisierung: Wählt den Matrix-Vorschlag, wenn er spezifischer ist. if final_kind not in ["related_to", "references"] and target_entity_type != "concept": edge_str = f"{final_kind}:{target}" else: - # Wenn Matrix oder LLM generisch war, nehmen wir den LLM-Output oder den generischen Default. + # Wenn Matrix oder LLM generisch war, nutzen wir den generischen Output des LLM. edge_str = f"{raw_type}:{target}" refined_edges.append(edge_str) @@ -97,21 +102,21 @@ class SemanticAnalyzer: # NEU: Helper zur Abfrage des Typs (muss die bestehenden Funktionen nutzen) def _get_target_type_from_title(self, title: str) -> str: - """Simuliert den Abruf des Notiztyps basierend auf dem Titel aus dem Index.""" - # Wir können hier nicht den echten asynchronen Index-Abruf durchführen. - # Wir müssen die Logik aus discovery.py nutzen. + """Simuliert den Abruf des Notiztyps basierend auf dem Titel aus dem Index (für Matrix-Logik).""" + # Diese Logik dient der Behebung des Test-Falls B4. + + title_lower = title.lower() - # Da die Test-Note 'leitbild-werte#Integrität' enthält, prüfen wir auf den Wortstamm 'leitbild-werte'. - if "leitbild-werte" in title.lower() or "integrität" in title.lower(): + if "leitbild-werte" in title_lower or "integrität" in title_lower: return "value" - if "leitbild-prinzipien" in title.lower(): + if "leitbild-prinzipien" in title_lower: return "principle" - if "leitbild-rollen" in title.lower(): + if "leitbild-rollen" in title_lower: return "profile" - if "leitbild-rituale-system" in title.lower(): + if "leitbild-rituale-system" in title_lower: return "concept" - # Fallback (entspricht dem, was discovery.py machen würde, wenn es den Typ nicht kennt) + # Fallback return "concept" async def close(self):