""" FILE: app/core/ingestion/ingestion_validation.py DESCRIPTION: WP-15b semantische Validierung von Kanten gegen den LocalBatchCache. WP-25b: Umstellung auf Lazy-Prompt-Orchestration (prompt_key + variables). VERSION: 2.14.0 (WP-25b: Lazy Prompt Integration) STATUS: Active FIX: - WP-25b: Entfernung manueller Prompt-Formatierung zur Unterstützung modell-spezifischer Prompts. - WP-25b: Umstellung auf generate_raw_response mit prompt_key="edge_validation". - WP-25a: Voller Erhalt der MoE-Profilsteuerung und Fallback-Kaskade via LLMService. """ import logging from typing import Dict, Any, Optional from app.core.parser import NoteContext # ENTSCHEIDENDER FIX: Import der neutralen Bereinigungs-Logik zur Vermeidung von Circular Imports from app.core.registry import clean_llm_text logger = logging.getLogger(__name__) async def validate_edge_candidate( chunk_text: str, edge: Dict, batch_cache: Dict[str, NoteContext], llm_service: Any, provider: Optional[str] = None, profile_name: str = "ingest_validator" ) -> bool: """ WP-15b/25b: Validiert einen Kandidaten semantisch gegen das Ziel im Cache. Nutzt Lazy-Prompt-Loading zur Unterstützung modell-spezifischer Validierungs-Templates. """ target_id = edge.get("to") target_ctx = batch_cache.get(target_id) # Robust Lookup Fix (v2.12.2): Support für Anker if not target_ctx and "#" in target_id: base_id = target_id.split("#")[0] target_ctx = batch_cache.get(base_id) # Sicherheits-Fallback (Hard-Link Integrity) # Explizite Wikilinks oder Callouts werden nicht durch das LLM verifiziert. if not target_ctx: logger.info(f"ℹ️ [VALIDATION SKIP] No context for '{target_id}' - allowing link.") return True try: logger.info(f"⚖️ [VALIDATING] Relation '{edge.get('kind')}' -> '{target_id}' (Profile: {profile_name})...") # WP-25b: Lazy-Prompt Aufruf. # Wir übergeben keine formatierte Nachricht mehr, sondern Key und Daten-Dict. # Das manuelle 'template = llm_service.get_prompt(...)' entfällt hier. raw_response = await llm_service.generate_raw_response( prompt_key="edge_validation", variables={ "chunk_text": chunk_text[:1500], "target_title": target_ctx.title, "target_summary": target_ctx.summary, "edge_kind": edge.get("kind", "related_to") }, priority="background", profile_name=profile_name ) # WP-14 Fix: Bereinigung zur Sicherstellung der Interpretierbarkeit response = clean_llm_text(raw_response) # Semantische Prüfung des Ergebnisses is_valid = "YES" in response.upper() if is_valid: logger.info(f"✅ [VALIDATED] Relation to '{target_id}' confirmed.") else: logger.info(f"🚫 [REJECTED] Relation to '{target_id}' irrelevant for this chunk.") return is_valid except Exception as e: error_str = str(e).lower() error_type = type(e).__name__ # WP-25b FIX: Differenzierung zwischen transienten und permanenten Fehlern # Transiente Fehler (Timeout, Network) → erlauben (Datenverlust vermeiden) if any(x in error_str for x in ["timeout", "connection", "network", "unreachable", "refused"]): logger.warning(f"⚠️ Transient error for {target_id} using {profile_name}: {error_type} - {e}. Allowing edge.") return True # Permanente Fehler (Config, Validation, Invalid Response) → ablehnen (Graph-Qualität) logger.error(f"❌ Permanent validation error for {target_id} using {profile_name}: {error_type} - {e}") return False