mindnet/app/services/semantic_analyzer.py

172 lines
6.6 KiB
Python
Raw Normal View History

2025-12-11 23:21:24 +01:00
"""
2025-12-15 15:40:39 +01:00
FILE: app/services/semantic_analyzer.py
DESCRIPTION: KI-gestützte Kanten-Validierung. Nutzt LLM (Background-Priority), um Kanten präzise einem Chunk zuzuordnen.
2025-12-23 18:51:12 +01:00
WP-20 Fix: Volle Kompatibilität mit der gehärteten LLMService (v3.3.2) Kaskade.
2025-12-23 21:57:50 +01:00
WP-22: Integration von valid_types zur Halluzinations-Vermeidung.
VERSION: 2.2.2
2025-12-15 15:40:39 +01:00
STATUS: Active
2025-12-23 21:57:50 +01:00
DEPENDENCIES: app.services.llm_service, app.services.edge_registry, json, logging
2025-12-23 18:17:34 +01:00
LAST_ANALYSIS: 2025-12-23
2025-12-11 23:21:24 +01:00
"""
2025-12-12 10:25:01 +01:00
2025-12-11 23:21:24 +01:00
import json
import logging
2025-12-12 13:53:54 +01:00
from typing import List, Optional
2025-12-12 10:25:01 +01:00
from dataclasses import dataclass
2025-12-11 23:21:24 +01:00
2025-12-12 11:40:38 +01:00
# Importe
2025-12-11 23:21:24 +01:00
from app.services.llm_service import LLMService
2025-12-23 21:57:50 +01:00
# WP-22: Registry für Vokabular-Erzwingung
from app.services.edge_registry import registry as edge_registry
2025-12-11 23:21:24 +01:00
logger = logging.getLogger(__name__)
class SemanticAnalyzer:
def __init__(self):
self.llm = LLMService()
def _is_valid_edge_string(self, edge_str: str) -> bool:
"""
Prüft, ob ein String eine valide Kante im Format 'kind:target' ist.
2025-12-23 18:17:34 +01:00
Verhindert, dass LLM-Geschwätz als Kante durchrutscht.
"""
if not isinstance(edge_str, str) or ":" not in edge_str:
return False
parts = edge_str.split(":", 1)
kind = parts[0].strip()
target = parts[1].strip()
# Regel 1: Ein 'kind' (Beziehungstyp) darf keine Leerzeichen enthalten.
if " " in kind:
return False
# Regel 2: Plausible Länge für den Typ
if len(kind) > 40 or len(kind) < 2:
return False
# Regel 3: Target darf nicht leer sein
if not target:
return False
return True
2025-12-12 11:40:38 +01:00
async def assign_edges_to_chunk(self, chunk_text: str, all_edges: List[str], note_type: str) -> List[str]:
2025-12-11 23:21:24 +01:00
"""
2025-12-12 11:40:38 +01:00
Sendet einen Chunk und eine Liste potenzieller Kanten an das LLM.
Das LLM filtert heraus, welche Kanten für diesen Chunk relevant sind.
2025-12-11 23:21:24 +01:00
"""
2025-12-12 11:40:38 +01:00
if not all_edges:
return []
2025-12-12 10:39:40 +01:00
2025-12-23 18:51:12 +01:00
# 1. Prompt laden via get_prompt (handelt die Provider-Kaskade automatisch ab)
2025-12-23 18:17:34 +01:00
prompt_template = self.llm.get_prompt("edge_allocation_template")
2025-12-23 18:51:12 +01:00
# Sicherheits-Check für die Format-Methode
2025-12-23 18:17:34 +01:00
if not prompt_template or isinstance(prompt_template, dict):
2025-12-23 18:51:12 +01:00
logger.warning("⚠️ [SemanticAnalyzer] Prompt 'edge_allocation_template' konnte nicht als String geladen werden. Nutze Not-Fallback.")
prompt_template = (
"TASK: Wähle aus den Kandidaten die relevanten Kanten für den Text.\n"
"TEXT: {chunk_text}\n"
"KANDIDATEN: {edge_list}\n"
"OUTPUT: JSON Liste von Strings [\"kind:target\"]."
)
2025-12-12 11:40:38 +01:00
2025-12-23 21:57:50 +01:00
# 2. Daten für Template vorbereiten (WP-22 Integration)
# Wir laden die validen Typen, um sie dem LLM als Leitplanken zu geben
edge_registry.ensure_latest()
valid_types_str = ", ".join(sorted(list(edge_registry.valid_types)))
2025-12-12 11:40:38 +01:00
edges_str = "\n".join([f"- {e}" for e in all_edges])
2025-12-12 13:26:31 +01:00
# LOG: Request Info
2025-12-12 13:26:31 +01:00
logger.debug(f"🔍 [SemanticAnalyzer] Request: {len(chunk_text)} chars Text, {len(all_edges)} Candidates.")
2025-12-12 11:40:38 +01:00
2025-12-23 21:57:50 +01:00
# 3. Prompt füllen (FIX: valid_types hinzugefügt, um FormatError zu beheben)
2025-12-23 18:17:34 +01:00
try:
final_prompt = prompt_template.format(
chunk_text=chunk_text[:3500],
2025-12-23 21:57:50 +01:00
edge_list=edges_str,
valid_types=valid_types_str
2025-12-23 18:17:34 +01:00
)
except Exception as format_err:
2025-12-23 21:57:50 +01:00
logger.error(f"❌ [SemanticAnalyzer] Format Error im Prompt-Template (Fehlender Parameter): {format_err}")
2025-12-23 18:17:34 +01:00
return []
2025-12-11 23:21:24 +01:00
try:
2025-12-23 18:17:34 +01:00
# 4. LLM Call mit Traffic Control (Background Priority)
2025-12-12 09:35:21 +01:00
response_json = await self.llm.generate_raw_response(
2025-12-12 11:40:38 +01:00
prompt=final_prompt,
2025-12-12 13:53:54 +01:00
force_json=True,
max_retries=5,
base_delay=5.0,
priority="background"
2025-12-12 09:35:21 +01:00
)
2025-12-12 10:25:01 +01:00
# LOG: Raw Response Preview
2025-12-12 13:26:31 +01:00
logger.debug(f"📥 [SemanticAnalyzer] Raw Response (Preview): {response_json[:200]}...")
# 5. Parsing & Cleaning
2025-12-12 11:40:38 +01:00
clean_json = response_json.replace("```json", "").replace("```", "").strip()
2025-12-12 13:26:31 +01:00
if not clean_json:
2025-12-23 18:17:34 +01:00
logger.warning("⚠️ [SemanticAnalyzer] Leere Antwort vom LLM erhalten.")
2025-12-12 13:26:31 +01:00
return []
try:
data = json.loads(clean_json)
except json.JSONDecodeError as json_err:
2025-12-23 18:17:34 +01:00
logger.error(f"❌ [SemanticAnalyzer] JSON Decode Error: {json_err}")
2025-12-12 13:26:31 +01:00
return []
2025-12-11 23:21:24 +01:00
valid_edges = []
2025-12-12 11:40:38 +01:00
# 6. Robuste Validierung (List vs Dict)
raw_candidates = []
2025-12-12 11:40:38 +01:00
if isinstance(data, list):
raw_candidates = data
2025-12-12 11:40:38 +01:00
elif isinstance(data, dict):
2025-12-23 18:17:34 +01:00
logger.info(f"ℹ️ [SemanticAnalyzer] LLM lieferte Dict statt Liste. Versuche Reparatur.")
2025-12-12 11:40:38 +01:00
for key, val in data.items():
2025-12-12 13:26:31 +01:00
if key.lower() in ["edges", "results", "kanten", "matches"] and isinstance(val, list):
raw_candidates.extend(val)
elif isinstance(val, str):
raw_candidates.append(f"{key}:{val}")
elif isinstance(val, list):
for target in val:
if isinstance(target, str):
raw_candidates.append(f"{key}:{target}")
2025-12-23 21:57:50 +01:00
# 7. Strict Validation Loop
for e in raw_candidates:
e_str = str(e)
if self._is_valid_edge_string(e_str):
valid_edges.append(e_str)
else:
logger.debug(f" [SemanticAnalyzer] Invalid edge format rejected: '{e_str}'")
2025-12-12 13:26:31 +01:00
final_result = [e for e in valid_edges if ":" in e]
if final_result:
logger.info(f"✅ [SemanticAnalyzer] Success. {len(final_result)} Kanten zugewiesen.")
else:
logger.debug(" [SemanticAnalyzer] Keine spezifischen Kanten erkannt (Empty Result).")
return final_result
2025-12-12 11:40:38 +01:00
2025-12-11 23:21:24 +01:00
except Exception as e:
2025-12-12 13:26:31 +01:00
logger.error(f"💥 [SemanticAnalyzer] Kritischer Fehler: {e}", exc_info=True)
2025-12-12 11:40:38 +01:00
return []
2025-12-11 23:21:24 +01:00
async def close(self):
if self.llm:
2025-12-12 10:25:01 +01:00
await self.llm.close()
2025-12-12 11:40:38 +01:00
# Singleton Helper
2025-12-12 10:25:01 +01:00
_analyzer_instance = None
def get_semantic_analyzer():
global _analyzer_instance
if _analyzer_instance is None:
_analyzer_instance = SemanticAnalyzer()
return _analyzer_instance