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: 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)

View File

@ -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}
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'!