bug fix Wp20

This commit is contained in:
Lars 2025-12-23 21:44:49 +01:00
parent 2c073c7d3c
commit 867a7a8b44
2 changed files with 86 additions and 59 deletions

View File

@ -5,7 +5,8 @@ DESCRIPTION: Haupt-Ingestion-Logik. Transformiert Markdown in den Graphen (Notes
WP-22: Integration von Content Lifecycle (Status Gate) und Edge Registry Validation. WP-22: Integration von Content Lifecycle (Status Gate) und Edge Registry Validation.
WP-22: Kontextsensitive Kanten-Validierung mit Fundort-Reporting (Zeilennummern). WP-22: Kontextsensitive Kanten-Validierung mit Fundort-Reporting (Zeilennummern).
WP-22: Multi-Hash Refresh für konsistente Change Detection. WP-22: Multi-Hash Refresh für konsistente Change Detection.
VERSION: 2.11.4 FIX: Robuste Verarbeitung von LLM-Antworten (Dict vs String) zur Vermeidung von Item-Assignment-Errors.
VERSION: 2.11.5
STATUS: Active STATUS: Active
DEPENDENCIES: app.core.parser, app.core.note_payload, app.core.chunker, app.services.llm_service, app.services.edge_registry DEPENDENCIES: app.core.parser, app.core.note_payload, app.core.chunker, app.services.llm_service, app.services.edge_registry
EXTERNAL_CONFIG: config/types.yaml, config/prompts.yaml EXTERNAL_CONFIG: config/types.yaml, config/prompts.yaml
@ -96,12 +97,11 @@ class IngestionService:
self.cfg = QdrantConfig.from_env() self.cfg = QdrantConfig.from_env()
self.cfg.prefix = self.prefix self.cfg.prefix = self.prefix
self.client = get_client(self.cfg) self.client = get_client(self.cfg)
self.dim = self.settings.VECTOR_SIZE # Synchronisiert mit Settings v0.6.2 self.dim = self.settings.VECTOR_SIZE
self.registry = load_type_registry() self.registry = load_type_registry()
self.embedder = EmbeddingsClient() self.embedder = EmbeddingsClient()
self.llm = LLMService() self.llm = LLMService()
# WP-22: Change Detection Modus aus Settings
self.active_hash_mode = self.settings.CHANGE_DETECTION_MODE self.active_hash_mode = self.settings.CHANGE_DETECTION_MODE
try: try:
@ -111,7 +111,7 @@ class IngestionService:
logger.warning(f"DB init warning: {e}") logger.warning(f"DB init warning: {e}")
def _get_chunk_config_by_profile(self, profile_name: str, note_type: str) -> Dict[str, Any]: def _get_chunk_config_by_profile(self, profile_name: str, note_type: str) -> Dict[str, Any]:
"""Holt die Chunker-Parameter (max, target, overlap) für ein spezifisches Profil.""" """Holt die Chunker-Parameter für ein spezifisches Profil."""
profiles = self.registry.get("chunking_profiles", {}) profiles = self.registry.get("chunking_profiles", {})
if profile_name in profiles: if profile_name in profiles:
cfg = profiles[profile_name].copy() cfg = profiles[profile_name].copy()
@ -125,18 +125,25 @@ class IngestionService:
WP-20: Nutzt den Hybrid LLM Service für die semantische Kanten-Extraktion. WP-20: Nutzt den Hybrid LLM Service für die semantische Kanten-Extraktion.
QUOTEN-SCHUTZ: Bevorzugt OpenRouter (Gemma 2), um Gemini-Tageslimits zu schonen. QUOTEN-SCHUTZ: Bevorzugt OpenRouter (Gemma 2), um Gemini-Tageslimits zu schonen.
""" """
# Bestimme Provider: Nutze OpenRouter falls Key vorhanden
provider = "openrouter" if self.settings.OPENROUTER_API_KEY else self.settings.MINDNET_LLM_PROVIDER provider = "openrouter" if self.settings.OPENROUTER_API_KEY else self.settings.MINDNET_LLM_PROVIDER
model = self.settings.GEMMA_MODEL # Hochdurchsatz-Modell aus config.py model = self.settings.GEMMA_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}")
# Hole das optimierte Prompt-Template (Kaskade: Provider -> gemini -> ollama) # WP-22: Hole valide Typen für das Prompt-Template
edge_registry.ensure_latest()
valid_types_str = ", ".join(sorted(list(edge_registry.valid_types)))
template = self.llm.get_prompt("edge_extraction", provider) template = self.llm.get_prompt("edge_extraction", provider)
prompt = template.format(text=text[:6000], note_id=note_id)
try: try:
# Hintergrund-Task mit Semaphore via LLMService (WP-06) # Befülle das Template (v2.5.0 erwartet valid_types)
prompt = template.format(
text=text[:6000],
note_id=note_id,
valid_types=valid_types_str
)
response_json = await self.llm.generate_raw_response( response_json = await self.llm.generate_raw_response(
prompt=prompt, prompt=prompt,
priority="background", priority="background",
@ -144,12 +151,44 @@ class IngestionService:
provider=provider, provider=provider,
model_override=model model_override=model
) )
data = json.loads(response_json)
for item in data: # Robustes Parsing (WP-20 Fix für 'str' object assignment error)
item["provenance"] = "semantic_ai" raw_data = json.loads(response_json)
item["line"] = f"ai-{provider}" processed_edges = []
return data
# Das LLM liefert manchmal ein Dict mit einem Key statt einer Liste
if isinstance(raw_data, dict):
logger.debug(f" [Ingestion] LLM returned dict for {note_id}, attempting recovery.")
for key in ["edges", "links", "results", "kanten"]:
if key in raw_data and isinstance(raw_data[key], list):
raw_data = raw_data[key]
break
if not isinstance(raw_data, list):
logger.warning(f"⚠️ [Ingestion] LLM output for {note_id} is not a list: {type(raw_data)}")
return []
for item in raw_data:
# Fall 1: Element ist bereits ein Dict (Idealfall)
if isinstance(item, dict) and "to" in item:
item["provenance"] = "semantic_ai"
item["line"] = f"ai-{provider}"
processed_edges.append(item)
# Fall 2: Element ist ein String (z.B. "kind:target") -> Umwandlung
elif isinstance(item, str) and ":" in item:
parts = item.split(":", 1)
processed_edges.append({
"to": parts[1].strip(),
"kind": parts[0].strip(),
"provenance": "semantic_ai",
"line": f"ai-{provider}"
})
else:
logger.debug(f"⏩ [Ingestion] Skipping unparseable AI edge: {item}")
return processed_edges
except Exception as e: except Exception as e:
logger.warning(f"⚠️ [Ingestion] Smart Edge Allocation failed for {note_id} on {provider}: {e}") logger.warning(f"⚠️ [Ingestion] Smart Edge Allocation failed for {note_id} on {provider}: {e}")
return [] return []
@ -256,7 +295,9 @@ class IngestionService:
# B. WP-20: Smart AI Edges (Hybrid Turbo Acceleration) # B. WP-20: Smart AI Edges (Hybrid Turbo Acceleration)
ai_edges = await self._perform_smart_edge_allocation(body_text, note_id) ai_edges = await self._perform_smart_edge_allocation(body_text, note_id)
for e in ai_edges: for e in ai_edges:
e["kind"] = edge_registry.resolve(edge_type=e.get("kind"), provenance="semantic_ai", context={**context, "line": e.get("line")}) # Validierung gegen EdgeRegistry (Vermeidet 'Transition' etc.)
valid_kind = edge_registry.resolve(edge_type=e.get("kind"), provenance="semantic_ai", context={**context, "line": e.get("line")})
e["kind"] = valid_kind
edges.append(e) edges.append(e)
# C. System-Kanten (Struktur) # C. System-Kanten (Struktur)

View File

@ -31,12 +31,10 @@ rag_template:
Beantworte die Frage präzise basierend auf den Quellen. Beantworte die Frage präzise basierend auf den Quellen.
Fasse die Informationen zusammen. Sei objektiv und neutral. Fasse die Informationen zusammen. Sei objektiv und neutral.
gemini: | gemini: |
Nutze das Wissen meines digitalen Zwillings aus folgendem Kontext: {context_str} Kontext meines digitalen Zwillings: {context_str}
Beantworte die Anfrage präzise, detailliert und strukturiert: {query} Beantworte strukturiert: {query}
openrouter: | openrouter: |
Kontext-Analyse für Gemma/Llama: Kontext: {context_str}
{context_str}
Anfrage: {query} Anfrage: {query}
# --------------------------------------------------------- # ---------------------------------------------------------
@ -63,13 +61,10 @@ decision_template:
- **Abgleich:** (Gibt es Konflikte mit Werten/Zielen? Nenne die Quelle!) - **Abgleich:** (Gibt es Konflikte mit Werten/Zielen? Nenne die Quelle!)
- **Empfehlung:** (Klare Meinung: Ja/No/Vielleicht mit Begründung) - **Empfehlung:** (Klare Meinung: Ja/No/Vielleicht mit Begründung)
gemini: | gemini: |
Agiere als Senior Strategy Consultant für meinen digitalen Zwilling. Agiere als strategischer Partner. Analysiere {query} basierend auf {context_str}.
Wäge die Frage {query} multiperspektivisch gegen meine Werte und langfristigen Ziele ab.
Kontext: {context_str}
openrouter: | openrouter: |
Strategischer Check via OpenRouter/Gemma: Entscheidungsanalyse für: {query}
Analyse der Entscheidungsfrage: {query} Datenbasis: {context_str}
Referenzdaten aus dem Graph: {context_str}
# --------------------------------------------------------- # ---------------------------------------------------------
# 3. EMPATHY: Der Spiegel / "Ich"-Modus (Intent: EMPATHY) # 3. EMPATHY: Der Spiegel / "Ich"-Modus (Intent: EMPATHY)
@ -92,13 +87,8 @@ empathy_template:
TONFALL: TONFALL:
Ruhig, verständnisvoll, reflektiert. Keine Aufzählungszeichen, sondern fließender Text. Ruhig, verständnisvoll, reflektiert. Keine Aufzählungszeichen, sondern fließender Text.
gemini: | gemini: "Sei mein digitaler Spiegel für {query}. Kontext: {context_str}"
Reflektiere meine aktuelle Situation {query} basierend auf meinen Werten {context_str}. openrouter: "Empathische Analyse: {query}. Kontext: {context_str}"
Sei mein empathischer digitaler Zwilling. Antworte als 'Ich'.
openrouter: |
Empathische Reflexion (OpenRouter):
Situation: {query}
Persönlicher Kontext: {context_str}
# --------------------------------------------------------- # ---------------------------------------------------------
# 4. TECHNICAL: Der Coder (Intent: CODING) # 4. TECHNICAL: Der Coder (Intent: CODING)
@ -123,13 +113,8 @@ technical_template:
- Kurze Erklärung des Ansatzes. - Kurze Erklärung des Ansatzes.
- Markdown Code-Block (Copy-Paste fertig). - Markdown Code-Block (Copy-Paste fertig).
- Wichtige Edge-Cases. - Wichtige Edge-Cases.
gemini: | gemini: "Generiere Code für {query}. Kontext: {context_str}"
Du bist Senior Software Engineer. Löse die technische Aufgabe {query} openrouter: "Technischer Support: {query}. Kontext: {context_str}"
unter Berücksichtigung meiner Dokumentation: {context_str}.
openrouter: |
Technischer Support via OpenRouter:
Task: {query}
Kontext-Snippets: {context_str}
# --------------------------------------------------------- # ---------------------------------------------------------
# 5. INTERVIEW: Der "One-Shot Extractor" (Performance Mode) # 5. INTERVIEW: Der "One-Shot Extractor" (Performance Mode)
@ -166,8 +151,8 @@ interview_template:
## (Zweiter Begriff aus STRUKTUR) ## (Zweiter Begriff aus STRUKTUR)
(Text...) (Text...)
gemini: "Transformiere den Input {query} in das Schema {schema_fields} für Typ {target_type}." gemini: "Extrahiere Daten für {target_type} aus {query}."
openrouter: "Extrahiere Daten für Typ {target_type} aus {query}. Schema: {schema_fields}." openrouter: "Strukturiere {query} nach {schema_fields}."
# --------------------------------------------------------- # ---------------------------------------------------------
# 6. EDGE_ALLOCATION: Kantenfilter (Intent: OFFLINE_FILTER) # 6. EDGE_ALLOCATION: Kantenfilter (Intent: OFFLINE_FILTER)
@ -194,16 +179,17 @@ edge_allocation_template:
DEIN OUTPUT (JSON): DEIN OUTPUT (JSON):
gemini: | gemini: |
Analysiere den Textabschnitt: {chunk_text} TASK: Ordne Kanten einem Textabschnitt zu.
Wähle aus folgender Liste alle relevanten Kanten aus: {edge_list} ERLAUBTE TYPEN: {valid_types}
Antworte STRIKT als JSON-Liste von Strings im Format ["typ:ziel"]. TEXT: {chunk_text}
Kein Text davor oder danach! KANDIDATEN: {edge_list}
OUTPUT: STRIKT eine flache JSON-Liste ["typ:ziel"]. Keine Objekte!
openrouter: | openrouter: |
Filtere die relevanten Kanten für den Graphen. Filtere relevante Kanten.
Kandidaten: {edge_list} ERLAUBTE TYPEN: {valid_types}
Text: {chunk_text} TEXT: {chunk_text}
Output: JSON-Liste ["typ:ziel"]. KANDIDATEN: {edge_list}
OUTPUT: STRIKT JSON-Liste von Strings ["typ:ziel"].
# --------------------------------------------------------- # ---------------------------------------------------------
# 7. SMART EDGE ALLOCATION: Extraktion (Intent: INGEST) # 7. SMART EDGE ALLOCATION: Extraktion (Intent: INGEST)
# --------------------------------------------------------- # ---------------------------------------------------------
@ -230,12 +216,12 @@ edge_extraction:
DEIN OUTPUT (JSON): DEIN OUTPUT (JSON):
gemini: | gemini: |
Führe eine semantische Analyse der Notiz '{note_id}' durch. Analysiere '{note_id}'. Extrahiere semantische Beziehungen.
Finde explizite und implizite Relationen. ERLAUBTE TYPEN: {valid_types}
Antworte STRIKT als JSON: [[{{"to": "Ziel", "kind": "typ", "reason": "begründung"}}]] TEXT: {text}
Keine Erklärungen, nur JSON. OUTPUT: STRIKT JSON-Liste von Objekten: [{"to": "Ziel", "kind": "typ"}]. Keine Erklärungen!
Text: {text}
openrouter: | openrouter: |
Analysiere den Text für den Graphen. Identifiziere semantische Verbindungen. Wissensgraph-Extraktion für '{note_id}'.
Output STRIKT als JSON-Liste: [[{{"to": "X", "kind": "Y"}}]]. ERLAUBTE TYPEN: {valid_types}
Text: {text} TEXT: {text}
OUTPUT: STRIKT JSON-Liste von Objekten: [{"to": "Ziel", "kind": "typ"}]. Keine Dictionaries mit Schlüsseln wie 'edges'!