bug fix Wp20
This commit is contained in:
parent
2c073c7d3c
commit
867a7a8b44
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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'!
|
||||||
Loading…
Reference in New Issue
Block a user