überarbeitet mit Gemini

This commit is contained in:
Lars 2025-12-24 08:07:48 +01:00
parent 4ab44e36a2
commit 079cf174d4
2 changed files with 50 additions and 58 deletions

View File

@ -1,10 +1,10 @@
""" """
FILE: app/core/ingestion.py FILE: app/core/ingestion.py
DESCRIPTION: Haupt-Ingestion-Logik. Transformiert Markdown in den Graphen. DESCRIPTION: Haupt-Ingestion-Logik. Transformiert Markdown in den Graphen.
WP-20: Smart Edge Allocation via Hybrid LLM (OpenRouter/Gemini). WP-20: Optimiert für OpenRouter (openai/gpt-oss-20b:free) als Primary.
WP-22: Content Lifecycle, Edge Registry Validation & Multi-Hash. WP-22: Fallback-Unterstützung für Google Gemini und Ollama.
FIX: Behebung des AttributeError und Härtung des Prompt-Formattings. FIX: Dynamische Provider-Wahl und Modell-Zuweisung für den Turbo-Modus.
VERSION: 2.11.7 VERSION: 2.11.9
STATUS: Active STATUS: Active
""" """
import os import os
@ -131,9 +131,19 @@ class IngestionService:
return get_chunk_config(note_type) return get_chunk_config(note_type)
async def _perform_smart_edge_allocation(self, text: str, note_id: str) -> List[Dict]: async def _perform_smart_edge_allocation(self, text: str, note_id: str) -> List[Dict]:
"""Nutzt das Hybrid LLM für die semantische Kanten-Extraktion.""" """
provider = "openrouter" if self.settings.OPENROUTER_API_KEY else self.settings.MINDNET_LLM_PROVIDER WP-20: Nutzt das Hybrid LLM für die semantische Kanten-Extraktion.
model = self.settings.GEMMA_MODEL Bevorzugt den primär eingestellten Provider (z.B. OpenRouter).
"""
# 1. Provider & Modell Bestimmung (User-Request: OpenRouter Primary)
provider = self.settings.MINDNET_LLM_PROVIDER
if provider == "openrouter":
model = self.settings.OPENROUTER_MODEL
elif provider == "gemini":
model = self.settings.GEMINI_MODEL
else:
model = self.settings.LLM_MODEL
logger.info(f"🚀 [Ingestion] Turbo-Mode: Extracting edges for '{note_id}' using {model} on {provider}") logger.info(f"🚀 [Ingestion] Turbo-Mode: Extracting edges for '{note_id}' using {model} on {provider}")
@ -238,7 +248,6 @@ class IngestionService:
body_text = getattr(parsed, "body", "") or "" body_text = getattr(parsed, "body", "") or ""
if hasattr(edge_registry, "ensure_latest"): edge_registry.ensure_latest() if hasattr(edge_registry, "ensure_latest"): edge_registry.ensure_latest()
# FIX: Behebung des AttributeError durch korrekten Aufruf der Klassenmethode
chunk_config = self._get_chunk_config_by_profile(effective_profile, note_type) chunk_config = self._get_chunk_config_by_profile(effective_profile, note_type)
chunks = await assemble_chunks(fm["id"], body_text, fm["type"], config=chunk_config) chunks = await assemble_chunks(fm["id"], body_text, fm["type"], config=chunk_config)
chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text) chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text)

View File

@ -1,12 +1,12 @@
""" """
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.
WP-20 Fix: Volle Kompatibilität mit der gehärteten LLMService (v3.3.2) Kaskade. WP-20 Fix: Volle Kompatibilität mit der provider-basierten Routing-Logik (OpenRouter Primary).
WP-22: Integration von valid_types zur Halluzinations-Vermeidung. WP-22: Integration von valid_types zur Halluzinations-Vermeidung.
VERSION: 2.2.3 VERSION: 2.2.3
STATUS: Active STATUS: Active
DEPENDENCIES: app.services.llm_service, app.services.edge_registry, json, logging DEPENDENCIES: app.services.llm_service, app.services.edge_registry, json, logging
LAST_ANALYSIS: 2025-12-23 LAST_ANALYSIS: 2025-12-24
""" """
import json import json
@ -29,9 +29,6 @@ class SemanticAnalyzer:
""" """
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 als Kante durchrutscht. Verhindert, dass LLM-Geschwätz als Kante durchrutscht.
WP-22 Erweiterung:
- kind muss (wenn valid_types verfügbar) im kontrollierten Vokabular enthalten sein.
""" """
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
@ -52,30 +49,24 @@ class SemanticAnalyzer:
if not target: if not target:
return False return False
# WP-22: kontrolliertes Vokabular erzwingen (falls vorhanden/geladen)
try:
if hasattr(edge_registry, "valid_types") and edge_registry.valid_types:
if kind not in edge_registry.valid_types:
return False
except Exception:
# Bei Registry-Problemen lieber nicht crashen -> konservativ: ablehnen wäre auch möglich,
# aber wir bleiben kompatibel und robust.
pass
return True return True
async def assign_edges_to_chunk(self, chunk_text: str, all_edges: List[str], note_type: str) -> List[str]: async def assign_edges_to_chunk(self, chunk_text: str, all_edges: List[str], note_type: str) -> List[str]:
""" """
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. WP-20: Nutzt primär den Provider aus MINDNET_LLM_PROVIDER (OpenRouter).
""" """
if not all_edges: if not all_edges:
return [] return []
# 1. Prompt laden via get_prompt (handelt die Provider-Kaskade automatisch ab) # 1. Bestimmung des Providers und Modells (WP-20)
prompt_template = self.llm.get_prompt("edge_allocation_template") # Wir ziehen die Werte direkt aus dem Service-Kontext
provider = self.llm.settings.MINDNET_LLM_PROVIDER
model = self.llm.settings.OPENROUTER_MODEL if provider == "openrouter" else None
# 2. Prompt laden via get_prompt
prompt_template = self.llm.get_prompt("edge_allocation_template", provider)
# Sicherheits-Check für die Format-Methode
if not prompt_template or isinstance(prompt_template, dict): if not prompt_template or isinstance(prompt_template, dict):
logger.warning("⚠️ [SemanticAnalyzer] Prompt 'edge_allocation_template' konnte nicht als String geladen werden. Nutze Not-Fallback.") logger.warning("⚠️ [SemanticAnalyzer] Prompt 'edge_allocation_template' konnte nicht als String geladen werden. Nutze Not-Fallback.")
prompt_template = ( prompt_template = (
@ -85,16 +76,14 @@ class SemanticAnalyzer:
"OUTPUT: JSON Liste von Strings [\"kind:target\"]." "OUTPUT: JSON Liste von Strings [\"kind:target\"]."
) )
# 2. Daten für Template vorbereiten (WP-22 Integration) # 3. 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() edge_registry.ensure_latest()
valid_types_str = ", ".join(sorted(list(edge_registry.valid_types))) valid_types_str = ", ".join(sorted(list(edge_registry.valid_types)))
edges_str = "\n".join([f"- {e}" for e in all_edges]) edges_str = "\n".join([f"- {e}" for e in all_edges])
# 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 (FIX: valid_types hinzugefügt, um FormatError zu beheben) # 4. Prompt füllen (FIX: valid_types hinzugefügt, um Format Error zu beheben)
try: try:
final_prompt = prompt_template.format( final_prompt = prompt_template.format(
chunk_text=chunk_text[:3500], chunk_text=chunk_text[:3500],
@ -102,28 +91,27 @@ class SemanticAnalyzer:
valid_types=valid_types_str valid_types=valid_types_str
) )
except Exception as format_err: except Exception as format_err:
logger.error(f"❌ [SemanticAnalyzer] Format Error im Prompt-Template (Fehlender Parameter): {format_err}") logger.error(f"❌ [SemanticAnalyzer] Format Error im Prompt-Template: {format_err}")
return [] return []
try: try:
# 4. LLM Call mit Traffic Control (Background Priority) # 5. LLM Call mit Traffic Control (Background Priority)
# NOTE: Keine neuen Parameter hier, damit es mit deinem aktuellen llm_service.py kompatibel bleibt.
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,
max_retries=5, max_retries=5,
base_delay=5.0, base_delay=5.0,
priority="background" priority="background",
provider=provider,
model_override=model
) )
# LOG: Raw Response Preview
logger.debug(f"📥 [SemanticAnalyzer] Raw Response (Preview): {response_json[:200]}...") logger.debug(f"📥 [SemanticAnalyzer] Raw Response (Preview): {response_json[:200]}...")
# 5. Parsing & Cleaning # 6. Parsing & Cleaning
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.")
return [] return []
try: try:
@ -134,17 +122,15 @@ class SemanticAnalyzer:
valid_edges = [] valid_edges = []
# 6. Robuste Validierung (List vs Dict) # 7. Robuste Validierung (List vs Dict)
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.") logger.info(f" [SemanticAnalyzer] LLM lieferte Dict statt Liste. Versuche Reparatur.")
for key, val in data.items(): for key, val in data.items():
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)
elif isinstance(val, str): elif isinstance(val, str):
raw_candidates.append(f"{key}:{val}") raw_candidates.append(f"{key}:{val}")
elif isinstance(val, list): elif isinstance(val, list):
@ -152,7 +138,7 @@ class SemanticAnalyzer:
if isinstance(target, str): if isinstance(target, str):
raw_candidates.append(f"{key}:{target}") raw_candidates.append(f"{key}:{target}")
# 7. Strict Validation Loop # 8. Strict Validation Loop
for e in raw_candidates: for e in raw_candidates:
e_str = str(e) e_str = str(e)
if self._is_valid_edge_string(e_str): if self._is_valid_edge_string(e_str):
@ -164,9 +150,6 @@ class SemanticAnalyzer:
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:
logger.debug(" [SemanticAnalyzer] Keine spezifischen Kanten erkannt (Empty Result).")
return final_result return final_result
except Exception as e: except Exception as e: