From 867a7a8b445dcda0d742762e1926986798a2e8bd Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 23 Dec 2025 21:44:49 +0100 Subject: [PATCH] bug fix Wp20 --- app/core/ingestion.py | 71 ++++++++++++++++++++++++++++++++--------- config/prompts.yaml | 74 ++++++++++++++++++------------------------- 2 files changed, 86 insertions(+), 59 deletions(-) diff --git a/app/core/ingestion.py b/app/core/ingestion.py index 36904e3..4eebdc0 100644 --- a/app/core/ingestion.py +++ b/app/core/ingestion.py @@ -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: Kontextsensitive Kanten-Validierung mit Fundort-Reporting (Zeilennummern). 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 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 @@ -96,12 +97,11 @@ class IngestionService: self.cfg = QdrantConfig.from_env() self.cfg.prefix = self.prefix 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.embedder = EmbeddingsClient() self.llm = LLMService() - # WP-22: Change Detection Modus aus Settings self.active_hash_mode = self.settings.CHANGE_DETECTION_MODE try: @@ -111,7 +111,7 @@ class IngestionService: logger.warning(f"DB init warning: {e}") 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", {}) if profile_name in profiles: cfg = profiles[profile_name].copy() @@ -125,18 +125,25 @@ class IngestionService: WP-20: Nutzt den Hybrid LLM Service für die semantische Kanten-Extraktion. 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 - 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}") - # 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) - prompt = template.format(text=text[:6000], note_id=note_id) 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( prompt=prompt, priority="background", @@ -144,12 +151,44 @@ class IngestionService: provider=provider, model_override=model ) - data = json.loads(response_json) - for item in data: - item["provenance"] = "semantic_ai" - item["line"] = f"ai-{provider}" - return data + # Robustes Parsing (WP-20 Fix für 'str' object assignment error) + raw_data = json.loads(response_json) + processed_edges = [] + + # 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: logger.warning(f"⚠️ [Ingestion] Smart Edge Allocation failed for {note_id} on {provider}: {e}") return [] @@ -256,7 +295,9 @@ class IngestionService: # B. WP-20: Smart AI Edges (Hybrid Turbo Acceleration) ai_edges = await self._perform_smart_edge_allocation(body_text, note_id) 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) # C. System-Kanten (Struktur) diff --git a/config/prompts.yaml b/config/prompts.yaml index 3dc5d40..f9d954c 100644 --- a/config/prompts.yaml +++ b/config/prompts.yaml @@ -31,12 +31,10 @@ rag_template: Beantworte die Frage präzise basierend auf den Quellen. Fasse die Informationen zusammen. Sei objektiv und neutral. gemini: | - Nutze das Wissen meines digitalen Zwillings aus folgendem Kontext: {context_str} - Beantworte die Anfrage präzise, detailliert und strukturiert: {query} + Kontext meines digitalen Zwillings: {context_str} + Beantworte strukturiert: {query} openrouter: | - Kontext-Analyse für Gemma/Llama: - {context_str} - + Kontext: {context_str} Anfrage: {query} # --------------------------------------------------------- @@ -63,13 +61,10 @@ decision_template: - **Abgleich:** (Gibt es Konflikte mit Werten/Zielen? Nenne die Quelle!) - **Empfehlung:** (Klare Meinung: Ja/No/Vielleicht mit Begründung) gemini: | - Agiere als Senior Strategy Consultant für meinen digitalen Zwilling. - Wäge die Frage {query} multiperspektivisch gegen meine Werte und langfristigen Ziele ab. - Kontext: {context_str} + Agiere als strategischer Partner. Analysiere {query} basierend auf {context_str}. openrouter: | - Strategischer Check via OpenRouter/Gemma: - Analyse der Entscheidungsfrage: {query} - Referenzdaten aus dem Graph: {context_str} + Entscheidungsanalyse für: {query} + Datenbasis: {context_str} # --------------------------------------------------------- # 3. EMPATHY: Der Spiegel / "Ich"-Modus (Intent: EMPATHY) @@ -92,13 +87,8 @@ empathy_template: TONFALL: Ruhig, verständnisvoll, reflektiert. Keine Aufzählungszeichen, sondern fließender Text. - gemini: | - Reflektiere meine aktuelle Situation {query} basierend auf meinen Werten {context_str}. - Sei mein empathischer digitaler Zwilling. Antworte als 'Ich'. - openrouter: | - Empathische Reflexion (OpenRouter): - Situation: {query} - Persönlicher Kontext: {context_str} + gemini: "Sei mein digitaler Spiegel für {query}. Kontext: {context_str}" + openrouter: "Empathische Analyse: {query}. Kontext: {context_str}" # --------------------------------------------------------- # 4. TECHNICAL: Der Coder (Intent: CODING) @@ -123,13 +113,8 @@ technical_template: - Kurze Erklärung des Ansatzes. - Markdown Code-Block (Copy-Paste fertig). - Wichtige Edge-Cases. - gemini: | - Du bist Senior Software Engineer. Löse die technische Aufgabe {query} - unter Berücksichtigung meiner Dokumentation: {context_str}. - openrouter: | - Technischer Support via OpenRouter: - Task: {query} - Kontext-Snippets: {context_str} + gemini: "Generiere Code für {query}. Kontext: {context_str}" + openrouter: "Technischer Support: {query}. Kontext: {context_str}" # --------------------------------------------------------- # 5. INTERVIEW: Der "One-Shot Extractor" (Performance Mode) @@ -166,8 +151,8 @@ interview_template: ## (Zweiter Begriff aus STRUKTUR) (Text...) - gemini: "Transformiere den Input {query} in das Schema {schema_fields} für Typ {target_type}." - openrouter: "Extrahiere Daten für Typ {target_type} aus {query}. Schema: {schema_fields}." + gemini: "Extrahiere Daten für {target_type} aus {query}." + openrouter: "Strukturiere {query} nach {schema_fields}." # --------------------------------------------------------- # 6. EDGE_ALLOCATION: Kantenfilter (Intent: OFFLINE_FILTER) @@ -194,16 +179,17 @@ edge_allocation_template: DEIN OUTPUT (JSON): gemini: | - Analysiere den Textabschnitt: {chunk_text} - Wähle aus folgender Liste alle relevanten Kanten aus: {edge_list} - Antworte STRIKT als JSON-Liste von Strings im Format ["typ:ziel"]. - Kein Text davor oder danach! + TASK: Ordne Kanten einem Textabschnitt zu. + ERLAUBTE TYPEN: {valid_types} + TEXT: {chunk_text} + KANDIDATEN: {edge_list} + OUTPUT: STRIKT eine flache JSON-Liste ["typ:ziel"]. Keine Objekte! openrouter: | - Filtere die relevanten Kanten für den Graphen. - Kandidaten: {edge_list} - Text: {chunk_text} - Output: JSON-Liste ["typ:ziel"]. - + Filtere relevante Kanten. + ERLAUBTE TYPEN: {valid_types} + TEXT: {chunk_text} + KANDIDATEN: {edge_list} + OUTPUT: STRIKT JSON-Liste von Strings ["typ:ziel"]. # --------------------------------------------------------- # 7. SMART EDGE ALLOCATION: Extraktion (Intent: INGEST) # --------------------------------------------------------- @@ -230,12 +216,12 @@ edge_extraction: DEIN OUTPUT (JSON): gemini: | - Führe eine semantische Analyse der Notiz '{note_id}' durch. - Finde explizite und implizite Relationen. - Antworte STRIKT als JSON: [[{{"to": "Ziel", "kind": "typ", "reason": "begründung"}}]] - Keine Erklärungen, nur JSON. - Text: {text} + Analysiere '{note_id}'. Extrahiere semantische Beziehungen. + ERLAUBTE TYPEN: {valid_types} + TEXT: {text} + OUTPUT: STRIKT JSON-Liste von Objekten: [{"to": "Ziel", "kind": "typ"}]. Keine Erklärungen! openrouter: | - Analysiere den Text für den Graphen. Identifiziere semantische Verbindungen. - Output STRIKT als JSON-Liste: [[{{"to": "X", "kind": "Y"}}]]. - Text: {text} \ No newline at end of file + Wissensgraph-Extraktion für '{note_id}'. + ERLAUBTE TYPEN: {valid_types} + TEXT: {text} + OUTPUT: STRIKT JSON-Liste von Objekten: [{"to": "Ziel", "kind": "typ"}]. Keine Dictionaries mit Schlüsseln wie 'edges'! \ No newline at end of file