semantic semantic_analyzer angepasst

This commit is contained in:
Lars 2025-12-23 18:17:34 +01:00
parent dcc3083455
commit f1bfa40b5b

View File

@ -1,10 +1,11 @@
""" """
FILE: app/services/semantic_analyzer.py FILE: app/services/semantic_analyzer.py
DESCRIPTION: KI-gestützte Kanten-Validierung. Nutzt LLM (Background-Priority), um Kanten präzise einem Chunk zuzuordnen. DESCRIPTION: KI-gestützte Kanten-Validierung. Nutzt LLM (Background-Priority), um Kanten präzise einem Chunk zuzuordnen.
VERSION: 2.1.0 (Fix: Strict Edge String Validation against LLM Hallucinations) WP-20 Fix: Kompatibilität mit Provider-basierten Prompt-Dictionaries (Hybrid-Modus).
VERSION: 2.2.0
STATUS: Active STATUS: Active
DEPENDENCIES: app.services.llm_service, json, logging DEPENDENCIES: app.services.llm_service, json, logging
LAST_ANALYSIS: 2025-12-16 LAST_ANALYSIS: 2025-12-23
""" """
import json import json
@ -24,7 +25,7 @@ class SemanticAnalyzer:
def _is_valid_edge_string(self, edge_str: str) -> bool: def _is_valid_edge_string(self, edge_str: str) -> bool:
""" """
Prüft, ob ein String eine valide Kante im Format 'kind:target' ist. Prüft, ob ein String eine valide Kante im Format 'kind:target' ist.
Verhindert, dass LLM-Geschwätz ("Here is the list: ...") als Kante durchrutscht. Verhindert, dass LLM-Geschwätz als Kante durchrutscht.
""" """
if not isinstance(edge_str, str) or ":" not in edge_str: if not isinstance(edge_str, str) or ":" not in edge_str:
return False return False
@ -34,8 +35,6 @@ class SemanticAnalyzer:
target = parts[1].strip() target = parts[1].strip()
# Regel 1: Ein 'kind' (Beziehungstyp) darf keine Leerzeichen enthalten. # Regel 1: Ein 'kind' (Beziehungstyp) darf keine Leerzeichen enthalten.
# Erlaubt: "derived_from", "related_to"
# Verboten: "derived end of instruction", "Here is the list"
if " " in kind: if " " in kind:
return False return False
@ -54,19 +53,16 @@ class SemanticAnalyzer:
Sendet einen Chunk und eine Liste potenzieller Kanten an das LLM. Sendet einen Chunk und eine Liste potenzieller Kanten an das LLM.
Das LLM filtert heraus, welche Kanten für diesen Chunk relevant sind. Das LLM filtert heraus, welche Kanten für diesen Chunk relevant sind.
Features: WP-20 Fix: Nutzt get_prompt(), um den 'AttributeError: dict object' zu vermeiden.
- Retry Strategy: Wartet bei Überlastung (max_retries=5).
- Priority Queue: Läuft als "background" Task, um den Chat nicht zu blockieren.
- Observability: Loggt Input-Größe, Raw-Response und Parsing-Details.
""" """
if not all_edges: if not all_edges:
return [] return []
# 1. Prompt laden # 1. Prompt laden via get_prompt (handelt die Provider-Kaskade automatisch ab) [WP-20 Fix]
prompt_template = self.llm.prompts.get("edge_allocation_template") prompt_template = self.llm.get_prompt("edge_allocation_template")
if not prompt_template: if not prompt_template or isinstance(prompt_template, dict):
logger.warning("⚠️ [SemanticAnalyzer] Prompt 'edge_allocation_template' fehlt. Nutze Fallback.") logger.warning("⚠️ [SemanticAnalyzer] Prompt 'edge_allocation_template' konnte nicht als String geladen werden. Nutze Hard-Fallback.")
prompt_template = ( prompt_template = (
"TASK: Wähle aus den Kandidaten die relevanten Kanten für den Text.\n" "TASK: Wähle aus den Kandidaten die relevanten Kanten für den Text.\n"
"TEXT: {chunk_text}\n" "TEXT: {chunk_text}\n"
@ -80,14 +76,18 @@ class SemanticAnalyzer:
# LOG: Request Info # LOG: Request Info
logger.debug(f"🔍 [SemanticAnalyzer] Request: {len(chunk_text)} chars Text, {len(all_edges)} Candidates.") logger.debug(f"🔍 [SemanticAnalyzer] Request: {len(chunk_text)} chars Text, {len(all_edges)} Candidates.")
# 3. Prompt füllen # 3. Prompt füllen (Hier trat der AttributeError auf, wenn prompt_template ein dict war)
final_prompt = prompt_template.format( try:
chunk_text=chunk_text[:3500], final_prompt = prompt_template.format(
edge_list=edges_str chunk_text=chunk_text[:3500],
) edge_list=edges_str
)
except Exception as format_err:
logger.error(f"❌ [SemanticAnalyzer] Format Error im Prompt-Template: {format_err}")
return []
try: try:
# 4. LLM Call mit Traffic Control # 4. LLM Call mit Traffic Control (Background Priority)
response_json = await self.llm.generate_raw_response( response_json = await self.llm.generate_raw_response(
prompt=final_prompt, prompt=final_prompt,
force_json=True, force_json=True,
@ -103,39 +103,30 @@ class SemanticAnalyzer:
clean_json = response_json.replace("```json", "").replace("```", "").strip() clean_json = response_json.replace("```json", "").replace("```", "").strip()
if not clean_json: if not clean_json:
logger.warning("⚠️ [SemanticAnalyzer] Leere Antwort vom LLM erhalten. Trigger Fallback.") logger.warning("⚠️ [SemanticAnalyzer] Leere Antwort vom LLM erhalten.")
return [] return []
try: try:
data = json.loads(clean_json) data = json.loads(clean_json)
except json.JSONDecodeError as json_err: except json.JSONDecodeError as json_err:
logger.error(f"❌ [SemanticAnalyzer] JSON Decode Error.") logger.error(f"❌ [SemanticAnalyzer] JSON Decode Error: {json_err}")
logger.error(f" Grund: {json_err}")
logger.error(f" Empfangener String: {clean_json[:500]}")
logger.info(" -> Workaround: Fallback auf 'Alle Kanten' (durch Chunker).")
return [] return []
valid_edges = [] valid_edges = []
# 6. Robuste Validierung (List vs Dict) # 6. Robuste Validierung (List vs Dict)
# Wir sammeln erst alle Strings ein
raw_candidates = [] raw_candidates = []
if isinstance(data, list): if isinstance(data, list):
raw_candidates = data raw_candidates = data
elif isinstance(data, dict): elif isinstance(data, dict):
logger.info(f" [SemanticAnalyzer] LLM lieferte Dict statt Liste. Versuche Reparatur. Keys: {list(data.keys())}") logger.info(f" [SemanticAnalyzer] LLM lieferte Dict statt Liste. Versuche Reparatur.")
for key, val in data.items(): for key, val in data.items():
# Fall A: {"edges": ["kind:target"]}
if key.lower() in ["edges", "results", "kanten", "matches"] and isinstance(val, list): if key.lower() in ["edges", "results", "kanten", "matches"] and isinstance(val, list):
raw_candidates.extend(val) raw_candidates.extend(val)
# Fall B: {"kind": "target"} (Beziehung als Key)
elif isinstance(val, str): elif isinstance(val, str):
raw_candidates.append(f"{key}:{val}") raw_candidates.append(f"{key}:{val}")
# Fall C: {"kind": ["target1", "target2"]}
elif isinstance(val, list): elif isinstance(val, list):
for target in val: for target in val:
if isinstance(target, str): if isinstance(target, str):
@ -149,10 +140,8 @@ class SemanticAnalyzer:
else: else:
logger.debug(f" [SemanticAnalyzer] Invalid edge format rejected: '{e_str}'") logger.debug(f" [SemanticAnalyzer] Invalid edge format rejected: '{e_str}'")
# Safety: Filtere nur Kanten, die halbwegs valide aussehen (Doppelcheck)
final_result = [e for e in valid_edges if ":" in e] final_result = [e for e in valid_edges if ":" in e]
# LOG: Ergebnis
if final_result: if final_result:
logger.info(f"✅ [SemanticAnalyzer] Success. {len(final_result)} Kanten zugewiesen.") logger.info(f"✅ [SemanticAnalyzer] Success. {len(final_result)} Kanten zugewiesen.")
else: else: