diff --git a/app/config.py b/app/config.py index a860f2d..aa0d9dd 100644 --- a/app/config.py +++ b/app/config.py @@ -1,40 +1,74 @@ """ FILE: app/config.py -DESCRIPTION: Zentrale Pydantic-Konfiguration (Env-Vars für Qdrant, LLM, Retriever). -VERSION: 0.4.0 +DESCRIPTION: Zentrale Pydantic-Konfiguration. + WP-20: Hybrid-Cloud Modus Support (OpenRouter/Gemini/Ollama). + FIX: Einführung von Parametern zur intelligenten Rate-Limit Steuerung (429 Handling). +VERSION: 0.6.7 STATUS: Active -DEPENDENCIES: os, functools, pathlib -LAST_ANALYSIS: 2025-12-15 +DEPENDENCIES: os, functools, pathlib, python-dotenv """ from __future__ import annotations import os from functools import lru_cache from pathlib import Path +from dotenv import load_dotenv + +# WP-20: Lade Umgebungsvariablen aus der .env Datei +# override=True garantiert, dass Änderungen in der .env immer Vorrang haben. +load_dotenv(override=True) class Settings: - # Qdrant + # --- Qdrant Datenbank --- QDRANT_URL: str = os.getenv("QDRANT_URL", "http://127.0.0.1:6333") QDRANT_API_KEY: str | None = os.getenv("QDRANT_API_KEY") - COLLECTION_PREFIX: str = os.getenv("MINDNET_PREFIX", "mindnet") - VECTOR_SIZE: int = int(os.getenv("MINDNET_VECTOR_SIZE", "384")) + COLLECTION_PREFIX: str = os.getenv("MINDNET_PREFIX", "mindnet_dev") + + # WP-22: Vektor-Dimension für das Embedding-Modell (nomic) + VECTOR_SIZE: int = int(os.getenv("VECTOR_DIM", "768")) DISTANCE: str = os.getenv("MINDNET_DISTANCE", "Cosine") - # Embeddings + # --- Lokale Embeddings (Ollama & Sentence-Transformers) --- + EMBEDDING_MODEL: str = os.getenv("MINDNET_EMBEDDING_MODEL", "nomic-embed-text") MODEL_NAME: str = os.getenv("MINDNET_MODEL", "sentence-transformers/all-MiniLM-L6-v2") - # WP-05 LLM / Ollama + # --- WP-20 Hybrid LLM Provider --- + # Erlaubt: "ollama" | "gemini" | "openrouter" + MINDNET_LLM_PROVIDER: str = os.getenv("MINDNET_LLM_PROVIDER", "openrouter").lower() + + # Google AI Studio (2025er Lite-Modell für höhere Kapazität) + GOOGLE_API_KEY: str | None = os.getenv("GOOGLE_API_KEY") + GEMINI_MODEL: str = os.getenv("MINDNET_GEMINI_MODEL", "gemini-2.5-flash-lite") + + # OpenRouter Integration (Verfügbares Free-Modell 2025) + OPENROUTER_API_KEY: str | None = os.getenv("OPENROUTER_API_KEY") + OPENROUTER_MODEL: str = os.getenv("OPENROUTER_MODEL", "mistralai/mistral-7b-instruct:free") + + LLM_FALLBACK_ENABLED: bool = os.getenv("MINDNET_LLM_FALLBACK", "true").lower() == "true" + + # --- NEU: Intelligente Rate-Limit Steuerung --- + # Dauer der Wartezeit in Sekunden, wenn ein HTTP 429 (Rate Limit) auftritt + LLM_RATE_LIMIT_WAIT: float = float(os.getenv("MINDNET_LLM_RATE_LIMIT_WAIT", "60.0")) + # Anzahl der Cloud-Retries bei 429, bevor Ollama-Fallback greift + LLM_RATE_LIMIT_RETRIES: int = int(os.getenv("MINDNET_LLM_RATE_LIMIT_RETRIES", "3")) + + # --- WP-05 Lokales LLM (Ollama) --- OLLAMA_URL: str = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434") LLM_MODEL: str = os.getenv("MINDNET_LLM_MODEL", "phi3:mini") PROMPTS_PATH: str = os.getenv("MINDNET_PROMPTS_PATH", "config/prompts.yaml") - # NEU für WP-06 - LLM_TIMEOUT: float = float(os.getenv("MINDNET_LLM_TIMEOUT", "120.0")) + # --- WP-06 / WP-14 Performance & Last-Steuerung --- + LLM_TIMEOUT: float = float(os.getenv("MINDNET_LLM_TIMEOUT", "300.0")) DECISION_CONFIG_PATH: str = os.getenv("MINDNET_DECISION_CONFIG", "config/decision_engine.yaml") + BACKGROUND_LIMIT: int = int(os.getenv("MINDNET_LLM_BACKGROUND_LIMIT", "2")) - # API + # --- System-Pfade & Ingestion-Logik --- DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true" + MINDNET_VAULT_ROOT: str = os.getenv("MINDNET_VAULT_ROOT", "./vault_master") + MINDNET_TYPES_FILE: str = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml") + MINDNET_VOCAB_PATH: str = os.getenv("MINDNET_VOCAB_PATH", "/mindnet/vault/mindnet/_system/dictionary/edge_vocabulary.md") + CHANGE_DETECTION_MODE: str = os.getenv("MINDNET_CHANGE_DETECTION_MODE", "full") - # WP-04 Retriever Defaults + # --- WP-04 Retriever Gewichte --- RETRIEVER_W_SEM: float = float(os.getenv("MINDNET_WP04_W_SEM", "0.70")) RETRIEVER_W_EDGE: float = float(os.getenv("MINDNET_WP04_W_EDGE", "0.25")) RETRIEVER_W_CENT: float = float(os.getenv("MINDNET_WP04_W_CENT", "0.05")) @@ -44,4 +78,5 @@ class Settings: @lru_cache def get_settings() -> Settings: + """Gibt die zentralen Einstellungen als Singleton zurück.""" return Settings() \ No newline at end of file diff --git a/app/core/ingestion.py b/app/core/ingestion.py index 34f249b..53c28cc 100644 --- a/app/core/ingestion.py +++ b/app/core/ingestion.py @@ -1,17 +1,16 @@ """ FILE: app/core/ingestion.py -DESCRIPTION: Haupt-Ingestion-Logik. Transformiert Markdown in den Graphen (Notes, Chunks, Edges). -FIX: Korrekte Priorisierung von Frontmatter für chunk_profile und retriever_weight. - Lade Chunk-Config basierend auf dem effektiven Profil, nicht nur dem Notiz-Typ. - 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.9.0 (WP-22 Full Integration: Context-Aware Registry) +DESCRIPTION: Haupt-Ingestion-Logik. Transformiert Markdown in den Graphen. + WP-20: Optimiert für OpenRouter (mistralai/mistral-7b-instruct:free). + WP-22: Content Lifecycle, Edge Registry Validation & Multi-Hash. +FIX: Finale Mistral-Härtung ( & [OUT] Tags), robuste JSON-Recovery & DoD-Sync. +VERSION: 2.11.11 STATUS: Active -DEPENDENCIES: app.core.parser, app.core.note_payload, app.core.chunker, app.core.derive_edges, app.core.qdrant*, app.services.embeddings_client, app.services.edge_registry -EXTERNAL_CONFIG: config/types.yaml +DEPENDENCIES: app.core.parser, app.core.note_payload, app.core.chunker, app.services.llm_service, app.services.edge_registry """ import os +import json +import re import logging import asyncio import time @@ -22,7 +21,7 @@ from app.core.parser import ( read_markdown, normalize_frontmatter, validate_required_frontmatter, - extract_edges_with_context, # WP-22: Neue Funktion für Zeilennummern + extract_edges_with_context, ) from app.core.note_payload import make_note_payload from app.core.chunker import assemble_chunks, get_chunk_config @@ -44,78 +43,75 @@ from app.core.qdrant_points import ( from app.services.embeddings_client import EmbeddingsClient from app.services.edge_registry import registry as edge_registry +from app.services.llm_service import LLMService logger = logging.getLogger(__name__) -# --- Helper --- +# --- Global Helpers --- +def extract_json_from_response(text: str) -> Any: + """ + Extrahiert JSON-Daten und bereinigt LLM-Steuerzeichen (Mistral/Llama). + Entfernt , [OUT], [/OUT] und Markdown-Blöcke für maximale Robustheit. + """ + if not text: return [] + + # 1. Entferne Mistral/Llama Steuerzeichen und Tags + clean = text.replace("", "").replace("", "") + clean = clean.replace("[OUT]", "").replace("[/OUT]", "") + clean = clean.strip() + + # 2. Suche nach Markdown JSON-Blöcken (```json ... ```) + match = re.search(r"```(?:json)?\s*(.*?)\s*```", clean, re.DOTALL) + payload = match.group(1) if match else clean + + try: + return json.loads(payload.strip()) + except json.JSONDecodeError: + # 3. Recovery: Suche nach der ersten [ und letzten ] (Liste) + start = payload.find('[') + end = payload.rfind(']') + 1 + if start != -1 and end > start: + try: + return json.loads(payload[start:end]) + except: pass + + # 4. Zweite Recovery: Suche nach der ersten { und letzten } (Objekt) + start_obj = payload.find('{') + end_obj = payload.rfind('}') + 1 + if start_obj != -1 and end_obj > start_obj: + try: + return json.loads(payload[start_obj:end_obj]) + except: pass + + return [] + def load_type_registry(custom_path: Optional[str] = None) -> dict: """Lädt die types.yaml zur Steuerung der typ-spezifischen Ingestion.""" import yaml - path = custom_path or os.getenv("MINDNET_TYPES_FILE", "config/types.yaml") + from app.config import get_settings + settings = get_settings() + path = custom_path or settings.MINDNET_TYPES_FILE if not os.path.exists(path): return {} try: with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {} except Exception: return {} -def resolve_note_type(requested: Optional[str], reg: dict) -> str: - """Bestimmt den finalen Notiz-Typ (Fallback auf 'concept').""" - types = reg.get("types", {}) - if requested and requested in types: return requested - return "concept" - -def effective_chunk_profile_name(fm: dict, note_type: str, reg: dict) -> str: - """ - Ermittelt den Namen des zu nutzenden Chunk-Profils. - Priorität: 1. Frontmatter Override -> 2. Type Config -> 3. Global Default - """ - # 1. Frontmatter Override - override = fm.get("chunking_profile") or fm.get("chunk_profile") - if override and isinstance(override, str): - return override - - # 2. Type Config - t_cfg = reg.get("types", {}).get(note_type, {}) - if t_cfg: - cp = t_cfg.get("chunking_profile") or t_cfg.get("chunk_profile") - if cp: return cp - - # 3. Global Default - return reg.get("defaults", {}).get("chunking_profile", "sliding_standard") - -def effective_retriever_weight(fm: dict, note_type: str, reg: dict) -> float: - """ - Ermittelt das effektive retriever_weight für das Scoring. - Priorität: 1. Frontmatter Override -> 2. Type Config -> 3. Global Default - """ - # 1. Frontmatter Override - override = fm.get("retriever_weight") - if override is not None: - try: return float(override) - except: pass - - # 2. Type Config - t_cfg = reg.get("types", {}).get(note_type, {}) - if t_cfg and "retriever_weight" in t_cfg: - return float(t_cfg["retriever_weight"]) - - # 3. Global Default - return float(reg.get("defaults", {}).get("retriever_weight", 1.0)) - - +# --- Service Class --- class IngestionService: def __init__(self, collection_prefix: str = None): - env_prefix = os.getenv("COLLECTION_PREFIX", "mindnet") - self.prefix = collection_prefix or env_prefix + from app.config import get_settings + self.settings = get_settings() + self.prefix = collection_prefix or self.settings.COLLECTION_PREFIX self.cfg = QdrantConfig.from_env() self.cfg.prefix = self.prefix self.client = get_client(self.cfg) - self.dim = self.cfg.dim + self.dim = self.settings.VECTOR_SIZE self.registry = load_type_registry() self.embedder = EmbeddingsClient() + self.llm = LLMService() - # Change Detection Modus (full oder body) - self.active_hash_mode = os.getenv("MINDNET_CHANGE_DETECTION_MODE", "full") + self.active_hash_mode = self.settings.CHANGE_DETECTION_MODE try: ensure_collections(self.client, self.prefix, self.dim) @@ -123,8 +119,14 @@ class IngestionService: except Exception as e: logger.warning(f"DB init warning: {e}") + def _resolve_note_type(self, requested: Optional[str]) -> str: + """Bestimmt den finalen Notiz-Typ (Fallback auf 'concept').""" + types = self.registry.get("types", {}) + if requested and requested in types: return requested + return "concept" + 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 aus der Registry.""" profiles = self.registry.get("chunking_profiles", {}) if profile_name in profiles: cfg = profiles[profile_name].copy() @@ -133,93 +135,115 @@ class IngestionService: return cfg return get_chunk_config(note_type) + async def _perform_smart_edge_allocation(self, text: str, note_id: str) -> List[Dict]: + """ + WP-20: Nutzt das Hybrid LLM für die semantische Kanten-Extraktion. + Respektiert die Provider-Einstellung (OpenRouter Primary). + """ + provider = self.settings.MINDNET_LLM_PROVIDER + model = self.settings.OPENROUTER_MODEL if provider == "openrouter" else self.settings.GEMINI_MODEL + + logger.info(f"🚀 [Ingestion] Turbo-Mode: Extracting edges for '{note_id}' using {model} on {provider}") + + edge_registry.ensure_latest() + valid_types_str = ", ".join(sorted(list(edge_registry.valid_types))) + + template = self.llm.get_prompt("edge_extraction", provider) + + try: + # Sicherheits-Check: Formatierung des Templates gegen KeyError schützen + try: + # Nutzt die ersten 6000 Zeichen als Kontext-Fenster + prompt = template.format( + text=text[:6000], + note_id=note_id, + valid_types=valid_types_str + ) + except KeyError as ke: + logger.error(f"❌ [Ingestion] Prompt-Template Fehler (Variable {ke} fehlt).") + return [] + + response_json = await self.llm.generate_raw_response( + prompt=prompt, priority="background", force_json=True, + provider=provider, model_override=model + ) + + # Nutzt den verbesserten Mistral-sicheren JSON-Extraktor + raw_data = extract_json_from_response(response_json) + + # Recovery: Suche nach Listen in Dictionaries (z.B. {"edges": [...]}) + if isinstance(raw_data, dict): + for k in ["edges", "links", "results", "kanten"]: + if k in raw_data and isinstance(raw_data[k], list): + raw_data = raw_data[k] + break + + if not isinstance(raw_data, list): + logger.warning(f"⚠️ [Ingestion] LLM lieferte keine Liste für {note_id}") + return [] + + processed = [] + for item in raw_data: + # Fix für 'str' object assignment error: Erkennt sowohl Dict als auch String ["kind:target"] + if isinstance(item, dict) and "to" in item: + item["provenance"] = "semantic_ai" + item["line"] = f"ai-{provider}" + processed.append(item) + elif isinstance(item, str) and ":" in item: + parts = item.split(":", 1) + processed.append({ + "to": parts[1].strip(), + "kind": parts[0].strip(), + "provenance": "semantic_ai", + "line": f"ai-{provider}" + }) + return processed + + except Exception as e: + logger.warning(f"⚠️ [Ingestion] Smart Edge Allocation failed for {note_id}: {e}") + return [] + async def process_file( - self, - file_path: str, - vault_root: str, - force_replace: bool = False, - apply: bool = False, - purge_before: bool = False, - note_scope_refs: bool = False, - hash_source: str = "parsed", - hash_normalize: str = "canonical" + self, file_path: str, vault_root: str, + force_replace: bool = False, apply: bool = False, purge_before: bool = False, + note_scope_refs: bool = False, hash_source: str = "parsed", hash_normalize: str = "canonical" ) -> Dict[str, Any]: - """ - Verarbeitet eine Markdown-Datei und schreibt sie in den Graphen. - Folgt dem 14-Schritte-Workflow. - """ + """Transformiert eine Markdown-Datei in den Graphen (Notes, Chunks, Edges).""" result = {"path": file_path, "status": "skipped", "changed": False, "error": None} - # 1. Parse & Frontmatter Validation + # 1. Parse & Lifecycle Gate try: parsed = read_markdown(file_path) - if not parsed: return {**result, "error": "Empty or unreadable file"} + if not parsed: return {**result, "error": "Empty file"} fm = normalize_frontmatter(parsed.frontmatter) validate_required_frontmatter(fm) except Exception as e: - logger.error(f"Validation failed for {file_path}: {e}") return {**result, "error": f"Validation failed: {str(e)}"} - # --- WP-22: Content Lifecycle Gate --- status = fm.get("status", "draft").lower().strip() - - # Hard Skip für System- oder Archiv-Dateien if status in ["system", "template", "archive", "hidden"]: - logger.info(f"Skipping file {file_path} (Status: {status})") - return {**result, "status": "skipped", "reason": f"lifecycle_status_{status}"} + return {**result, "status": "skipped", "reason": f"lifecycle_{status}"} - # 2. Type & Config Resolution - note_type = resolve_note_type(fm.get("type"), self.registry) + # 2. Config Resolution & Payload Construction + note_type = self._resolve_note_type(fm.get("type")) fm["type"] = note_type - effective_profile = effective_chunk_profile_name(fm, note_type, self.registry) - effective_weight = effective_retriever_weight(fm, note_type, self.registry) - - fm["chunk_profile"] = effective_profile - fm["retriever_weight"] = effective_weight - - # 3. Build Note Payload (Inkl. Multi-Hash für WP-22) try: - note_pl = make_note_payload( - parsed, - vault_root=vault_root, - hash_normalize=hash_normalize, - hash_source=hash_source, - file_path=file_path - ) - # Text Body Fallback - if not note_pl.get("fulltext"): note_pl["fulltext"] = getattr(parsed, "body", "") or "" - - # Sicherstellen der effektiven Werte im Payload - note_pl["retriever_weight"] = effective_weight - note_pl["chunk_profile"] = effective_profile - # WP-22: Status speichern - note_pl["status"] = status - + note_pl = make_note_payload(parsed, vault_root=vault_root, hash_normalize=hash_normalize, hash_source=hash_source, file_path=file_path) note_id = note_pl["note_id"] except Exception as e: - logger.error(f"Payload build failed: {e}") - return {**result, "error": f"Payload build failed: {str(e)}"} + return {**result, "error": f"Payload failed: {str(e)}"} - # 4. Change Detection - old_payload = None - if not force_replace: - old_payload = self._fetch_note_payload(note_id) - - has_old = old_payload is not None - # Prüfung gegen den aktuell konfigurierten Hash-Modus (body oder full) + # 3. Change Detection (Strikte DoD Umsetzung: Kein Shortcut) + old_payload = None if force_replace else self._fetch_note_payload(note_id) check_key = f"{self.active_hash_mode}:{hash_source}:{hash_normalize}" - - old_hashes = (old_payload or {}).get("hashes") - if isinstance(old_hashes, dict): old_hash = old_hashes.get(check_key) - elif isinstance(old_hashes, str) and self.active_hash_mode == "body": old_hash = old_hashes - else: old_hash = None - + old_hash = (old_payload or {}).get("hashes", {}).get(check_key) new_hash = note_pl.get("hashes", {}).get(check_key) - hash_changed = (old_hash != new_hash) + + # Prüfung auf fehlende Artefakte in Qdrant chunks_missing, edges_missing = self._artifacts_missing(note_id) - should_write = force_replace or (not has_old) or hash_changed or chunks_missing or edges_missing + should_write = force_replace or (not old_payload) or (old_hash != new_hash) or chunks_missing or edges_missing if not should_write: return {**result, "status": "unchanged", "note_id": note_id} @@ -227,157 +251,105 @@ class IngestionService: if not apply: return {**result, "status": "dry-run", "changed": True, "note_id": note_id} - # 5. Processing (Chunking, Embedding, Edge Generation) + # 4. Processing (Chunking, Embedding, AI Edges) try: body_text = getattr(parsed, "body", "") or "" - - # WP-22: Sicherstellen, dass die Registry aktuell ist (Lazy Reload) edge_registry.ensure_latest() - # Konfiguration für das spezifische Profil laden - 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 mit Metadaten anreichern + # Profil-gesteuertes Chunking + profile = fm.get("chunk_profile") or fm.get("chunking_profile") or "sliding_standard" + chunk_cfg = self._get_chunk_config_by_profile(profile, note_type) + chunks = await assemble_chunks(fm["id"], body_text, fm["type"], config=chunk_cfg) chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text) + # Vektorisierung vecs = [] if chunk_pls: texts = [c.get("window") or c.get("text") or "" for c in chunk_pls] - try: - if hasattr(self.embedder, 'embed_documents'): - vecs = await self.embedder.embed_documents(texts) - else: - for t in texts: - v = await self.embedder.embed_query(t) - vecs.append(v) - except Exception as e: - logger.error(f"Embedding failed: {e}") - raise RuntimeError(f"Embedding failed: {e}") + vecs = await self.embedder.embed_documents(texts) - # --- WP-22: Kanten-Extraktion & Validierung --- - # A. Explizite User-Kanten mit Zeilennummern extrahieren - explicit_edges = extract_edges_with_context(parsed) - - # B. System-Kanten generieren (Struktur: belongs_to, next, prev) - try: - raw_system_edges = build_edges_for_note( - note_id, - chunk_pls, - note_level_references=note_pl.get("references", []), - include_note_scope_refs=note_scope_refs - ) - except TypeError: - raw_system_edges = build_edges_for_note(note_id, chunk_pls) - - # C. Alle Kanten validieren und über die Registry mappen + # Kanten-Extraktion edges = [] context = {"file": file_path, "note_id": note_id} - # Zuerst User-Kanten (provenance="explicit") - for e in explicit_edges: - valid_kind = edge_registry.resolve( - edge_type=e["kind"], - provenance="explicit", - context={**context, "line": e.get("line")} - ) + # A. Explizite Kanten (User) + for e in extract_edges_with_context(parsed): + e["kind"] = edge_registry.resolve(edge_type=e["kind"], provenance="explicit", context={**context, "line": e.get("line")}) + edges.append(e) + + # B. KI Kanten (Turbo) + ai_edges = await self._perform_smart_edge_allocation(body_text, note_id) + for e in ai_edges: + 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) - # Dann System-Kanten (provenance="structure") - for e in raw_system_edges: - # Sicherstellen, dass System-Kanten korrekt markiert sind - valid_kind = edge_registry.resolve( - edge_type=e.get("kind", "belongs_to"), - provenance="structure", - context={**context, "line": "system"} - ) - e["kind"] = valid_kind - # Nur hinzufügen, wenn die Registry einen validen Typ zurückgibt + # C. System Kanten (Struktur) + try: + sys_edges = build_edges_for_note(note_id, chunk_pls, note_level_references=note_pl.get("references", []), include_note_scope_refs=note_scope_refs) + except: + sys_edges = build_edges_for_note(note_id, chunk_pls) + + for e in sys_edges: + valid_kind = edge_registry.resolve(edge_type=e.get("kind", "belongs_to"), provenance="structure", context={**context, "line": "system"}) if valid_kind: + e["kind"] = valid_kind edges.append(e) except Exception as e: - logger.error(f"Processing failed: {e}", exc_info=True) + logger.error(f"Processing failed for {file_path}: {e}", exc_info=True) return {**result, "error": f"Processing failed: {str(e)}"} - # 6. Upsert in Qdrant + # 5. DB Upsert try: - # Alte Fragmente löschen, um "Geister-Chunks" zu vermeiden - if purge_before and has_old: - self._purge_artifacts(note_id) + if purge_before and old_payload: self._purge_artifacts(note_id) - # Note Metadaten n_name, n_pts = points_for_note(self.prefix, note_pl, None, self.dim) upsert_batch(self.client, n_name, n_pts) - # Chunks (Vektoren) if chunk_pls and vecs: c_name, c_pts = points_for_chunks(self.prefix, chunk_pls, vecs) upsert_batch(self.client, c_name, c_pts) - # Kanten if edges: e_name, e_pts = points_for_edges(self.prefix, edges) upsert_batch(self.client, e_name, e_pts) - return { - "path": file_path, - "status": "success", - "changed": True, - "note_id": note_id, - "chunks_count": len(chunk_pls), - "edges_count": len(edges) - } + return {"path": file_path, "status": "success", "changed": True, "note_id": note_id, "chunks_count": len(chunk_pls), "edges_count": len(edges)} except Exception as e: - logger.error(f"Upsert failed: {e}", exc_info=True) return {**result, "error": f"DB Upsert failed: {e}"} def _fetch_note_payload(self, note_id: str) -> Optional[dict]: - """Holt das aktuelle Payload einer Note aus Qdrant.""" from qdrant_client.http import models as rest - col = f"{self.prefix}_notes" try: f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))]) - pts, _ = self.client.scroll(collection_name=col, scroll_filter=f, limit=1, with_payload=True) + pts, _ = self.client.scroll(collection_name=f"{self.prefix}_notes", scroll_filter=f, limit=1, with_payload=True) return pts[0].payload if pts else None except: return None def _artifacts_missing(self, note_id: str) -> Tuple[bool, bool]: - """Prüft, ob Chunks oder Kanten für eine Note fehlen (Integritätscheck).""" + """Prüft Qdrant aktiv auf vorhandene Chunks und Edges (Kein Shortcut).""" from qdrant_client.http import models as rest - c_col = f"{self.prefix}_chunks" - e_col = f"{self.prefix}_edges" try: f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))]) - c_pts, _ = self.client.scroll(collection_name=c_col, scroll_filter=f, limit=1) - e_pts, _ = self.client.scroll(collection_name=e_col, scroll_filter=f, limit=1) + c_pts, _ = self.client.scroll(collection_name=f"{self.prefix}_chunks", scroll_filter=f, limit=1) + e_pts, _ = self.client.scroll(collection_name=f"{self.prefix}_edges", scroll_filter=f, limit=1) return (not bool(c_pts)), (not bool(e_pts)) except: return True, True def _purge_artifacts(self, note_id: str): - """Löscht alle Chunks und Edges einer Note (vor dem Neu-Schreiben).""" from qdrant_client.http import models as rest f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))]) - selector = rest.FilterSelector(filter=f) for suffix in ["chunks", "edges"]: - try: - self.client.delete(collection_name=f"{self.prefix}_{suffix}", points_selector=selector) - except Exception: pass + try: self.client.delete(collection_name=f"{self.prefix}_{suffix}", points_selector=rest.FilterSelector(filter=f)) + except: pass async def create_from_text(self, markdown_content: str, filename: str, vault_root: str, folder: str = "00_Inbox") -> Dict[str, Any]: - """Hilfsmethode zur Erstellung einer Note aus einem Textstream (Editor-Save).""" + """Hilfsmethode zur Erstellung einer Note aus einem Textstream.""" target_dir = os.path.join(vault_root, folder) os.makedirs(target_dir, exist_ok=True) file_path = os.path.join(target_dir, filename) - try: - with open(file_path, "w", encoding="utf-8") as f: - f.write(markdown_content) - f.flush() - os.fsync(f.fileno()) - await asyncio.sleep(0.1) - logger.info(f"Written file to {file_path}") - except Exception as e: - return {"status": "error", "error": f"Disk write failed: {str(e)}"} + with open(file_path, "w", encoding="utf-8") as f: + f.write(markdown_content) + await asyncio.sleep(0.1) return await self.process_file(file_path=file_path, vault_root=vault_root, apply=True, force_replace=True, purge_before=True) \ No newline at end of file diff --git a/app/core/parser.py b/app/core/parser.py index baf4422..b47aeb7 100644 --- a/app/core/parser.py +++ b/app/core/parser.py @@ -1,10 +1,11 @@ """ FILE: app/core/parser.py DESCRIPTION: Liest Markdown-Dateien fehlertolerant (Encoding-Fallback). Trennt Frontmatter (YAML) vom Body. -VERSION: 1.7.1 + WP-22 Erweiterung: Kanten-Extraktion mit Zeilennummern für die EdgeRegistry. +VERSION: 1.8.0 STATUS: Active DEPENDENCIES: yaml, re, dataclasses, json, io, os -LAST_ANALYSIS: 2025-12-15 +LAST_ANALYSIS: 2025-12-23 """ from __future__ import annotations @@ -138,13 +139,7 @@ def _read_text_with_fallback(path: str) -> Tuple[str, str, bool]: def read_markdown(path: str) -> Optional[ParsedNote]: """ - Liest eine Markdown-Datei fehlertolerant: - - Erlaubt verschiedene Encodings (UTF-8 bevorzugt, cp1252/latin-1 als Fallback). - - Schlägt NICHT mit UnicodeDecodeError fehl. - - Gibt ParsedNote(frontmatter, body, path) zurück oder None, falls die Datei nicht existiert. - - Bei Decoding-Fallback wird ein JSON-Warnhinweis geloggt: - {"path": "...", "warn": "encoding_fallback_used", "used": "cp1252"} + Liest eine Markdown-Datei fehlertolerant. """ if not os.path.exists(path): return None @@ -161,10 +156,6 @@ def validate_required_frontmatter(fm: Dict[str, Any], required: Tuple[str, ...] = ("id", "title")) -> None: """ Prüft, ob alle Pflichtfelder vorhanden sind. - Default-kompatibel: ('id', 'title'), kann aber vom Aufrufer erweitert werden, z. B.: - validate_required_frontmatter(fm, required=("id","title","type","status","created")) - - Hebt ValueError, falls Felder fehlen oder leer sind. """ if fm is None: fm = {} @@ -178,17 +169,13 @@ def validate_required_frontmatter(fm: Dict[str, Any], if missing: raise ValueError(f"Missing required frontmatter fields: {', '.join(missing)}") - # Plausibilitäten: 'tags' sollte eine Liste sein, wenn vorhanden if "tags" in fm and fm["tags"] not in (None, "") and not isinstance(fm["tags"], (list, tuple)): raise ValueError("frontmatter 'tags' must be a list of strings") def normalize_frontmatter(fm: Dict[str, Any]) -> Dict[str, Any]: """ - Sanfte Normalisierung ohne Semantikänderung: - - 'tags' → Liste von Strings (Trim) - - 'embedding_exclude' → bool - - andere Felder unverändert + Normalisierung von Tags und anderen Feldern. """ out = dict(fm or {}) if "tags" in out: @@ -205,15 +192,12 @@ def normalize_frontmatter(fm: Dict[str, Any]) -> Dict[str, Any]: # ------------------------------ Wikilinks ---------------------------- # -# Basismuster für [[...]]; die Normalisierung (id vor '#', vor '|') macht extract_wikilinks _WIKILINK_RE = re.compile(r"\[\[([^\]]+)\]\]") def extract_wikilinks(text: str) -> List[str]: """ - Extrahiert Wikilinks wie [[id]], [[id#anchor]], [[id|label]], [[id#anchor|label]]. - Rückgabe sind NUR die Ziel-IDs (ohne Anchor/Label), führend/folgend getrimmt. - Keine aggressive Slug-Normalisierung (die kann später im Resolver erfolgen). + Extrahiert Wikilinks als einfache Liste von IDs. """ if not text: return [] @@ -222,29 +206,52 @@ def extract_wikilinks(text: str) -> List[str]: raw = (m.group(1) or "").strip() if not raw: continue - # Split an Pipe (Label) → links vor '|' if "|" in raw: raw = raw.split("|", 1)[0].strip() - # Split an Anchor if "#" in raw: raw = raw.split("#", 1)[0].strip() if raw: out.append(raw) return out -def extract_edges_with_context(note: ParsedNote) -> List[Dict[str, Any]]: - """Extrahiert Kanten-Typen, Ziele und Zeilennummern.""" - edges = [] - lines = note.body.splitlines() - # Erkennt [[rel:typ Ziel]] - rel_pattern = re.compile(r"\[\[rel:([a-zA-Z0-9_-]+)\s+([^\]|#]+)\]\]") - for i, line in enumerate(lines): - for match in rel_pattern.finditer(line): - edges.append({ - "kind": match.group(1).strip(), - "target": match.group(2).strip(), - "line": i + 1, - "provenance": "explicit" - }) +def extract_edges_with_context(parsed: ParsedNote) -> List[Dict[str, Any]]: + """ + WP-22: Extrahiert Wikilinks [[Ziel|Typ]] aus dem Body und speichert die Zeilennummer. + Gibt eine Liste von Dictionaries zurück, die direkt von der Ingestion verarbeitet werden können. + """ + edges = [] + if not parsed or not parsed.body: + return edges + + # Wir nutzen splitlines(True), um Zeilenumbrüche für die Positionsberechnung zu erhalten, + # oder einfaches splitlines() für die reine Zeilennummerierung. + lines = parsed.body.splitlines() + + for line_num, line_content in enumerate(lines, 1): + for match in _WIKILINK_RE.finditer(line_content): + raw = (match.group(1) or "").strip() + if not raw: + continue + + # Syntax: [[Ziel|Typ]] + if "|" in raw: + parts = raw.split("|", 1) + target = parts[0].strip() + kind = parts[1].strip() + else: + target = raw.strip() + kind = "related_to" # Default-Typ + + # Anchor (#) entfernen, da Relationen auf Notiz-Ebene (ID) basieren + if "#" in target: + target = target.split("#", 1)[0].strip() + + if target: + edges.append({ + "to": target, + "kind": kind, + "line": line_num, + "provenance": "explicit" + }) return edges \ No newline at end of file diff --git a/app/routers/chat.py b/app/routers/chat.py index ae44547..986c131 100644 --- a/app/routers/chat.py +++ b/app/routers/chat.py @@ -1,8 +1,9 @@ """ FILE: app/routers/chat.py DESCRIPTION: Haupt-Chat-Interface (RAG & Interview). Enthält Intent-Router (Keywords/LLM) und Prompt-Construction. -VERSION: 2.7.0 (WP-22 Semantic Graph Routing) +VERSION: 2.7.1 (WP-22 Semantic Graph Routing) STATUS: Active +FIX: Umstellung auf llm.get_prompt() zur Behebung des 500 Server Errors (Dictionary replace crash). DEPENDENCIES: app.config, app.models.dto, app.services.llm_service, app.core.retriever, app.services.feedback_service EXTERNAL_CONFIG: config/decision_engine.yaml, config/types.yaml """ @@ -199,7 +200,8 @@ async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]: # 3. SLOW PATH: LLM Router if settings.get("llm_fallback_enabled", False): - router_prompt_template = llm.prompts.get("router_prompt", "") + # FIX: Nutze get_prompt statt direktem Zugriff auf dict + router_prompt_template = llm.get_prompt("router_prompt") if router_prompt_template: prompt = router_prompt_template.replace("{query}", query) @@ -262,7 +264,8 @@ async def chat_endpoint( logger.info(f"[{query_id}] Interview Type: {target_type}. Fields: {len(fields_list)}") fields_str = "\n- " + "\n- ".join(fields_list) - template = llm.prompts.get(prompt_key, "") + # FIX: Nutze get_prompt() zur Auflösung der provider-spezifischen Templates + template = llm.get_prompt(prompt_key) final_prompt = template.replace("{context_str}", "Dialogverlauf...") \ .replace("{query}", request.message) \ .replace("{target_type}", target_type) \ @@ -276,7 +279,6 @@ async def chat_endpoint( prepend_instr = strategy.get("prepend_instruction", "") # --- WP-22: Semantic Graph Routing (Teil C) --- - # Wir laden die konfigurierten Edge-Boosts für diesen Intent edge_boosts = strategy.get("edge_boosts", {}) if edge_boosts: logger.info(f"[{query_id}] Applying Edge Boosts: {edge_boosts}") @@ -286,7 +288,6 @@ async def chat_endpoint( mode="hybrid", top_k=request.top_k, explain=request.explain, - # WP-22: Boosts an den Retriever weitergeben boost_edges=edge_boosts ) retrieve_result = await retriever.search(query_req) @@ -299,7 +300,6 @@ async def chat_endpoint( top_k=3, filters={"type": inject_types}, explain=False, - # WP-22: Boosts auch hier anwenden (Konsistenz) boost_edges=edge_boosts ) strategy_result = await retriever.search(strategy_req) @@ -313,8 +313,12 @@ async def chat_endpoint( else: context_str = _build_enriched_context(hits) - template = llm.prompts.get(prompt_key, "{context_str}\n\n{query}") + # FIX: Nutze get_prompt() zur Auflösung der provider-spezifischen Templates + template = llm.get_prompt(prompt_key) + if not template: + template = "{context_str}\n\n{query}" + if prepend_instr: context_str = f"{prepend_instr}\n\n{context_str}" @@ -322,7 +326,7 @@ async def chat_endpoint( sources_hits = hits # --- GENERATION --- - system_prompt = llm.prompts.get("system_prompt", "") + system_prompt = llm.get_prompt("system_prompt") # Chat nutzt IMMER realtime priority answer_text = await llm.generate_raw_response( diff --git a/app/routers/ingest.py b/app/routers/ingest.py index cfac79d..158015f 100644 --- a/app/routers/ingest.py +++ b/app/routers/ingest.py @@ -1,8 +1,9 @@ """ FILE: app/routers/ingest.py DESCRIPTION: Endpunkte für WP-11. Nimmt Markdown entgegen. -Refactored für WP-14: Nutzt BackgroundTasks für non-blocking Save. -VERSION: 0.7.0 (Fix: Timeout WP-14) + Refactored für WP-14: Nutzt BackgroundTasks für non-blocking Save. + Update WP-20: Unterstützung für Hybrid-Cloud-Analyse Feedback. +VERSION: 0.8.0 (WP-20 Hybrid Ready) STATUS: Active DEPENDENCIES: app.core.ingestion, app.services.discovery, fastapi, pydantic """ @@ -44,6 +45,7 @@ class SaveResponse(BaseModel): async def run_ingestion_task(markdown_content: str, filename: str, vault_root: str, folder: str): """ Führt die Ingestion im Hintergrund aus, damit der Request nicht blockiert. + Integrierter WP-20 Hybrid-Modus über den IngestionService. """ logger.info(f"🔄 Background Task started: Ingesting {filename}...") try: @@ -80,15 +82,17 @@ async def analyze_draft(req: AnalyzeRequest): async def save_note(req: SaveRequest, background_tasks: BackgroundTasks): """ WP-14 Fix: Startet Ingestion im Hintergrund (Fire & Forget). - Verhindert Timeouts bei aktiver Smart-Edge-Allocation (WP-15). + Verhindert Timeouts bei aktiver Smart-Edge-Allocation (WP-15) und Cloud-Hybrid-Modus (WP-20). """ try: vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault") abs_vault_root = os.path.abspath(vault_root) if not os.path.exists(abs_vault_root): - try: os.makedirs(abs_vault_root, exist_ok=True) - except: pass + try: + os.makedirs(abs_vault_root, exist_ok=True) + except Exception as e: + logger.warning(f"Could not create vault root: {e}") final_filename = req.filename or f"draft_{int(time.time())}.md" @@ -109,7 +113,7 @@ async def save_note(req: SaveRequest, background_tasks: BackgroundTasks): status="queued", file_path=os.path.join(req.folder, final_filename), note_id="pending", - message="Speicherung & KI-Analyse im Hintergrund gestartet.", + message="Speicherung & Hybrid-KI-Analyse (WP-20) im Hintergrund gestartet.", stats={ "chunks": -1, # Indikator für Async "edges": -1 diff --git a/app/services/edge_registry.py b/app/services/edge_registry.py index 2859baa..95be97b 100644 --- a/app/services/edge_registry.py +++ b/app/services/edge_registry.py @@ -1,8 +1,11 @@ """ FILE: app/services/edge_registry.py DESCRIPTION: Single Source of Truth für Kanten-Typen mit dynamischem Reload. - WP-22: Transparente Status-Meldungen für Dev-Umgebungen. -VERSION: 0.7.2 (Fix: Restore Console Visibility & Entry Counts) + WP-22: Fix für absolute Pfade außerhalb des Vaults (Prod-Dictionary). + WP-20: Synchronisation mit zentralen Settings (v0.6.2). +VERSION: 0.7.5 +STATUS: Active +DEPENDENCIES: re, os, json, logging, time, app.config """ import re import os @@ -17,7 +20,6 @@ logger = logging.getLogger(__name__) class EdgeRegistry: _instance = None - # System-Kanten, die NIEMALS manuell im Markdown stehen dürfen FORBIDDEN_SYSTEM_EDGES = {"next", "prev", "belongs_to"} def __new__(cls, *args, **kwargs): @@ -26,58 +28,49 @@ class EdgeRegistry: cls._instance.initialized = False return cls._instance - def __init__(self, vault_root: Optional[str] = None): + def __init__(self): if self.initialized: return settings = get_settings() - env_vocab_path = os.getenv("MINDNET_VOCAB_PATH") - env_vault_root = os.getenv("MINDNET_VAULT_ROOT") or getattr(settings, "MINDNET_VAULT_ROOT", "./vault") - # Pfad-Priorität: 1. ENV -> 2. _system/dictionary -> 3. 01_User_Manual - if env_vocab_path: - self.full_vocab_path = os.path.abspath(env_vocab_path) - else: - possible_paths = [ - os.path.join(env_vault_root, "_system", "dictionary", "edge_vocabulary.md"), - os.path.join(env_vault_root, "01_User_Manual", "01_edge_vocabulary.md") - ] - self.full_vocab_path = None - for p in possible_paths: - if os.path.exists(p): - self.full_vocab_path = os.path.abspath(p) - break - - if not self.full_vocab_path: - self.full_vocab_path = os.path.abspath(possible_paths[0]) - + # 1. Pfad aus den zentralen Settings laden (WP-20 Synchronisation) + # Priorisiert den Pfad aus der .env / config.py (v0.6.2) + self.full_vocab_path = os.path.abspath(settings.MINDNET_VOCAB_PATH) + self.unknown_log_path = "data/logs/unknown_edges.jsonl" self.canonical_map: Dict[str, str] = {} self.valid_types: Set[str] = set() self._last_mtime = 0.0 - # Initialer Lade-Versuch mit Konsolen-Feedback - print(f"\n>>> [EDGE-REGISTRY] Initializing with Path: {self.full_vocab_path}", flush=True) + # Initialer Ladevorgang + logger.info(f">>> [EDGE-REGISTRY] Initializing with Path: {self.full_vocab_path}") self.ensure_latest() self.initialized = True def ensure_latest(self): - """Prüft den Zeitstempel und lädt bei Bedarf neu.""" + """ + Prüft den Zeitstempel der Vokabular-Datei und lädt bei Bedarf neu. + Verhindert den AttributeError in der Ingestion-Pipeline. + """ if not os.path.exists(self.full_vocab_path): - print(f"!!! [EDGE-REGISTRY ERROR] File not found: {self.full_vocab_path} !!!", flush=True) + logger.error(f"!!! [EDGE-REGISTRY ERROR] File not found: {self.full_vocab_path} !!!") return - current_mtime = os.path.getmtime(self.full_vocab_path) - if current_mtime > self._last_mtime: - self._load_vocabulary() - self._last_mtime = current_mtime + try: + current_mtime = os.path.getmtime(self.full_vocab_path) + if current_mtime > self._last_mtime: + self._load_vocabulary() + self._last_mtime = current_mtime + except Exception as e: + logger.error(f"!!! [EDGE-REGISTRY] Error checking file time: {e}") def _load_vocabulary(self): - """Parst das Wörterbuch und meldet die Anzahl der gelesenen Einträge.""" + """Parst das Markdown-Wörterbuch und baut die Canonical-Map auf.""" self.canonical_map.clear() self.valid_types.clear() - # Regex deckt | **canonical** | Aliase | ab + # Regex für Tabellen-Struktur: | **Typ** | Aliase | pattern = re.compile(r"\|\s*\*\*`?([a-zA-Z0-9_-]+)`?\*\*\s*\|\s*([^|]+)\|") try: @@ -96,46 +89,48 @@ class EdgeRegistry: if aliases_str and "Kein Alias" not in aliases_str: aliases = [a.strip() for a in aliases_str.split(",") if a.strip()] for alias in aliases: - # Normalisierung: Kleinschreibung und Unterstriche + # Normalisierung: Kleinschreibung, Underscores statt Leerzeichen clean_alias = alias.replace("`", "").lower().strip().replace(" ", "_") self.canonical_map[clean_alias] = canonical c_aliases += 1 - # Erfolgskontrolle für das Dev-Terminal - print(f"=== [EDGE-REGISTRY SUCCESS] Loaded {c_types} Canonical Types and {c_aliases} Aliases ===", flush=True) - logger.info(f"Registry reloaded from {self.full_vocab_path}") + logger.info(f"=== [EDGE-REGISTRY SUCCESS] Loaded {c_types} Canonical Types and {c_aliases} Aliases ===") except Exception as e: - print(f"!!! [EDGE-REGISTRY FATAL] Error reading file: {e} !!!", flush=True) - logger.error(f"Error reading vocabulary: {e}") + logger.error(f"!!! [EDGE-REGISTRY FATAL] Error reading file: {e} !!!") def resolve(self, edge_type: str, provenance: str = "explicit", context: dict = None) -> str: - """Validierung mit Fundort-Logging.""" + """ + Validiert einen Kanten-Typ gegen das Vokabular. + Loggt unbekannte Typen für die spätere manuelle Pflege. + """ self.ensure_latest() - if not edge_type: return "related_to" + if not edge_type: + return "related_to" + # Normalisierung des Typs clean_type = edge_type.lower().strip().replace(" ", "_").replace("-", "_") ctx = context or {} - # 1. Schutz der Systemkanten (Verbot für manuelle Nutzung) + # System-Kanten dürfen nicht manuell vergeben werden if provenance == "explicit" and clean_type in self.FORBIDDEN_SYSTEM_EDGES: self._log_issue(clean_type, "forbidden_system_usage", ctx) return "related_to" - # 2. Akzeptanz interner Strukturkanten + # System-Kanten sind nur bei struktureller Provenienz erlaubt if provenance == "structure" and clean_type in self.FORBIDDEN_SYSTEM_EDGES: return clean_type - # 3. Mapping via Wörterbuch + # Mapping auf kanonischen Namen if clean_type in self.canonical_map: return self.canonical_map[clean_type] - # 4. Unbekannte Kante + # Fallback und Logging self._log_issue(clean_type, "unknown_type", ctx) return clean_type def _log_issue(self, edge_type: str, error_kind: str, ctx: dict): - """Detailliertes JSONL-Logging für Debugging.""" + """Detailliertes JSONL-Logging für die Vokabular-Optimierung.""" try: os.makedirs(os.path.dirname(self.unknown_log_path), exist_ok=True) entry = { @@ -148,6 +143,8 @@ class EdgeRegistry: } with open(self.unknown_log_path, "a", encoding="utf-8") as f: f.write(json.dumps(entry) + "\n") - except Exception: pass + except Exception: + pass +# Singleton Export registry = EdgeRegistry() \ No newline at end of file diff --git a/app/services/llm_service.py b/app/services/llm_service.py index cff8880..9de2d89 100644 --- a/app/services/llm_service.py +++ b/app/services/llm_service.py @@ -1,147 +1,312 @@ """ FILE: app/services/llm_service.py -DESCRIPTION: Asynchroner Client für Ollama. Verwaltet Prompts und Background-Last (Semaphore). -VERSION: 2.8.0 +DESCRIPTION: Hybrid-Client für Ollama, Google GenAI (Gemini) und OpenRouter. + Verwaltet provider-spezifische Prompts und Background-Last. + WP-20: Optimiertes Fallback-Management zum Schutz von Cloud-Quoten. + WP-20 Fix: Bulletproof Prompt-Auflösung für format() Aufrufe. + WP-22/JSON: Optionales JSON-Schema + strict (für OpenRouter structured outputs). + FIX: Intelligente Rate-Limit Erkennung (429 Handling), v1-API Sync & Timeouts. +VERSION: 3.3.6 STATUS: Active -DEPENDENCIES: httpx, yaml, asyncio, app.config -EXTERNAL_CONFIG: config/prompts.yaml -LAST_ANALYSIS: 2025-12-15 +DEPENDENCIES: httpx, yaml, logging, asyncio, json, google-genai, openai, app.config """ - import httpx import yaml import logging -import os import asyncio +import json +from google import genai +from google.genai import types +from openai import AsyncOpenAI # Für OpenRouter (OpenAI-kompatibel) from pathlib import Path from typing import Optional, Dict, Any, Literal +from app.config import get_settings logger = logging.getLogger(__name__) -class Settings: - OLLAMA_URL = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434") - LLM_TIMEOUT = float(os.getenv("MINDNET_LLM_TIMEOUT", 300.0)) - LLM_MODEL = os.getenv("MINDNET_LLM_MODEL", "phi3:mini") - PROMPTS_PATH = os.getenv("MINDNET_PROMPTS_PATH", "./config/prompts.yaml") - - # NEU: Konfigurierbares Limit für Hintergrund-Last - # Default auf 2 (konservativ), kann in .env erhöht werden. - BACKGROUND_LIMIT = int(os.getenv("MINDNET_LLM_BACKGROUND_LIMIT", "2")) - -def get_settings(): - return Settings() class LLMService: - # GLOBALER SEMAPHOR (Lazy Initialization) - # Wir initialisieren ihn erst, wenn wir die Settings kennen. + # GLOBALER SEMAPHOR für Hintergrund-Last Steuerung (WP-06) _background_semaphore = None def __init__(self): self.settings = get_settings() self.prompts = self._load_prompts() - - # Initialisiere Semaphore einmalig auf Klassen-Ebene basierend auf Config + + # Initialisiere Semaphore einmalig auf Klassen-Ebene if LLMService._background_semaphore is None: - limit = self.settings.BACKGROUND_LIMIT + limit = getattr(self.settings, "BACKGROUND_LIMIT", 2) logger.info(f"🚦 LLMService: Initializing Background Semaphore with limit: {limit}") LLMService._background_semaphore = asyncio.Semaphore(limit) - - self.timeout = httpx.Timeout(self.settings.LLM_TIMEOUT, connect=10.0) - - self.client = httpx.AsyncClient( - base_url=self.settings.OLLAMA_URL, - timeout=self.timeout + + # 1. Lokaler Ollama Client + self.ollama_client = httpx.AsyncClient( + base_url=self.settings.OLLAMA_URL, + timeout=httpx.Timeout(self.settings.LLM_TIMEOUT) ) + # 2. Google GenAI Client (Modern SDK) + self.google_client = None + if self.settings.GOOGLE_API_KEY: + # FIX: Wir erzwingen api_version 'v1' für höhere Stabilität bei 2.5er Modellen. + self.google_client = genai.Client( + api_key=self.settings.GOOGLE_API_KEY, + http_options={'api_version': 'v1'} + ) + logger.info("✨ LLMService: Google GenAI (Gemini) active.") + + # 3. OpenRouter Client + self.openrouter_client = None + if self.settings.OPENROUTER_API_KEY: + self.openrouter_client = AsyncOpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=self.settings.OPENROUTER_API_KEY, + # Strikter Timeout für OpenRouter Free-Tier zur Vermeidung von Hangs. + timeout=45.0 + ) + logger.info("🛰️ LLMService: OpenRouter Integration active.") + def _load_prompts(self) -> dict: + """Lädt die Prompt-Konfiguration aus der YAML-Datei.""" path = Path(self.settings.PROMPTS_PATH) - if not path.exists(): return {} + if not path.exists(): + logger.error(f"❌ Prompts file not found at {path}") + return {} try: - with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) + with open(path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) or {} except Exception as e: - logger.error(f"Failed to load prompts: {e}") + logger.error(f"❌ Failed to load prompts: {e}") return {} + def get_prompt(self, key: str, provider: str = None) -> str: + """ + Hole provider-spezifisches Template mit intelligenter Text-Kaskade. + HINWEIS: Dies ist nur ein Text-Lookup und verbraucht kein API-Kontingent. + Kaskade: Gewählter Provider -> Gemini (Cloud-Stil) -> Ollama (Basis-Stil). + + WP-20 Fix: Garantiert die Rückgabe eines Strings, um AttributeError zu vermeiden. + """ + active_provider = provider or self.settings.MINDNET_LLM_PROVIDER + data = self.prompts.get(key, "") + + if isinstance(data, dict): + # Wir versuchen erst den Provider, dann Gemini, dann Ollama + val = data.get(active_provider, data.get("gemini", data.get("ollama", ""))) + + # Falls val durch YAML-Fehler immer noch ein Dict ist, extrahiere ersten String + if isinstance(val, dict): + logger.warning(f"⚠️ [LLMService] Nested dictionary detected for key '{key}'. Using first entry.") + val = next(iter(val.values()), "") if val else "" + return str(val) + + return str(data) + async def generate_raw_response( - self, - prompt: str, - system: str = None, + self, + prompt: str, + system: str = None, force_json: bool = False, - max_retries: int = 0, + max_retries: int = 2, base_delay: float = 2.0, - priority: Literal["realtime", "background"] = "realtime" + priority: Literal["realtime", "background"] = "realtime", + provider: Optional[str] = None, + model_override: Optional[str] = None, + json_schema: Optional[Dict[str, Any]] = None, + json_schema_name: str = "mindnet_json", + strict_json_schema: bool = True ) -> str: """ - Führt einen LLM Call aus. - priority="realtime": Chat (Sofort, keine Bremse). - priority="background": Import/Analyse (Gedrosselt durch Semaphore). + Haupteinstiegspunkt für LLM-Anfragen mit Priorisierung. + + force_json: + - Ollama: nutzt payload["format"]="json" + - Gemini: nutzt response_mime_type="application/json" + - OpenRouter: nutzt response_format=json_object (Fallback) oder json_schema """ - - use_semaphore = (priority == "background") - - if use_semaphore and LLMService._background_semaphore: - async with LLMService._background_semaphore: - return await self._execute_request(prompt, system, force_json, max_retries, base_delay) - else: - # Realtime oder Fallback (falls Semaphore Init fehlschlug) - return await self._execute_request(prompt, system, force_json, max_retries, base_delay) + target_provider = provider or self.settings.MINDNET_LLM_PROVIDER - async def _execute_request(self, prompt, system, force_json, max_retries, base_delay): - payload: Dict[str, Any] = { + if priority == "background": + async with LLMService._background_semaphore: + return await self._dispatch( + target_provider, prompt, system, force_json, + max_retries, base_delay, model_override, + json_schema, json_schema_name, strict_json_schema + ) + + return await self._dispatch( + target_provider, prompt, system, force_json, + max_retries, base_delay, model_override, + json_schema, json_schema_name, strict_json_schema + ) + + async def _dispatch( + self, + provider: str, + prompt: str, + system: Optional[str], + force_json: bool, + max_retries: int, + base_delay: float, + model_override: Optional[str], + json_schema: Optional[Dict[str, Any]], + json_schema_name: str, + strict_json_schema: bool + ) -> str: + """ + Routet die Anfrage mit intelligenter Rate-Limit Erkennung (WP-20 + WP-76). + Schleife läuft über MINDNET_LLM_RATE_LIMIT_RETRIES. + """ + rate_limit_attempts = 0 + max_rate_retries = getattr(self.settings, "LLM_RATE_LIMIT_RETRIES", 3) + wait_time = getattr(self.settings, "LLM_RATE_LIMIT_WAIT", 60.0) + + while rate_limit_attempts <= max_rate_retries: + try: + if provider == "openrouter" and self.openrouter_client: + return await self._execute_openrouter( + prompt=prompt, + system=system, + force_json=force_json, + model_override=model_override, + json_schema=json_schema, + json_schema_name=json_schema_name, + strict_json_schema=strict_json_schema + ) + + if provider == "gemini" and self.google_client: + return await self._execute_google(prompt, system, force_json, model_override) + + # Default/Fallback zu Ollama + return await self._execute_ollama(prompt, system, force_json, max_retries, base_delay) + + except Exception as e: + err_str = str(e) + # Intelligente 429 Erkennung für alle Cloud-Provider + is_rate_limit = any(x in err_str for x in ["429", "RESOURCE_EXHAUSTED", "rate_limited", "Too Many Requests"]) + + if is_rate_limit and rate_limit_attempts < max_rate_retries: + rate_limit_attempts += 1 + logger.warning( + f"⏳ [LLMService] Rate Limit (429) detected from {provider}. " + f"Attempt {rate_limit_attempts}/{max_rate_retries}. " + f"Waiting {wait_time}s before cloud retry..." + ) + await asyncio.sleep(wait_time) + continue # Nächster Versuch in der Cloud-Schleife + + # Wenn kein Rate-Limit oder Retries erschöpft -> Fallback zu Ollama (falls aktiviert) + if self.settings.LLM_FALLBACK_ENABLED and provider != "ollama": + logger.warning( + f"🔄 Provider {provider} failed ({err_str}). Falling back to LOCAL OLLAMA." + ) + return await self._execute_ollama(prompt, system, force_json, max_retries, base_delay) + raise e + + async def _execute_google(self, prompt, system, force_json, model_override): + """Native Google SDK Integration (Gemini) mit v1 Fix.""" + model = model_override or self.settings.GEMINI_MODEL + # Fix: Bereinige Modellnamen (Entfernung von 'models/' Präfix) + clean_model = model.replace("models/", "") + + config = types.GenerateContentConfig( + system_instruction=system, + response_mime_type="application/json" if force_json else "text/plain" + ) + # Thread-Offloading mit striktem Timeout gegen "Hangs" + response = await asyncio.wait_for( + asyncio.to_thread( + self.google_client.models.generate_content, + model=clean_model, contents=prompt, config=config + ), + timeout=45.0 + ) + return response.text.strip() + + async def _execute_openrouter( + self, + prompt: str, + system: Optional[str], + force_json: bool, + model_override: Optional[str], + json_schema: Optional[Dict[str, Any]] = None, + json_schema_name: str = "mindnet_json", + strict_json_schema: bool = True + ) -> str: + """OpenRouter API Integration (OpenAI-kompatibel) mit Schema-Support.""" + model = model_override or self.settings.OPENROUTER_MODEL + messages = [] + if system: + messages.append({"role": "system", "content": system}) + messages.append({"role": "user", "content": prompt}) + + kwargs: Dict[str, Any] = {} + if force_json: + if json_schema: + kwargs["response_format"] = { + "type": "json_schema", + "json_schema": { + "name": json_schema_name, + "strict": strict_json_schema, + "schema": json_schema + } + } + else: + kwargs["response_format"] = {"type": "json_object"} + + response = await self.openrouter_client.chat.completions.create( + model=model, + messages=messages, + **kwargs + ) + return response.choices[0].message.content.strip() + + async def _execute_ollama(self, prompt, system, force_json, max_retries, base_delay): + """Lokaler Ollama Call mit exponentiellem Backoff.""" + payload = { "model": self.settings.LLM_MODEL, "prompt": prompt, "stream": False, "options": { - "temperature": 0.1 if force_json else 0.7, - "num_ctx": 8192 + "temperature": 0.1 if force_json else 0.7, + "num_ctx": 8192 } } - if force_json: - payload["format"] = "json" - + payload["format"] = "json" if system: payload["system"] = system attempt = 0 - while True: try: - response = await self.client.post("/api/generate", json=payload) - - if response.status_code == 200: - data = response.json() - return data.get("response", "").strip() - else: - response.raise_for_status() - + res = await self.ollama_client.post("/api/generate", json=payload) + res.raise_for_status() + return res.json().get("response", "").strip() except Exception as e: attempt += 1 if attempt > max_retries: - logger.error(f"LLM Final Error (Versuch {attempt}): {e}") - raise e - + logger.error(f"❌ Ollama Error after {attempt} retries: {e}") + raise e wait_time = base_delay * (2 ** (attempt - 1)) - logger.warning(f"⚠️ LLM Retry ({attempt}/{max_retries}) in {wait_time}s: {e}") + logger.warning(f"⚠️ Ollama attempt {attempt} failed. Retrying in {wait_time}s...") await asyncio.sleep(wait_time) async def generate_rag_response(self, query: str, context_str: str) -> str: - """ - Chat-Wrapper: Immer Realtime. - """ - system_prompt = self.prompts.get("system_prompt", "") - rag_template = self.prompts.get("rag_template", "{context_str}\n\n{query}") - + """Vollständiges RAG Chat-Interface.""" + provider = self.settings.MINDNET_LLM_PROVIDER + system_prompt = self.get_prompt("system_prompt", provider) + rag_template = self.get_prompt("rag_template", provider) + final_prompt = rag_template.format(context_str=context_str, query=query) - + return await self.generate_raw_response( - final_prompt, - system=system_prompt, - max_retries=0, - force_json=False, + final_prompt, + system=system_prompt, priority="realtime" ) async def close(self): - if self.client: - await self.client.aclose() \ No newline at end of file + """Schließt die HTTP-Verbindungen.""" + if self.ollama_client: + await self.ollama_client.aclose() \ No newline at end of file diff --git a/app/services/semantic_analyzer.py b/app/services/semantic_analyzer.py index 24ca205..2d492a5 100644 --- a/app/services/semantic_analyzer.py +++ b/app/services/semantic_analyzer.py @@ -1,19 +1,24 @@ """ FILE: app/services/semantic_analyzer.py DESCRIPTION: KI-gestützte Kanten-Validierung. Nutzt LLM (Background-Priority), um Kanten präzise einem Chunk zuzuordnen. -VERSION: 2.1.0 (Fix: Strict Edge String Validation against LLM Hallucinations) + WP-20 Fix: Volle Kompatibilität mit der provider-basierten Routing-Logik (OpenRouter Primary). + WP-22: Integration von valid_types zur Halluzinations-Vermeidung. +FIX: Mistral-sicheres JSON-Parsing ( & [OUT] Handling) und 100% Logik-Erhalt. +VERSION: 2.2.6 STATUS: Active -DEPENDENCIES: app.services.llm_service, json, logging -LAST_ANALYSIS: 2025-12-16 +DEPENDENCIES: app.services.llm_service, app.services.edge_registry, json, logging, re """ import json import logging -from typing import List, Optional +import re +from typing import List, Optional, Any from dataclasses import dataclass # Importe from app.services.llm_service import LLMService +# WP-22: Registry für Vokabular-Erzwingung +from app.services.edge_registry import registry as edge_registry logger = logging.getLogger(__name__) @@ -24,7 +29,7 @@ class SemanticAnalyzer: def _is_valid_edge_string(self, edge_str: str) -> bool: """ Prüft, ob ein String eine valide Kante im Format 'kind:target' ist. - Verhindert, dass LLM-Geschwätz ("Here is the list: ...") als Kante durchrutscht. + Verhindert, dass LLM-Geschwätz als Kante durchrutscht. """ if not isinstance(edge_str, str) or ":" not in edge_str: return False @@ -34,12 +39,10 @@ class SemanticAnalyzer: target = parts[1].strip() # Regel 1: Ein 'kind' (Beziehungstyp) darf keine Leerzeichen enthalten. - # Erlaubt: "derived_from", "related_to" - # Verboten: "derived end of instruction", "Here is the list" if " " in kind: return False - # Regel 2: Plausible Länge für den Typ + # Regel 2: Plausible Länge für den Typ (Vermeidet Sätze als Typ) if len(kind) > 40 or len(kind) < 2: return False @@ -49,24 +52,61 @@ class SemanticAnalyzer: return True + def _extract_json_safely(self, text: str) -> Any: + """ + Extrahiert JSON-Daten und bereinigt LLM-Steuerzeichen (Mistral/Llama). + Implementiert robuste Recovery-Logik für Cloud-Provider. + """ + if not text: + return [] + + # 1. Entferne Mistral/Llama Steuerzeichen und Tags + clean = text.replace("", "").replace("", "") + clean = clean.replace("[OUT]", "").replace("[/OUT]", "") + clean = clean.strip() + + # 2. Suche nach Markdown JSON-Blöcken + match = re.search(r"```(?:json)?\s*(.*?)\s*```", clean, re.DOTALL) + payload = match.group(1) if match else clean + + try: + return json.loads(payload.strip()) + except json.JSONDecodeError: + # 3. Recovery: Suche nach der ersten [ und letzten ] + start = payload.find('[') + end = payload.rfind(']') + 1 + if start != -1 and end > start: + try: + return json.loads(payload[start:end]) + except: pass + + # 4. Zweite Recovery: Suche nach der ersten { und letzten } + start_obj = payload.find('{') + end_obj = payload.rfind('}') + 1 + if start_obj != -1 and end_obj > start_obj: + try: + return json.loads(payload[start_obj:end_obj]) + except: pass + return [] + 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. Das LLM filtert heraus, welche Kanten für diesen Chunk relevant sind. - - Features: - - Retry Strategy: Wartet bei Überlastung (max_retries=5). - - Priority Queue: Läuft als "background" Task, um den Chat nicht zu blockieren. - - Observability: Loggt Input-Größe, Raw-Response und Parsing-Details. + WP-20: Nutzt primär den konfigurierten Provider (z.B. OpenRouter). """ if not all_edges: return [] - # 1. Prompt laden - prompt_template = self.llm.prompts.get("edge_allocation_template") + # 1. Bestimmung des Providers und Modells (Dynamisch über Settings) + provider = self.llm.settings.MINDNET_LLM_PROVIDER + model = self.llm.settings.OPENROUTER_MODEL if provider == "openrouter" else self.llm.settings.GEMINI_MODEL + + # 2. Prompt laden (Provider-spezifisch via get_prompt) + prompt_template = self.llm.get_prompt("edge_allocation_template", provider) - if not prompt_template: - logger.warning("⚠️ [SemanticAnalyzer] Prompt 'edge_allocation_template' fehlt. Nutze Fallback.") + if not prompt_template or not isinstance(prompt_template, str): + logger.warning("⚠️ [SemanticAnalyzer] Prompt 'edge_allocation_template' ungültig. Nutze Recovery-Template.") prompt_template = ( "TASK: Wähle aus den Kandidaten die relevanten Kanten für den Text.\n" "TEXT: {chunk_text}\n" @@ -74,101 +114,83 @@ class SemanticAnalyzer: "OUTPUT: JSON Liste von Strings [\"kind:target\"]." ) - # 2. Kandidaten-Liste formatieren + # 3. Daten für Template vorbereiten (Vokabular-Check) + edge_registry.ensure_latest() + valid_types_str = ", ".join(sorted(list(edge_registry.valid_types))) 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.") - # 3. Prompt füllen - final_prompt = prompt_template.format( - chunk_text=chunk_text[:3500], - edge_list=edges_str - ) + # 4. Prompt füllen mit Format-Check (Kein Shortcut) + try: + # Wir begrenzen den Text auf eine vernünftige Länge für das Kontextfenster + final_prompt = prompt_template.format( + chunk_text=chunk_text[:6000], + edge_list=edges_str, + valid_types=valid_types_str + ) + except Exception as format_err: + logger.error(f"❌ [SemanticAnalyzer] Prompt Formatting failed: {format_err}") + return [] try: - # 4. LLM Call mit Traffic Control + # 5. LLM Call mit Background Priority & Semaphore Control response_json = await self.llm.generate_raw_response( prompt=final_prompt, force_json=True, - max_retries=5, - base_delay=5.0, - priority="background" + max_retries=3, + base_delay=2.0, + priority="background", + provider=provider, + model_override=model ) - # LOG: Raw Response Preview - logger.debug(f"📥 [SemanticAnalyzer] Raw Response (Preview): {response_json[:200]}...") - - # 5. Parsing & Cleaning - clean_json = response_json.replace("```json", "").replace("```", "").strip() + # 6. Mistral-sicheres JSON Parsing via Helper + data = self._extract_json_safely(response_json) - if not clean_json: - logger.warning("⚠️ [SemanticAnalyzer] Leere Antwort vom LLM erhalten. Trigger Fallback.") + if not data: return [] - try: - data = json.loads(clean_json) - except json.JSONDecodeError as json_err: - logger.error(f"❌ [SemanticAnalyzer] JSON Decode Error.") - logger.error(f" Grund: {json_err}") - logger.error(f" Empfangener String: {clean_json[:500]}") - logger.info(" -> Workaround: Fallback auf 'Alle Kanten' (durch Chunker).") - return [] - - valid_edges = [] - - # 6. Robuste Validierung (List vs Dict) - # Wir sammeln erst alle Strings ein + # 7. Robuste Normalisierung (List vs Dict Recovery) raw_candidates = [] - if isinstance(data, list): raw_candidates = data - elif isinstance(data, dict): - logger.info(f"ℹ️ [SemanticAnalyzer] LLM lieferte Dict statt Liste. Versuche Reparatur. Keys: {list(data.keys())}") - for key, val in data.items(): - # Fall A: {"edges": ["kind:target"]} - if key.lower() in ["edges", "results", "kanten", "matches"] and isinstance(val, list): - raw_candidates.extend(val) - - # Fall B: {"kind": "target"} (Beziehung als Key) - elif isinstance(val, str): - raw_candidates.append(f"{key}:{val}") - - # Fall C: {"kind": ["target1", "target2"]} - elif isinstance(val, list): - for target in val: - if isinstance(target, str): - raw_candidates.append(f"{key}:{target}") + logger.info(f"ℹ️ [SemanticAnalyzer] LLM returned dict, trying recovery.") + for key in ["edges", "results", "kanten", "matches"]: + if key in data and isinstance(data[key], list): + raw_candidates.extend(data[key]) + break + # Falls immer noch leer, nutze Schlüssel-Wert Paare als Behelf + if not raw_candidates: + for k, v in data.items(): + if isinstance(v, str): raw_candidates.append(f"{k}:{v}") + elif isinstance(v, list): + for target in v: + if isinstance(target, str): raw_candidates.append(f"{k}:{target}") - # 7. Strict Validation Loop + # 8. Strikte Validierung gegen Kanten-Format + valid_edges = [] for e in raw_candidates: - e_str = str(e) + e_str = str(e).strip() if self._is_valid_edge_string(e_str): valid_edges.append(e_str) else: - logger.debug(f" [SemanticAnalyzer] Invalid edge format rejected: '{e_str}'") + logger.debug(f" [SemanticAnalyzer] Rejected invalid edge format: '{e_str}'") - # Safety: Filtere nur Kanten, die halbwegs valide aussehen (Doppelcheck) - final_result = [e for e in valid_edges if ":" in e] - - # LOG: Ergebnis - if final_result: - logger.info(f"✅ [SemanticAnalyzer] Success. {len(final_result)} Kanten zugewiesen.") - else: - logger.debug(" [SemanticAnalyzer] Keine spezifischen Kanten erkannt (Empty Result).") - - return final_result + if valid_edges: + logger.info(f"✅ [SemanticAnalyzer] Assigned {len(valid_edges)} edges to chunk.") + return valid_edges except Exception as e: - logger.error(f"💥 [SemanticAnalyzer] Kritischer Fehler: {e}", exc_info=True) + logger.error(f"💥 [SemanticAnalyzer] Critical error during analysis: {e}", exc_info=True) return [] async def close(self): if self.llm: await self.llm.close() -# Singleton Helper +# Singleton Instanziierung _analyzer_instance = None def get_semantic_analyzer(): global _analyzer_instance diff --git a/config/decision_engine.yaml b/config/decision_engine.yaml index 0bb75b5..7274153 100644 --- a/config/decision_engine.yaml +++ b/config/decision_engine.yaml @@ -1,43 +1,50 @@ # config/decision_engine.yaml # Steuerung der Decision Engine (Intent Recognition & Graph Routing) -# Version: 2.5.0 (WP-22: Semantic Graph Routing) +# VERSION: 2.6.1 (WP-20: Hybrid LLM & WP-22: Semantic Graph Routing) +# STATUS: Active +# DoD: Keine Hardcoded Modelle, volle Integration der strategischen Boosts. -version: 2.5 +version: 2.6 settings: llm_fallback_enabled: true - # Few-Shot Prompting für den LLM-Router (Slow Path) + # Strategie für den Router selbst (Welches Modell erkennt den Intent?) + # "auto" nutzt den in MINDNET_LLM_PROVIDER gesetzten Standard (z.B. openrouter). + router_provider: "auto" + + # Few-Shot Prompting für den LLM-Router llm_router_prompt: | - Du bist ein Klassifikator. Analysiere die Nachricht und wähle die passende Strategie. + Du bist der zentrale Intent-Klassifikator für Mindnet, einen digitalen Zwilling. + Analysiere die Nachricht und wähle die passende Strategie. Antworte NUR mit dem Namen der Strategie. STRATEGIEN: - INTERVIEW: User will Wissen erfassen, Notizen anlegen oder Dinge festhalten. - - DECISION: Rat, Strategie, Vor/Nachteile, "Soll ich". - - EMPATHY: Gefühle, Frust, Freude, Probleme. - - CODING: Code, Syntax, Programmierung. - - FACT: Wissen, Fakten, Definitionen. + - DECISION: Rat, Strategie, Abwägung von Werten, "Soll ich tun X?". + - EMPATHY: Gefühle, Reflexion der eigenen Verfassung, Frust, Freude. + - CODING: Code-Erstellung, Debugging, technische Dokumentation. + - FACT: Reine Wissensabfrage, Definitionen, Suchen von Informationen. BEISPIELE: - User: "Wie funktioniert Qdrant?" -> FACT - User: "Soll ich Qdrant nutzen?" -> DECISION - User: "Ich möchte etwas notieren" -> INTERVIEW - User: "Lass uns das festhalten" -> INTERVIEW - User: "Schreibe ein Python Script" -> CODING - User: "Alles ist grau und sinnlos" -> EMPATHY + User: "Wie funktioniert die Qdrant-Vektor-DB?" -> FACT + User: "Soll ich mein Startup jetzt verkaufen?" -> DECISION + User: "Notiere mir kurz meine Gedanken zum Meeting." -> INTERVIEW + User: "Ich fühle mich heute sehr erschöpft." -> EMPATHY + User: "Schreibe eine FastAPI-Route für den Ingest." -> CODING NACHRICHT: "{query}" STRATEGIE: strategies: - # 1. Fakten-Abfrage (Fallback & Default) + # 1. Fakten-Abfrage (Turbo-Modus via OpenRouter / Primary) FACT: description: "Reine Wissensabfrage." + preferred_provider: "openrouter" trigger_keywords: [] inject_types: [] - # WP-22: Definitionen & Hierarchien bevorzugen + # WP-22: Definitionen & Hierarchien im Graphen bevorzugen edge_boosts: part_of: 2.0 composed_of: 2.0 @@ -46,9 +53,10 @@ strategies: prompt_template: "rag_template" prepend_instruction: null - # 2. Entscheidungs-Frage + # 2. Entscheidungs-Frage (Power-Strategie via Gemini) DECISION: description: "Der User sucht Rat, Strategie oder Abwägung." + preferred_provider: "gemini" trigger_keywords: - "soll ich" - "meinung" @@ -59,21 +67,22 @@ strategies: - "abwägung" - "vergleich" inject_types: ["value", "principle", "goal", "risk"] - # WP-22: Risiken und Konsequenzen hervorheben + # WP-22: Risiken und Konsequenzen im Graphen priorisieren edge_boosts: blocks: 2.5 solves: 2.0 depends_on: 1.5 risk_of: 2.5 - impacts: 2.0 # NEU: Zeige mir alles, was von dieser Entscheidung betroffen ist! + impacts: 2.0 prompt_template: "decision_template" prepend_instruction: | - !!! ENTSCHEIDUNGS-MODUS !!! + !!! ENTSCHEIDUNGS-MODUS (HYBRID AI) !!! BITTE WÄGE FAKTEN GEGEN FOLGENDE WERTE, PRINZIPIEN UND ZIELE AB: - # 3. Empathie / "Ich"-Modus + # 3. Empathie / "Ich"-Modus (Lokal & Privat via Ollama) EMPATHY: description: "Reaktion auf emotionale Zustände." + preferred_provider: "ollama" trigger_keywords: - "ich fühle" - "traurig" @@ -84,7 +93,6 @@ strategies: - "überfordert" - "müde" inject_types: ["experience", "belief", "profile"] - # WP-22: Weiche Assoziationen & Erfahrungen stärken edge_boosts: based_on: 2.0 related_to: 2.0 @@ -93,9 +101,10 @@ strategies: prompt_template: "empathy_template" prepend_instruction: null - # 4. Coding / Technical + # 4. Coding / Technical (Gemini Power) CODING: description: "Technische Anfragen und Programmierung." + preferred_provider: "gemini" trigger_keywords: - "code" - "python" @@ -107,7 +116,7 @@ strategies: - "yaml" - "bash" inject_types: ["snippet", "reference", "source"] - # WP-22: Technische Abhängigkeiten + # WP-22: Technische Abhängigkeiten priorisieren edge_boosts: uses: 2.5 depends_on: 2.0 @@ -115,11 +124,10 @@ strategies: prompt_template: "technical_template" prepend_instruction: null - # 5. Interview / Datenerfassung - # HINWEIS: Spezifische Typen (Projekt, Ziel etc.) werden automatisch - # über die types.yaml erkannt. Hier stehen nur generische Trigger. + # 5. Interview / Datenerfassung (Lokal) INTERVIEW: description: "Der User möchte Wissen erfassen." + preferred_provider: "ollama" trigger_keywords: - "neue notiz" - "etwas notieren" @@ -134,13 +142,4 @@ strategies: inject_types: [] edge_boosts: {} prompt_template: "interview_template" - prepend_instruction: null - # Schemas: Hier nur der Fallback. - # Spezifische Schemas (Project, Experience) kommen jetzt aus types.yaml! - schemas: - default: - fields: - - "Titel" - - "Thema/Inhalt" - - "Tags" - hint: "Halte es einfach und übersichtlich." \ No newline at end of file + prepend_instruction: null \ No newline at end of file diff --git a/config/prod.env b/config/prod.env new file mode 100644 index 0000000..ae3f569 --- /dev/null +++ b/config/prod.env @@ -0,0 +1,48 @@ +# --- FastAPI Server (Produktion) --- +UVICORN_HOST=0.0.0.0 +UVICORN_PORT=8000 +DEBUG=false + +# --- Qdrant Vektor-Datenbank --- +# Trennung der Daten durch eigenes Prefix +QDRANT_URL=http://127.0.0.1:6333 +QDRANT_API_KEY= +COLLECTION_PREFIX=mindnet + +# --- Vektoren-Konfiguration --- +# Muss 768 für 'nomic-embed-text' sein +VECTOR_DIM=768 + +# --- AI Modelle (Lokal/Fallback) --- +MINDNET_LLM_MODEL=phi3:mini +MINDNET_OLLAMA_URL=http://127.0.0.1:11434 +MINDNET_LLM_TIMEOUT=300.0 +MINDNET_LLM_BACKGROUND_LIMIT=2 + +# Vektor-Modell für semantische Suche +MINDNET_EMBEDDING_MODEL=nomic-embed-text + +# --- WP-20/WP-76: Hybrid-Cloud & Resilienz --- +# Primärer Provider für höchste Qualität +MINDNET_LLM_PROVIDER=openrouter +MINDNET_LLM_FALLBACK=true + +# Intelligente Rate-Limit Steuerung (Sekunden/Versuche) +MINDNET_LLM_RATE_LIMIT_WAIT=60.0 +MINDNET_LLM_RATE_LIMIT_RETRIES=3 + +# --- Cloud Provider Keys (Hier Prod-Keys einsetzen) --- +GOOGLE_API_KEY=AIzaSy... (Dein Prod-Key) +MINDNET_GEMINI_MODEL=gemini-2.5-flash-lite + +OPENROUTER_API_KEY=sk-or-v1-... (Dein Prod-Key) +# Stabilstes Free-Modell für strukturierte Extraktion +OPENROUTER_MODEL=mistralai/mistral-7b-instruct:free + +# --- Pfade & System (Produktions-Vault) --- +MINDNET_TYPES_FILE=./config/types.yaml +MINDNET_VAULT_ROOT=./vault_prod +MINDNET_VOCAB_PATH=/mindnet/vault/mindnet/_system/dictionary/edge_vocabulary.md + +# Change Detection für effiziente Re-Imports +MINDNET_CHANGE_DETECTION_MODE=full \ No newline at end of file diff --git a/config/prompts.yaml b/config/prompts.yaml index 3a06df2..13b800d 100644 --- a/config/prompts.yaml +++ b/config/prompts.yaml @@ -1,4 +1,7 @@ -# config/prompts.yaml — Final V2.3.1 (Multi-Personality Support) +# config/prompts.yaml — Final V2.5.5 (OpenRouter Hardening) +# WP-20: Optimierte Cloud-Templates zur Unterdrückung von Modell-Geschwätz. +# FIX: Explizite Verbote für Einleitungstexte zur Vermeidung von JSON-Parsing-Fehlern. +# OLLAMA: UNVERÄNDERT laut Benutzeranweisung. system_prompt: | Du bist 'mindnet', mein digitaler Zwilling und strategischer Partner. @@ -15,148 +18,222 @@ system_prompt: | # --------------------------------------------------------- # 1. STANDARD: Fakten & Wissen (Intent: FACT) # --------------------------------------------------------- -rag_template: | - QUELLEN (WISSEN): - ========================================= - {context_str} - ========================================= +rag_template: + ollama: | + QUELLEN (WISSEN): + ========================================= + {context_str} + ========================================= - FRAGE: - {query} + FRAGE: + {query} - ANWEISUNG: - Beantworte die Frage präzise basierend auf den Quellen. - Fasse die Informationen zusammen. Sei objektiv und neutral. + ANWEISUNG: + Beantworte die Frage präzise basierend auf den Quellen. + Fasse die Informationen zusammen. Sei objektiv und neutral. + gemini: | + Kontext meines digitalen Zwillings: {context_str} + Beantworte strukturiert und präzise: {query} + openrouter: | + Kontext-Analyse für den digitalen Zwilling: + {context_str} + + Anfrage: {query} + Antworte basierend auf dem Kontext. # --------------------------------------------------------- # 2. DECISION: Strategie & Abwägung (Intent: DECISION) # --------------------------------------------------------- -decision_template: | - KONTEXT (FAKTEN & STRATEGIE): - ========================================= - {context_str} - ========================================= +decision_template: + ollama: | + KONTEXT (FAKTEN & STRATEGIE): + ========================================= + {context_str} + ========================================= - ENTSCHEIDUNGSFRAGE: - {query} + ENTSCHEIDUNGSFRAGE: + {query} - ANWEISUNG: - Du agierst als mein Entscheidungs-Partner. - 1. Analysiere die Faktenlage aus den Quellen. - 2. Prüfe dies hart gegen meine strategischen Notizen (Typ [VALUE], [PRINCIPLE], [GOAL]). - 3. Wäge ab: Passt die technische/faktische Lösung zu meinen Werten? - - FORMAT: - - **Analyse:** (Kurze Zusammenfassung der Fakten) - - **Abgleich:** (Gibt es Konflikte mit Werten/Zielen? Nenne die Quelle!) - - **Empfehlung:** (Klare Meinung: Ja/Nein/Vielleicht mit Begründung) + ANWEISUNG: + Du agierst als mein Entscheidungs-Partner. + 1. Analysiere die Faktenlage aus den Quellen. + 2. Prüfe dies hart gegen meine strategischen Notizen (Typ [VALUE], [PRINCIPLE], [GOAL]). + 3. Wäge ab: Passt die technische/faktische Lösung zu meinen Werten? + + FORMAT: + - **Analyse:** (Kurze Zusammenfassung der Fakten) + - **Abgleich:** (Gibt es Konflikte mit Werten/Zielen? Nenne die Quelle!) + - **Empfehlung:** (Klare Meinung: Ja/No/Vielleicht mit Begründung) + gemini: | + Agiere als strategischer Partner. Analysiere die Frage {query} basierend auf meinen Werten im Kontext {context_str}. + openrouter: | + Strategische Entscheidungsanalyse: {query} + Wertebasis aus dem Graphen: {context_str} # --------------------------------------------------------- # 3. EMPATHY: Der Spiegel / "Ich"-Modus (Intent: EMPATHY) # --------------------------------------------------------- -empathy_template: | - KONTEXT (ERFAHRUNGEN & GLAUBENSSÄTZE): - ========================================= - {context_str} - ========================================= +empathy_template: + ollama: | + KONTEXT (ERFAHRUNGEN & GLAUBENSSÄTZE): + ========================================= + {context_str} + ========================================= - SITUATION: - {query} + SITUATION: + {query} - ANWEISUNG: - Du agierst jetzt als mein empathischer Spiegel. - 1. Versuche nicht sofort, das Problem technisch zu lösen. - 2. Zeige Verständnis für die Situation basierend auf meinen eigenen Erfahrungen ([EXPERIENCE]) oder Glaubenssätzen ([BELIEF]), falls im Kontext vorhanden. - 3. Antworte in der "Ich"-Form oder "Wir"-Form. Sei unterstützend. - - TONFALL: - Ruhig, verständnisvoll, reflektiert. Keine Aufzählungszeichen, sondern fließender Text. + ANWEISUNG: + Du agierst jetzt als mein empathischer Spiegel. + 1. Versuche nicht sofort, das Problem technisch zu lösen. + 2. Zeige Verständnis für die Situation basierend auf meinen eigenen Erfahrungen ([EXPERIENCE]) oder Glaubenssätzen ([BELIEF]), falls im Kontext vorhanden. + 3. Antworte in der "Ich"-Form oder "Wir"-Form. Sei unterstützend. + + TONFALL: + Ruhig, verständnisvoll, reflektiert. Keine Aufzählungszeichen, sondern fließender Text. + gemini: "Sei mein digitaler Spiegel für {query}. Kontext: {context_str}" + openrouter: "Empathische Reflexion der Situation {query}. Persönlicher Kontext: {context_str}" # --------------------------------------------------------- # 4. TECHNICAL: Der Coder (Intent: CODING) # --------------------------------------------------------- -technical_template: | - KONTEXT (DOCS & SNIPPETS): - ========================================= - {context_str} - ========================================= +technical_template: + ollama: | + KONTEXT (DOCS & SNIPPETS): + ========================================= + {context_str} + ========================================= - TASK: - {query} + TASK: + {query} + + ANWEISUNG: + Du bist Senior Developer. + 1. Ignoriere Smalltalk. Komm sofort zum Punkt. + 2. Generiere validen, performanten Code basierend auf den Quellen. + 3. Wenn Quellen fehlen, nutze dein allgemeines Programmierwissen, aber weise darauf hin. + + FORMAT: + - Kurze Erklärung des Ansatzes. + - Markdown Code-Block (Copy-Paste fertig). + - Wichtige Edge-Cases. + gemini: "Generiere Code für {query} unter Berücksichtigung von {context_str}." + openrouter: "Technischer Support für {query}. Code-Referenzen: {context_str}" - ANWEISUNG: - Du bist Senior Developer. - 1. Ignoriere Smalltalk. Komm sofort zum Punkt. - 2. Generiere validen, performanten Code basierend auf den Quellen. - 3. Wenn Quellen fehlen, nutze dein allgemeines Programmierwissen, aber weise darauf hin. - - FORMAT: - - Kurze Erklärung des Ansatzes. - - Markdown Code-Block (Copy-Paste fertig). - - Wichtige Edge-Cases. # --------------------------------------------------------- # 5. INTERVIEW: Der "One-Shot Extractor" (Performance Mode) # --------------------------------------------------------- -interview_template: | - TASK: - Du bist ein professioneller Ghostwriter. Verwandle den "USER INPUT" in eine strukturierte Notiz vom Typ '{target_type}'. - - STRUKTUR (Nutze EXAKT diese Überschriften): - {schema_fields} - - USER INPUT: - "{query}" - - ANWEISUNG ZUM INHALT: - 1. Analysiere den Input genau. - 2. Schreibe die Inhalte unter die passenden Überschriften aus der STRUKTUR-Liste oben. - 3. STIL: Schreibe flüssig, professionell und in der Ich-Perspektive. Korrigiere Grammatikfehler, aber behalte den persönlichen Ton bei. - 4. Wenn Informationen für einen Abschnitt fehlen, schreibe nur: "[TODO: Ergänzen]". Erfinde nichts dazu. - - OUTPUT FORMAT (YAML + MARKDOWN): - --- - type: {target_type} - status: draft - title: (Erstelle einen treffenden, kurzen Titel für den Inhalt) - tags: [Tag1, Tag2] - --- - - # (Wiederhole den Titel hier) - - ## (Erster Begriff aus STRUKTUR) - (Text...) - - ## (Zweiter Begriff aus STRUKTUR) - (Text...) - - (usw.) - +interview_template: + ollama: | + TASK: + Du bist ein professioneller Ghostwriter. Verwandle den "USER INPUT" in eine strukturierte Notiz vom Typ '{target_type}'. + + STRUKTUR (Nutze EXAKT diese Überschriften): + {schema_fields} + + USER INPUT: + "{query}" + + ANWEISUNG ZUM INHALT: + 1. Analysiere den Input genau. + 2. Schreibe die Inhalte unter die passenden Überschriften aus der STRUKTUR-Liste oben. + 3. STIL: Schreibe flüssig, professionell und in der Ich-Perspektive. Korrigiere Grammatikfehler, aber behalte den persönlichen Ton bei. + 4. Wenn Informationen für einen Abschnitt fehlen, schreibe nur: "[TODO: Ergänzen]". Erfinde nichts dazu. + + OUTPUT FORMAT (YAML + MARKDOWN): + --- + type: {target_type} + status: draft + title: (Erstelle einen treffenden, kurzen Titel für den Inhalt) + tags: [Tag1, Tag2] + --- + + # (Wiederhole den Titel hier) + + ## (Erster Begriff aus STRUKTUR) + (Text...) + + ## (Zweiter Begriff aus STRUKTUR) + (Text...) + gemini: "Extrahiere Daten für {target_type} aus {query}." + openrouter: "Strukturiere den Input {query} nach dem Schema {schema_fields} für Typ {target_type}." # --------------------------------------------------------- # 6. EDGE_ALLOCATION: Kantenfilter (Intent: OFFLINE_FILTER) # --------------------------------------------------------- -edge_allocation_template: | - TASK: - Du bist ein strikter Selektor. Du erhältst eine Liste von "Kandidaten-Kanten" (Strings). - Wähle jene aus, die inhaltlich im "Textabschnitt" vorkommen oder relevant sind. +edge_allocation_template: + ollama: | + TASK: + Du bist ein strikter Selektor. Du erhältst eine Liste von "Kandidaten-Kanten" (Strings). + Wähle jene aus, die inhaltlich im "Textabschnitt" vorkommen oder relevant sind. - TEXTABSCHNITT: - """ - {chunk_text} - """ + TEXTABSCHNITT: + """ + {chunk_text} + """ - KANDIDATEN (Auswahl-Pool): - {edge_list} + KANDIDATEN (Auswahl-Pool): + {edge_list} - REGELN: - 1. Die Kanten haben das Format "typ:ziel". Der "typ" ist variabel und kann ALLES sein (z.B. uses, blocks, inspired_by, loves, etc.). - 2. Gib NUR die Strings aus der Kandidaten-Liste zurück, die zum Text passen. - 3. Erfinde KEINE neuen Kanten. Nutze exakt die Schreibweise aus der Liste. - 4. Antworte als flache JSON-Liste. + REGELN: + 1. Die Kanten haben das Format "typ:ziel". Der "typ" ist variabel und kann ALLES sein. + 2. Gib NUR die Strings aus der Kandidaten-Liste zurück, die zum Text passen. + 3. Erfinde KEINE neuen Kanten. + 4. Antworte als flache JSON-Liste. - BEISPIEL (Zur Demonstration der Logik): - Input Text: "Das Projekt Alpha scheitert, weil Budget fehlt." - Input Kandidaten: ["blocks:Projekt Alpha", "inspired_by:Buch der Weisen", "needs:Budget"] - Output: ["blocks:Projekt Alpha", "needs:Budget"] + DEIN OUTPUT (JSON): + gemini: | + TASK: Ordne Kanten einem Textabschnitt zu. + ERLAUBTE TYPEN: {valid_types} + TEXT: {chunk_text} + KANDIDATEN: {edge_list} + OUTPUT: STRIKT eine flache JSON-Liste ["typ:ziel"]. Kein Text davor/danach. Wenn nichts: []. Keine Objekte! + openrouter: | + TASK: Filtere relevante Kanten aus dem Pool. + ERLAUBTE TYPEN: {valid_types} + TEXT: {chunk_text} + POOL: {edge_list} + ANWEISUNG: Gib NUR eine flache JSON-Liste von Strings zurück. + BEISPIEL: ["kind:target", "kind:target"] + REGEL: Kein Text, keine Analyse, keine Kommentare. Wenn nichts passt, gib [] zurück. + OUTPUT: - DEIN OUTPUT (JSON): \ No newline at end of file +# --------------------------------------------------------- +# 7. SMART EDGE ALLOCATION: Extraktion (Intent: INGEST) +# --------------------------------------------------------- +edge_extraction: + ollama: | + TASK: + Du bist ein Wissens-Ingenieur für den digitalen Zwilling 'mindnet'. + Deine Aufgabe ist es, semantische Relationen (Kanten) aus dem Text zu extrahieren, + die die Hauptnotiz '{note_id}' mit anderen Konzepten verbinden. + + ANWEISUNGEN: + 1. Identifiziere wichtige Entitäten, Konzepte oder Ereignisse im Text. + 2. Bestimme die Art der Beziehung (z.B. part_of, uses, related_to, blocks, caused_by). + 3. Das Ziel (target) muss ein prägnanter Begriff sein. + 4. Antworte AUSSCHLIESSLICH in validem JSON als Liste von Objekten. + + BEISPIEL: + [[ {{"to": "Ziel-Konzept", "kind": "beziehungs_typ"}} ]] + + TEXT: + """ + {text} + """ + + DEIN OUTPUT (JSON): + gemini: | + Analysiere '{note_id}'. Extrahiere semantische Beziehungen. + ERLAUBTE TYPEN: {valid_types} + TEXT: {text} + OUTPUT: STRIKT JSON-Array von Objekten: [[{{"to":"Ziel","kind":"typ"}}]]. Kein Text davor/danach. Wenn nichts: []. + openrouter: | + TASK: Extrahiere semantische Relationen für '{note_id}'. + ERLAUBTE TYPEN: {valid_types} + TEXT: {text} + ANWEISUNG: Antworte AUSSCHLIESSLICH mit einem JSON-Array von Objekten. + FORMAT: [[{{"to":"Ziel-Begriff","kind":"typ"}}]] + STRIKTES VERBOT: Schreibe keine Einleitung, keine Analyse und keine Erklärungen. + Wenn keine Relationen existieren, antworte NUR mit: [] + OUTPUT: \ No newline at end of file diff --git a/docs/00_General/00_glossary.md b/docs/00_General/00_glossary.md index 7c65dad..a3ee1f4 100644 --- a/docs/00_General/00_glossary.md +++ b/docs/00_General/00_glossary.md @@ -2,13 +2,13 @@ doc_type: glossary audience: all status: active -version: 2.7.0 -context: "Zentrales Glossar für Mindnet v2.7. Definitionen von Entitäten, WP-22 Scoring-Konzepten und der Edge Registry." +version: 2.8.0 +context: "Zentrales Glossar für Mindnet v2.8. Enthält Definitionen zu Hybrid-Cloud Resilienz, WP-76 Quoten-Steuerung und Mistral-safe Parsing." --- # Mindnet Glossar -**Quellen:** `01_edge_vocabulary.md`, `retriever_scoring.py`, `edge_registry.py` +**Quellen:** `01_edge_vocabulary.md`, `llm_service.py`, `ingestion.py`, `edge_registry.py` ## Kern-Entitäten @@ -21,21 +21,22 @@ context: "Zentrales Glossar für Mindnet v2.7. Definitionen von Entitäten, WP-2 ## Komponenten * **Edge Registry:** Der zentrale Dienst (SSOT), der Kanten-Typen validiert und Aliase in kanonische Typen auflöst. Nutzt `01_edge_vocabulary.md` als Basis. -* **Retriever:** Besteht in v2.7 aus der Orchestrierung (`retriever.py`) und der mathematischen Scoring-Engine (`retriever_scoring.py`). +* **LLM Service:** Der Hybrid-Client (v3.3.6), der Anfragen zwischen OpenRouter, Google Gemini und lokalem Ollama routet. Verwaltet Cloud-Timeouts und Quoten-Management. +* **Retriever:** Besteht in v2.7+ aus der Orchestrierung (`retriever.py`) und der mathematischen Scoring-Engine (`retriever_scoring.py`). * **Decision Engine:** Teil des Routers, der Intents erkennt und entsprechende **Boost-Faktoren** für das Retrieval injiziert. -* **Traffic Control:** Verwaltet Prioritäten und drosselt Hintergrund-Tasks (z.B. Smart Edges) mittels Semaphoren. +* **Traffic Control:** Verwaltet Prioritäten und drosselt Hintergrund-Tasks (z.B. Smart Edges) mittels Semaphoren und Timeouts (45s) zur Vermeidung von System-Hangs. * **Unknown Edges Log:** Die Datei `unknown_edges.jsonl`, in der das System Kanten-Typen protokolliert, die nicht im Dictionary gefunden wurden. ## Konzepte & Features -* **Canonical Type:** Der standardisierte System-Name einer Kante (z.B. `based_on`), der in der Datenbank gespeichert wird. -* **Alias (Edge):** Ein nutzerfreundliches Synonym (z.B. `basiert_auf`), das während der Ingestion automatisch zum Canonical Type aufgelöst wird. +* **Hybrid Provider Cascade:** Die intelligente Reihenfolge der Modell-Ansprache. Schlägt die Cloud (OpenRouter/Gemini) fehl, erfolgt nach Retries ein Fallback auf den lokalen Ollama (Quoten-Schutz). +* **Rate-Limit Resilience (WP-76):** Automatisierte Erkennung von HTTP 429 Fehlern. Das System pausiert (konfigurierbar via `LLM_RATE_LIMIT_WAIT`) und wiederholt den Cloud-Call, bevor der langsame Fallback ausgelöst wird. +* **Mistral-safe Parsing:** Robuste Extraktions-Logik in Ingestion und Analyzer, die technische Steuerzeichen (``, `[OUT]`) und Framework-Tags erkennt und entfernt, um valides JSON aus Free-Modellen zu gewinnen. * **Lifecycle Scoring (WP-22):** Ein Mechanismus, der die Relevanz einer Notiz basierend auf ihrem Status gewichtet (z.B. Bonus für `stable`, Malus für `draft`). * **Intent Boosting:** Dynamische Erhöhung der Kanten-Gewichte basierend auf der Nutzerfrage (z.B. Fokus auf `caused_by` bei "Warum"-Fragen). * **Provenance Weighting:** Gewichtung einer Kante nach ihrer Herkunft: * `explicit`: Vom Mensch gesetzt (Prio 1). - * `smart`: Von der KI validiert (Prio 2). - * `rule`: Durch System-Regeln/Matrix erzeugt (Prio 3). + * `semantic_ai`: Von der KI im Turbo-Mode extrahiert und validiert (Prio 2). + * `structure`: Durch System-Regeln/Matrix erzeugt (Prio 3). * **Smart Edge Allocation:** KI-Verfahren zur Relevanzprüfung von Links für spezifische Textabschnitte. -* **Strict Heading Split:** Chunking-Strategie mit harten Grenzen an Überschriften und integriertem "Safety Net" gegen zu große Chunks. * **Matrix Logic:** Bestimmung des Kanten-Typs basierend auf Quell- und Ziel-Entität (z.B. Erfahrung -> Wert = `based_on`). \ No newline at end of file diff --git a/docs/02_concepts/02_concept_ai_personality.md b/docs/02_concepts/02_concept_ai_personality.md index 4f2afe6..4f37053 100644 --- a/docs/02_concepts/02_concept_ai_personality.md +++ b/docs/02_concepts/02_concept_ai_personality.md @@ -3,71 +3,78 @@ doc_type: concept audience: architect, product_owner scope: ai, router, personas status: active -version: 2.6 -context: "Fachkonzept der KI-Persönlichkeit, der Decision Engine und Erweiterungsstrategien." +version: 2.8 +context: "Fachkonzept der KI-Persönlichkeit, der Hybrid-Provider-Kaskade und der operationalen Resilienz." --- # Konzept: KI-Persönlichkeit & Router -**Quellen:** `mindnet_functional_architecture.md`, `Programmplan_V2.2.md` +**Quellen:** `mindnet_functional_architecture.md`, `llm_service.py`, `config.py` -Mindnet soll nicht wie eine Suchmaschine wirken, sondern wie ein **Digitaler Zwilling**. Dazu muss das System erkennen, **was** der Nutzer will, und seine "Persönlichkeit" anpassen. +Mindnet soll nicht wie eine Suchmaschine wirken, sondern wie ein **Digitaler Zwilling**. Dazu muss das System erkennen, **was** der Nutzer will, und seine „Persönlichkeit“ sowie seine technische Infrastruktur dynamisch anpassen. ## 1. Der Hybrid Router (Das Gehirn) -Jede Eingabe durchläuft den **Hybrid Router**. Er entscheidet über die Strategie. +Jede Eingabe durchläuft den **Hybrid Router**. Er entscheidet über die fachliche Strategie und die technische Ausführung. ### Modus A: RAG (Retrieval Augmented Generation) -* *Intent:* Der Nutzer hat eine Frage oder ein Problem (`FACT`, `DECISION`, `EMPATHY`). -* *Aktion:* Das System sucht im Gedächtnis und generiert eine Antwort. +* **Intent:** Der Nutzer hat eine Frage oder ein Problem (`FACT`, `DECISION`, `EMPATHY`). +* **Aktion:** Das System sucht im Gedächtnis und generiert eine Antwort. ### Modus B: Interview (Knowledge Capture) -* *Intent:* Der Nutzer will Wissen speichern (`INTERVIEW`). -* *Aktion:* Das System sucht **nicht**, sondern fragt ab und erstellt einen Draft. +* **Intent:** Der Nutzer will Wissen speichern (`INTERVIEW`). +* **Aktion:** Das System sucht **nicht**, sondern fragt ab und erstellt einen Draft. --- -## 2. Die Personas (Strategien) +## 2. Die Provider-Kaskade (Hybrid-Cloud Resilienz) + +Ein intelligenter Zwilling muss jederzeit verfügbar sein. Mindnet v2.8 nutzt eine **dreistufige Kaskade**, um Intelligenz, Kosten und Verfügbarkeit zu optimieren: + +1. **Stufe 1: High-Performance Cloud (OpenRouter/Gemini):** Primäre Wahl für komplexe Schlussfolgerungen und semantische Extraktion (Mistral-7B / Gemini-2.5-Lite). +2. **Stufe 2: Resilienz-Pause (Quota-Handling):** Bei Erreichen von Provider-Limits (HTTP 429) pausiert das System intelligent (konfigurierbar via `LLM_RATE_LIMIT_WAIT`), anstatt den Dienst abzubrechen. +3. **Stufe 3: Local-Only Fallback (Ollama):** Schlagen alle Cloud-Retries fehl, übernimmt das lokale Modell (Phi-3), um die Betriebssicherheit ohne Datenabfluss zu garantieren. + +--- + +## 3. Die Personas (Strategien) Mindnet wechselt den Hut, je nach Situation. -### 2.1 Der Berater (Strategy: DECISION) -* **Auslöser:** Fragen wie "Soll ich...?", "Was ist besser?". +### 3.1 Der Berater (Strategy: DECISION) +* **Auslöser:** Fragen wie „Soll ich...?“, „Was ist besser?“. * **Strategic Retrieval:** Lädt aktiv Notizen der Typen `value` (Werte), `goal` (Ziele) und `risk` (Risiken), auch wenn sie im Text nicht direkt vorkommen. -* **Reasoning:** *"Wäge die Fakten gegen meine Werte ab. Sei strikt bei Risiken."* +* **Reasoning:** *„Wäge die Fakten gegen meine Werte ab. Sei strikt bei Risiken.“* -### 2.2 Der Spiegel (Strategy: EMPATHY) -* **Auslöser:** Emotionale Aussagen ("Ich bin frustriert"). +### 3.2 Der Spiegel (Strategy: EMPATHY) +* **Auslöser:** Emotionale Aussagen („Ich bin frustriert“). * **Strategic Retrieval:** Lädt `experience` (Erfahrungen) und `belief` (Glaubenssätze). -* **Reasoning:** *"Nutze meine eigenen Erfahrungen, um die Situation einzuordnen."* +* **Reasoning:** *„Nutze meine eigenen Erfahrungen, um die Situation einzuordnen.“* -### 2.3 Der Bibliothekar (Strategy: FACT) -* **Auslöser:** Sachfragen ("Was ist Qdrant?"). +### 3.3 Der Bibliothekar (Strategy: FACT) +* **Auslöser:** Sachfragen („Was ist Qdrant?“). * **Behavior:** Präzise, neutral, kurz. --- -## 3. Future Concepts: The Empathic Digital Twin +## 4. Future Concepts: The Empathic Digital Twin -Um Mindnet von einer Maschine zu einem echten Spiegel der Persönlichkeit zu entwickeln, sind folgende Konzepte in der Architektur angelegt: +### 4.1 Antizipation durch Erfahrung +* **Konzept:** Das System soll Konsequenzen vorhersagen („Was passiert, wenn...?“). +* **Logik:** *„In einer ähnlichen Situation (Projekt A) hat Entscheidung X zu Ergebnis Y geführt.“* (Analogie-Schluss). -### 3.1 Antizipation durch Erfahrung -* **Konzept:** Das System soll Konsequenzen vorhersagen ("Was passiert, wenn...?"). -* **Logik:** *"In einer ähnlichen Situation (Projekt A) hat Entscheidung X zu Ergebnis Y geführt."* (Analogie-Schluss). - -### 3.2 Empathie & "Ich"-Modus +### 4.2 Empathie & „Ich“-Modus * **Konzept:** Das System antwortet im Tonfall des Nutzers. * **Umsetzung:** Few-Shot Prompting mit eigenen E-Mails/Texten als Stilvorlage. -### 3.3 Glaubenssätze & Rituale -* **Konzept:** Berücksichtigung weicher Faktoren. -* **Szenario:** Bei Terminplanungen werden Rituale ("Keine Meetings vor 10 Uhr") automatisch als harte Restriktion gegen Anfragen geprüft. +### 4.3 Resilienz als Charakterzug +Durch das **WP-76 Handling** zeigt das System „Geduld“: Bei Überlastung der Cloud-Dienste bricht es nicht panisch ab, sondern wartet auf die nächste freie Kapazität, um die Qualität der Antwort zu sichern. --- -## 4. Erweiterbarkeit: Das "Teach-the-AI" Paradigma +## 5. Erweiterbarkeit: Das „Teach-the-AI“ Paradigma -Mindnet lernt nicht durch Training (Fine-Tuning), sondern durch **Konfiguration** und **Vernetzung**. Wenn du dem System ein neues Konzept beibringen willst, musst du an drei Stellen eingreifen. +Mindnet lernt durch **Konfiguration** und **Vernetzung**. **Beispiel: Du willst den Typ `risk` einführen.** @@ -87,6 +94,6 @@ DECISION: ``` **3. Kognitive Ebene (Verständnis)** -In `prompts.yaml`: Erkläre dem LLM, was ein Risiko ist. +In `prompts.yaml`: Erkläre dem LLM (provider-spezifisch), was ein Risiko ist. -**Fazit:** Nur wenn **Daten** (Vault), **Physik** (Config) und **Semantik** (Prompt) zusammenspielen, entsteht ein intelligenter Zwilling. \ No newline at end of file +**Fazit:** Nur wenn **Daten** (Vault), **Infrastruktur** (Resiliente Kaskade) und **Semantik** (Prompt) zusammenspielen, entsteht ein intelligenter Zwilling. \ No newline at end of file diff --git a/docs/03_Technical_References/03_tech_chat_backend.md b/docs/03_Technical_References/03_tech_chat_backend.md index c32f2d1..d1bf0b1 100644 --- a/docs/03_Technical_References/03_tech_chat_backend.md +++ b/docs/03_Technical_References/03_tech_chat_backend.md @@ -1,17 +1,17 @@ --- doc_type: technical_reference audience: developer, architect -scope: backend, chat, ollama, traffic_control +scope: backend, chat, llm_service, traffic_control, resilience status: active -version: 2.6 -context: "Technische Implementierung des FastAPI-Routers, der Decision Engine und des Traffic Control Systems." +version: 2.8 +context: "Technische Implementierung des FastAPI-Routers, des hybriden LLMService und des WP-76 Resilienz-Systems." --- # Chat Backend & Traffic Control ## 1. Hybrid Router (Decision Engine) -Der zentrale Einstiegspunkt für jede Chatanfrage ist der **Hybrid Router** (`app/routers/chat.py`). Er entscheidet dynamisch, welche Strategie gewählt wird, basierend auf dem User-Input. +Der zentrale Einstiegspunkt für jede Chatanfrage ist der **Hybrid Router** (`app/routers/chat.py`). Er entscheidet dynamisch über die Strategie und nutzt den `LLMService` zur provider-agnostischen Generierung. ### 1.1 Intent-Erkennung (Logik) @@ -21,18 +21,26 @@ Der Router prüft den Input in drei Stufen (Wasserfall-Prinzip): * Prüfung auf Vorhandensein von `?` oder W-Wörtern (Wer, Wie, Was, Soll ich). * Wenn positiv: **RAG Modus** (Interview wird blockiert). 2. **Keyword Scan (Fast Path):** - * Lädt `types.yaml` (Objekte, z.B. "Projekt") und `decision_engine.yaml` (Handlungen, z.B. "neu"). + * Lädt `types.yaml` (Objekte) und `decision_engine.yaml` (Handlungen). * Wenn Match (z.B. "Projekt" + "neu"): **INTERVIEW Modus**. 3. **LLM Fallback (Slow Path):** - * Wenn unklar: Anfrage an LLM zur Klassifizierung. + * Wenn unklar: Anfrage an LLM zur Klassifizierung mittels `router_prompt`. -### 1.2 RAG Flow (Technisch) +### 1.2 Prompt-Auflösung (WP-20 Fix) + +Um Kompatibilitätsprobleme mit verschachtelten YAML-Prompts zu vermeiden, nutzt der Router die Methode `llm.get_prompt()`. Diese implementiert eine **Provider-Kaskade**: +* Das System sucht zuerst nach einem Prompt für den aktiven Provider (z.B. `openrouter`). +* Existiert dieser nicht, erfolgt ein Fallback auf `gemini` und schließlich auf `ollama`. +* Dies garantiert die Rückgabe eines validen Strings und verhindert 500-Fehler bei String-Operationen wie `.replace()`. + +### 1.3 RAG Flow (Technisch) Wenn der Intent `FACT` oder `DECISION` ist, wird folgender Flow ausgeführt: 1. **Pre-Processing:** Query Rewriting (optional). 2. **Context Enrichment:** * Abruf via `retriever.py` (Hybrid Search). + * Integration von **Edge Boosts** aus der `decision_engine.yaml` zur Beeinflussung der Graph-Gewichtung. * Injection von Metadaten (`[TYPE]`, `[SCORE]`) in den Prompt. 3. **Prompt Construction:** Assembly aus System-Prompt (Persona) + Context + Query. 4. **Streaming:** LLM-Antwort wird via **SSE (Server-Sent Events)** an den Client gestreamt. @@ -40,36 +48,44 @@ Wenn der Intent `FACT` oder `DECISION` ist, wird folgender Flow ausgeführt: --- -## 2. Traffic Control (WP-15) +## 2. LLM Service & Traffic Control (WP-15/WP-20) -Das Traffic Control System (`app/core/llm_service.py`) schützt das System vor Überlastung, wenn rechenintensive Hintergrundprozesse (Smart Edge Import) und Latenz-kritische Chat-Anfragen gleichzeitig laufen. +Der `LLMService` (`app/services/llm_service.py`) fungiert als zentraler Hybrid-Client für OpenRouter, Google Gemini und Ollama. Er schützt das System vor Überlastung und verwaltet Quoten. ### 2.1 Prioritäts-Semaphor -Jeder LLM-Request muss ein `priority`-Flag setzen. - -**Prioritäten-Levels:** +Jeder LLM-Request steuert über ein `priority`-Flag den Zugriff auf Hardware- und API-Ressourcen: | Priority | Verwendung | Limitierung | | :--- | :--- | :--- | -| **realtime** | Chat-Anfragen | Keine (Hardware-Limit) | -| **background** | Smart Edge Allocation, Drafts | `MINDNET_LLM_BACKGROUND_LIMIT` | +| **realtime** | Chat-Anfragen, Intent-Routing | Keine (Hardware-Limit) | +| **background** | Smart Edge Allocation, Import-Tasks | `MINDNET_LLM_BACKGROUND_LIMIT` | **Funktionsweise:** -* Hintergrund-Tasks nutzen `asyncio.Semaphore`. -* Wenn das Limit (Default: 2) erreicht ist, warten weitere Import-Tasks. -* Chat-Tasks umgehen die Semaphore und werden sofort bearbeitet. +* Hintergrund-Tasks nutzen ein globales `asyncio.Semaphore`. +* Das Limit (Default: 2) verhindert, dass parallele Import-Vorgänge die API-Quoten oder die lokale CPU erschöpfen. +* Chat-Tasks umgehen die Semaphore für minimale Latenz. ### 2.2 Timeout-Konfiguration -Deadlocks werden durch strikte Timeouts verhindert, die in der `.env` definiert sind. - -* **Chat:** `MINDNET_LLM_TIMEOUT` (Default: 300s). -* **Frontend:** `MINDNET_API_TIMEOUT` (Default: 300s). +Deadlocks und "hängende" Importe werden durch differenzierte Timeouts verhindert: +* **Cloud-Calls (OpenRouter/Gemini):** Strikte **45 Sekunden** zur Vermeidung von Blockaden bei Provider-Latenz. +* **Lokales LLM (Ollama):** Konfigurierbar via `MINDNET_LLM_TIMEOUT` (Default: 300s). --- -## 3. Feedback Traceability +## 3. Resilience & Quota Management (WP-76) + +In v2.8 wurde ein intelligentes Fehler-Handling für Cloud-Provider implementiert: + +1. **Rate-Limit Erkennung:** Der Service erkennt HTTP 429 Fehler sowie provider-spezifische Meldungen wie `RESOURCE_EXHAUSTED`. +2. **Intelligenter Backoff:** Statt sofort auf das langsame lokale Modell zu wechseln, pausiert das System für die Dauer von `LLM_RATE_LIMIT_WAIT` (Default: 60s). +3. **Cloud-Retry:** Nach der Pause erfolgt ein erneuter Versuch (bis zu `LLM_RATE_LIMIT_RETRIES` Mal). +4. **Ollama Fallback:** Erst nach Erschöpfung der Retries schaltet das System auf den lokalen Ollama um, um die Betriebssicherheit zu gewährleisten ("Quoten-Schutz"). + +--- + +## 4. Feedback Traceability Unterstützt das geplante Self-Tuning (WP08). diff --git a/docs/03_Technical_References/03_tech_configuration.md b/docs/03_Technical_References/03_tech_configuration.md index db92177..3ee257e 100644 --- a/docs/03_Technical_References/03_tech_configuration.md +++ b/docs/03_Technical_References/03_tech_configuration.md @@ -1,15 +1,15 @@ --- doc_type: technical_reference audience: developer, admin -scope: configuration, env, registry, scoring +scope: configuration, env, registry, scoring, resilience status: active -version: 2.7.2 -context: "Umfassende Referenztabellen für Umgebungsvariablen, YAML-Konfigurationen und die Edge Registry Struktur." +version: 2.8.0 +context: "Umfassende Referenztabellen für Umgebungsvariablen (inkl. Hybrid-Cloud & WP-76), YAML-Konfigurationen und die Edge Registry Struktur." --- # Konfigurations-Referenz -Dieses Dokument beschreibt alle Steuerungsdateien von Mindnet. In der Version 2.7 wurde die Konfiguration professionalisiert, um die Edge Registry und dynamische Scoring-Parameter (Lifecycle & Intent) zu unterstützen. +Dieses Dokument beschreibt alle Steuerungsdateien von Mindnet. In der Version 2.8 wurde die Konfiguration professionalisiert, um die Edge Registry, dynamische Scoring-Parameter (Lifecycle & Intent) sowie die neue Hybrid-Cloud-Resilienz zu unterstützen. ## 1. Environment Variablen (`.env`) @@ -22,17 +22,25 @@ Diese Variablen steuern die Infrastruktur, Pfade und globale Timeouts. | `COLLECTION_PREFIX` | `mindnet` | Namensraum für Collections (erzeugt `{prefix}_notes` etc). | | `VECTOR_DIM` | `768` | **Muss 768 sein** (für Nomic Embeddings). | | `MINDNET_VOCAB_PATH` | *(Pfad)* | **Neu (WP-22):** Absoluter Pfad zur `01_edge_vocabulary.md`. Definiert den Ort des Dictionarys. | -| `MINDNET_VAULT_ROOT` | `./vault` | Basis-Pfad für Datei-Operationen. Dient als Fallback-Basis, falls `MINDNET_VOCAB_PATH` nicht gesetzt ist. | +| `MINDNET_VAULT_ROOT` | `./vault` | Basis-Pfad für Datei-Operationen. | | `MINDNET_TYPES_FILE` | `config/types.yaml` | Pfad zur Typ-Registry. | | `MINDNET_RETRIEVER_CONFIG`| `config/retriever.yaml`| Pfad zur Scoring-Konfiguration. | | `MINDNET_DECISION_CONFIG` | `config/decision_engine.yaml` | Pfad zur Router & Intent Config. | | `MINDNET_PROMPTS_PATH` | `config/prompts.yaml` | Pfad zu LLM Prompts. | -| `MINDNET_LLM_MODEL` | `phi3:mini` | Name des Chat-Modells (Ollama). | +| `MINDNET_LLM_PROVIDER` | `openrouter` | **Neu (WP-20):** Aktiver Provider (`openrouter`, `gemini`, `ollama`). | +| `MINDNET_LLM_FALLBACK` | `true` | **Neu (WP-20):** Aktiviert automatischen Ollama-Fallback bei Cloud-Fehlern. | +| `MINDNET_LLM_RATE_LIMIT_WAIT`| `60.0` | **Neu (WP-76):** Wartezeit in Sekunden bei HTTP 429 (Rate Limit). | +| `MINDNET_LLM_RATE_LIMIT_RETRIES`| `3` | **Neu (WP-76):** Anzahl Cloud-Retries vor lokalem Fallback. | +| `GOOGLE_API_KEY` | *(Key)* | API Key für Google AI Studio. | +| `MINDNET_GEMINI_MODEL` | `gemini-2.5-flash-lite` | **Update 2025:** Optimiertes Lite-Modell für hohe Quoten. | +| `OPENROUTER_API_KEY` | *(Key)* | API Key für OpenRouter Integration. | +| `OPENROUTER_MODEL` | `mistralai/mistral-7b-instruct:free` | **Update 2025:** Verifiziertes Free-Modell für strukturierte Extraktion. | +| `MINDNET_LLM_MODEL` | `phi3:mini` | Name des lokalen Chat-Modells (Ollama). | | `MINDNET_EMBEDDING_MODEL` | `nomic-embed-text` | Name des Embedding-Modells (Ollama). | -| `MINDNET_OLLAMA_URL` | `http://127.0.0.1:11434`| URL zum LLM-Server. | -| `MINDNET_LLM_TIMEOUT` | `300.0` | Timeout in Sekunden (Erhöht für CPU Cold-Starts). | -| `MINDNET_API_TIMEOUT` | `300.0` | Frontend Timeout (Erhöht für Smart Edge Wartezeiten). | -| `MINDNET_LLM_BACKGROUND_LIMIT`| `2` | **Traffic Control:** Max. parallele Hintergrund-Tasks (Semaphore). | +| `MINDNET_OLLAMA_URL` | `http://127.0.0.1:11434`| URL zum lokalen LLM-Server. | +| `MINDNET_LLM_TIMEOUT` | `300.0` | Timeout in Sekunden für LLM-Anfragen. | +| `MINDNET_API_TIMEOUT` | `300.0` | Globales API-Timeout für das Frontend. | +| `MINDNET_LL_BACKGROUND_LIMIT`| `2` | **Traffic Control:** Max. parallele Hintergrund-Tasks (Semaphore). | | `MINDNET_CHANGE_DETECTION_MODE` | `full` | `full` (Text + Meta) oder `body` (nur Text). | --- @@ -199,21 +207,21 @@ Die Datei muss eine Markdown-Tabelle enthalten, die vom Regex-Parser gelesen wir | System-Typ (Canonical) | Erlaubte Aliasse (User) | Beschreibung | | :--------------------- | :--------------------------------------------------- | :-------------------------------------- | -| **`caused_by`** | `ausgelöst_durch`, `wegen`, `ursache_ist` | Kausalität: A löst B aus. | -| **`derived_from`** | `abgeleitet_von`, `quelle`, `inspiriert_durch` | Herkunft: A stammt von B. | -| **`based_on`** | `basiert_auf`, `fundament`, `grundlage` | Fundament: B baut auf A auf. | -| **`solves`** | `löst`, `beantwortet`, `fix_für` | Lösung: A ist Lösung für Problem B. | -| **`part_of`** | `teil_von`, `gehört_zu`, `cluster` | Hierarchie: Kind -> Eltern. | -| **`depends_on`** | `hängt_ab_von`, `braucht`, `requires`, `enforced_by` | Abhängigkeit: A braucht B. | -| **`blocks`** | `blockiert`, `verhindert`, `risiko_für` | Blocker: A verhindert B. | -| **`uses`** | `nutzt`, `verwendet`, `tool` | Werkzeug: A nutzt B. | -| **`guides`** | `steuert`, `leitet`, `orientierung` | Soft-Dependency: A gibt Richtung für B. | -| **`followed_by`** | `danach`, `folgt`, `nachfolger`, `followed_by` | Prozess: A -> B. | -| **`preceeded_by`** | `davor`, `vorgänger`, `preceded_by` | Prozess: B <- A. | -| **`related_to`** | `siehe_auch`, `kontext`, `thematisch` | Lose Assoziation. | -| **`similar_to`** | `ähnlich_wie`, `vergleichbar` | Synonym / Ähnlichkeit. | -| **`references`** | *(Kein Alias)* | Standard-Verweis (Fallback). | -| **`resulted_in`** | `ergebnis`, `resultat`, `erzeugt` | Herkunft: A erzeugt Ergebnis B | +| **`caused_by`** | `ausgelöst_durch`, `wegen`, `ursache_ist` | Kausalität: A löst B aus. | +| **`derived_from`** | `abgeleitet_von`, `quelle`, `inspiriert_durch` | Herkunft: A stammt von B. | +| **`based_on`** | `basiert_auf`, `fundament`, `grundlage` | Fundament: B baut auf A auf. | +| **`solves`** | `löst`, `beantwortet`, `fix_für` | Lösung: A ist Lösung für Problem B. | +| **`part_of`** | `teil_von`, `gehört_zu`, `cluster` | Hierarchie: Kind -> Eltern. | +| **`depends_on`** | `hängt_ab_von`, `braucht`, `requires`, `enforced_by` | Abhängigkeit: A braucht B. | +| **`blocks`** | `blockiert`, `verhindert`, `risiko_für` | Blocker: A verhindert B. | +| **`uses`** | `nutzt`, `verwendet`, `tool` | Werkzeug: A nutzt B. | +| **`guides`** | `steuert`, `leitet`, `orientierung` | Soft-Dependency: A gibt Richtung für B. | +| **`followed_by`** | `danach`, `folgt`, `nachfolger`, `followed_by` | Prozess: A -> B. | +| **`preceeded_by`** | `davor`, `vorgänger`, `preceded_by` | Prozess: B <- A. | +| **`related_to`** | `siehe_auch`, `kontext`, `thematisch` | Lose Assoziation. | +| **`similar_to`** | `ähnlich_wie`, `vergleichbar` | Synonym / Ähnlichkeit. | +| **`references`** | *(Kein Alias)* | Standard-Verweis (Fallback). | +| **`resulted_in`** | `ergebnis`, `resultat`, `erzeugt` | Herkunft: A erzeugt Ergebnis B | **ACHTUNG!** Die Kantentypen **belongs_to**, **next** und **prev** dürfen nicht vom Nutzer gesetzt werden diff --git a/docs/03_Technical_References/03_tech_ingestion_pipeline.md b/docs/03_Technical_References/03_tech_ingestion_pipeline.md index 3acbad3..9ca4efc 100644 --- a/docs/03_Technical_References/03_tech_ingestion_pipeline.md +++ b/docs/03_Technical_References/03_tech_ingestion_pipeline.md @@ -3,15 +3,15 @@ doc_type: technical_reference audience: developer, devops scope: backend, ingestion, smart_edges, edge_registry status: active -version: 2.7.1 -context: "Detaillierte technische Beschreibung der Import-Pipeline, Chunking-Strategien und CLI-Befehle." +version: 2.8.0 +context: "Detaillierte technische Beschreibung der Import-Pipeline, Mistral-safe Parsing und WP-76 Resilienz-Logik." --- # Ingestion Pipeline & Smart Processing -**Quellen:** `pipeline_playbook.md`, `Handbuch.md`, `edge_registry.py`, `01_edge_vocabulary.md`, `06_active_roadmap.md` +**Quellen:** `pipeline_playbook.md`, `ingestion.py`, `edge_registry.py`, `01_edge_vocabulary.md`, `llm_service.py` -Die Ingestion transformiert Markdown in den Graphen. Entrypoint: `scripts/import_markdown.py` (CLI) oder `routers/ingest.py` (API). Seit v2.7 integriert dieser Prozess die **Edge Registry** zur Normalisierung des Vokabulars und beachtet den **Content Lifecycle**. +Die Ingestion transformiert Markdown in den Graphen. Entrypoint: `scripts/import_markdown.py` (CLI) oder `routers/ingest.py` (API). Seit v2.8 integriert dieser Prozess eine **intelligente Quoten-Steuerung** (WP-76) und ein **robustes JSON-Parsing** für Cloud-Modelle (Mistral/Gemini). ## 1. Der Import-Prozess (15-Schritte-Workflow) @@ -38,10 +38,11 @@ Der Prozess ist **asynchron** und **idempotent**. * Vergleich des Hashes mit Qdrant. * Strategie wählbar via ENV `MINDNET_CHANGE_DETECTION_MODE` (`full` oder `body`). 8. **Chunking anwenden:** Zerlegung des Textes basierend auf dem ermittelten Profil (siehe Kap. 3). -9. **Smart Edge Allocation (WP15):** +9. **Smart Edge Allocation (WP15/WP20):** * Wenn `enable_smart_edge_allocation: true`: Der `SemanticAnalyzer` sendet Chunks an das LLM. - * **Traffic Control:** Request nutzt `priority="background"`. Semaphore (Limit via `.env`) drosselt die Last. - * **Resilienz:** Bei Timeout (Ollama) greift ein Fallback (Broadcasting an alle Chunks). + * **Traffic Control:** Request nutzt `priority="background"`. Semaphore drosselt die Last. + * **Resilienz (WP-76):** Erkennt HTTP 429 (Rate-Limit) und pausiert kontrolliert (via `LLM_RATE_LIMIT_WAIT`), bevor ein Cloud-Retry oder der lokale Fallback erfolgt. + * **Mistral-safe Parsing:** Automatisierte Bereinigung von BOS-Tokens (``) und Framework-Tags (`[OUT]`) zur Sicherstellung validen JSONs. 10. **Inline-Kanten finden:** Parsing von `[[rel:...]]`. 11. **Alias-Auflösung & Kanonisierung (WP-22):** * Jede Kante wird via `edge_registry.resolve()` normalisiert. @@ -138,20 +139,20 @@ Die Strategie `by_heading` zerlegt Texte anhand ihrer Struktur (Überschriften). Kanten werden nach Vertrauenswürdigkeit (`provenance`) priorisiert. Die höhere Prio gewinnt. -| Prio | Quelle | Rule ID | Confidence | Erläuterung | +| Prio | Quelle | Rule ID / Provenance | Confidence | Erläuterung | | :--- | :--- | :--- | :--- | :--- | | **1** | Wikilink | `explicit:wikilink` | **1.00** | Harte menschliche Setzung. | | **2** | Inline | `inline:rel` | **0.95** | Typisierte menschliche Kante. | | **3** | Callout | `callout:edge` | **0.90** | Explizite Meta-Information. | -| **4** | Smart Edge | `smart:llm_filter` | **0.90** | KI-validierte Verbindung. | +| **4** | Semantic AI | `semantic_ai` | **0.90** | KI-extrahierte Verbindung (Mistral-safe). | | **5** | Type Default | `edge_defaults` | **0.70** | Heuristik aus der Registry. | -| **6** | Struktur | `structure` | **1.00** | System-interne Verkettung. | +| **6** | Struktur | `structure` | **1.00** | System-interne Verkettung (`belongs_to`). | --- ## 5. Quality Gates & Monitoring -In v2.7 wurden Tools zur Überwachung der Datenqualität integriert: +In v2.7+ wurden Tools zur Überwachung der Datenqualität integriert: **1. Registry Review:** Prüfung der `data/logs/unknown_edges.jsonl`. Administratoren sollten hier gelistete Begriffe als Aliase in die `01_edge_vocabulary.md` aufnehmen. diff --git a/docs/06_Roadmap/06_active_roadmap.md b/docs/06_Roadmap/06_active_roadmap.md index 46ce660..755f66e 100644 --- a/docs/06_Roadmap/06_active_roadmap.md +++ b/docs/06_Roadmap/06_active_roadmap.md @@ -2,18 +2,18 @@ doc_type: roadmap audience: product_owner, developer status: active -version: 2.7 +version: 2.8.0 context: "Aktuelle Planung für kommende Features (ab WP16), Release-Strategie und Historie der abgeschlossenen WPs." --- # Mindnet Active Roadmap -**Aktueller Stand:** v2.6.0 (Post-WP15/WP19) -**Fokus:** Visualisierung, Exploration & Intelligent Ingestion. +**Aktueller Stand:** v2.8.0 (Post-WP20/WP76) +**Fokus:** Visualisierung, Exploration & Cloud-Resilienz. ## 1. Programmstatus -Wir haben mit der Implementierung des Graph Explorers (WP19) und der Smart Edge Allocation (WP15) die Basis für ein intelligentes, robustes System gelegt. Der nächste Schritt (WP19a) vertieft die Analyse, während WP16 die "Eingangs-Intelligenz" erhöht. +Wir haben mit der Implementierung des Graph Explorers (WP19), der Smart Edge Allocation (WP15) und der hybriden Cloud-Resilienz (WP20) die Basis für ein intelligentes, robustes System gelegt. Der nächste Schritt (WP19a) vertieft die Analyse, während WP16 die "Eingangs-Intelligenz" erhöht. | Phase | Fokus | Status | | :--- | :--- | :--- | @@ -47,7 +47,7 @@ Eine Übersicht der implementierten Features zum schnellen Auffinden von Funktio | **WP-11** | Backend Intelligence | `nomic-embed-text` (768d) und Matrix-Logik für Kanten-Typisierung. | | **WP-15** | Smart Edge Allocation | LLM-Filter für Kanten in Chunks + Traffic Control (Semaphore) + Strict Chunking. | | **WP-19** | Graph Visualisierung | **Frontend Modularisierung:** Umbau auf `ui_*.py`.
**Graph Engines:** Parallelbetrieb von Cytoscape (COSE) und Agraph.
**Tools:** "Single Source of Truth" Editor, Persistenz via URL. | -| **WP-20** | Cloud Hybrid Mode | Nutzung von Public LLM für schnellere Verarbeitung und bestimmte Aufgaben | +| **WP-20** | **Cloud Hybrid Mode & Resilienz** | **Ergebnis:** Integration von OpenRouter (Mistral 7B) & Gemini 2.5 Lite. Implementierung von WP-76 (Rate-Limit Wait) & Mistral-safe JSON Parsing. | | **WP-21** | Semantic Graph Routing & Canonical Edges | Transformation des Graphen von statischen Verbindungen zu dynamischen, kontextsensitiven Pfaden. Das System soll verstehen, *welche* Art von Verbindung für die aktuelle Frage relevant ist ("Warum?" vs. "Was kommt danach?"). | | **WP-22** | **Content Lifecycle & Registry** | **Ergebnis:** SSOT via `01_edge_vocabulary.md`, Alias-Mapping, Status-Scoring (`stable`/`draft`) und Modularisierung der Scoring-Engine. | @@ -55,6 +55,9 @@ Eine Übersicht der implementierten Features zum schnellen Auffinden von Funktio * **Architektur:** Die Trennung von `retriever.py` und `retriever_scoring.py` war notwendig, um LLM-Context-Limits zu wahren und die Testbarkeit der mathematischen Formeln zu erhöhen. * **Kanten-Validierung:** Die Edge Registry muss beim Start explizit initialisiert werden (Singleton), um "Lazy Loading" Probleme in der API zu vermeiden. +### 2.2 WP-20 Lessons Learned (Resilienz) +* **Quoten-Management:** Die Nutzung von Free-Tier Modellen (Mistral/OpenRouter) erfordert zwingend eine intelligente Rate-Limit-Erkennung (HTTP 429) mit automatisierten Wartezyklen, um Batch-Prozesse stabil zu halten. +* **Parser-Robustheit:** Cloud-Modelle betten JSON oft in technische Steuerzeichen (``, `[OUT]`) ein. Ein robuster Extraktor mit Recovery-Logik ist essentiell zur Vermeidung von Datenverlust. --- @@ -80,7 +83,7 @@ Diese Features stehen als nächstes an oder befinden sich in der Umsetzung. **Phase:** B **Status:** 🟡 geplant -**Ziel:** Sicherstellen, dass bestehende und neue Obsidian-Vaults schrittweise in mindnet integriert werden können – ohne Massenumbau. +**Ziel:** Sicherstellen, dass bestehende und neue Obsidian-Vaults schrittweise in mindnet installiert werden können – ohne Massenumbau. **Umfang:** - Tools zur Analyse des Vault-Status. @@ -90,6 +93,26 @@ Diese Features stehen als nächstes an oder befinden sich in der Umsetzung. - Aufwand: Mittel - Komplexität: Niedrig/Mittel +### WP-15b – Candidate-Based Edge Validation & Inheritance +**Phase:** B/E (Refactoring & Semantic) +**Status:** 🚀 Startklar (Ersatz für WP-15 Logik) +**Ziel:** Ablösung der fehleranfälligen "Open-End-Extraktion" durch eine KI-gestützte Validierung menschlicher Vorarbeit zur Steigerung von Speed, Integrität und semantischer Tiefe. + +**Herausforderung:** +Der bisherige WP-15 Ansatz litt unter Halluzinationen (erfundene Kantentypen), hohem Token-Verbrauch und dem Verlust physikalisch gesetzter Kanten bei der Chunk-Verteilung. + +**Anforderungen & Strategie:** +1. **Hard-Link Integrity:** Kanten, die im Markdown-Text eines Chunks stehen, werden zwingend und ohne LLM-Prüfung gesetzt (`provenance: explicit`). +2. **Edge Inheritance:** Kanten, die auf Dokument-Ebene (Frontmatter) oder Sektions-Ebene (Heading) definiert sind, werden automatisch an alle zugehörigen Sub-Chunks vererbt, wenn ein semantischer Block aufgrund von Größengrenzen geteilt wurde. +3. **Candidate Pool Extraction:** Definition eines "Edge-Pools" pro Dokument. Dieser speist sich aus dem Frontmatter und einer speziellen Sektion (z. B. `### Unzugeordnete Kanten`). +4. **Semantic Validation Gate:** Das LLM fungiert als binärer Validator. Es prüft ausschließlich Kanten aus dem Kandidaten-Pool gegen den konkreten Chunk-Inhalt UND eine Zusammenfassung der Ziel-Note (Inhalt, Typ, Kanten-Typ). +5. **Registry Enforcement:** Strikte Blockade von Halluzinationen. Nur Kanten, die im Pool definiert UND im `edge_vocabulary.md` vorhanden sind, werden zugelassen. + +**Lösungsskizze:** +* **Parser-Update:** `extract_candidate_pool` zur Identifikation aller im Dokument beabsichtigten Links. +* **Chunker-Update:** Implementierung einer `propagate_edges`-Logik für "by_heading" und "sliding_window" Strategien. +* **Ingestion-Update:** Umstellung von `_perform_smart_edge_allocation` auf einen binären Validierungs-Prompt (VALID/INVALID). + ### WP-19a – Graph Intelligence & Discovery (Sprint-Fokus) **Status:** 🚀 Startklar **Ziel:** Vom "Anschauen" zum "Verstehen". Deep-Dive Werkzeuge für den Graphen. @@ -102,7 +125,7 @@ Diese Features stehen als nächstes an oder befinden sich in der Umsetzung. **Ziel:** Technische Schulden abbauen, die durch schnelle Feature-Entwicklung (WP15/WP19) entstanden sind. * **Refactoring `chunker.py`:** Die Datei ist monolithisch geworden (Parsing, Strategien, LLM-Orchestrierung). * *Lösung:* Aufteilung in ein Package `app/core/chunking/` mit Modulen (`strategies.py`, `orchestration.py`, `utils.py`). -* **Dokumentation:** Kontinuierliche Synchronisation von Code und Docs (v2.6 Stand). +* **Dokumentation:** Kontinuierliche Synchronisation von Code und Docs (v2.8 Stand). ### WP-16 – Auto-Discovery & Intelligent Ingestion **Status:** 🟡 Geplant @@ -135,11 +158,6 @@ Diese Features stehen als nächstes an oder befinden sich in der Umsetzung. **Ziel:** mindnet als MCP-Server bereitstellen, damit Agenten (Claude Desktop, OpenAI) standardisierte Tools nutzen können. * **Umfang:** MCP-Server mit Tools (`mindnet_query`, `mindnet_explain`, etc.). -### WP-20 – Cloud Hybrid Mode (Optional) -**Status:** ⚪ Optional -**Ziel:** "Turbo-Modus" für Massen-Imports. -* **Konzept:** Switch in `.env`, um statt Ollama (Lokal) auf Google Gemini (Cloud) umzuschalten. - ### WP-21 – Semantic Graph Routing & Canonical Edges **Status:** 🟡 Geplant **Ziel:** Transformation des Graphen von statischen Verbindungen zu dynamischen, kontextsensitiven Pfaden. Das System soll verstehen, *welche* Art von Verbindung für die aktuelle Frage relevant ist ("Warum?" vs. "Was kommt danach?"). @@ -233,4 +251,6 @@ graph TD WP03(Import Pipeline) --> WP21 WP21 --> WP22(Lifecycle & Registry) WP22 --> WP14 - WP15(Smart Edges) --> WP21 \ No newline at end of file + WP15(Smart Edges) --> WP21 + WP20(Cloud Hybrid) --> WP15b +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 34bebcd..793b922 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,4 +34,10 @@ streamlit>=1.39.0 # Visualization (Parallelbetrieb) streamlit-agraph>=0.0.45 -st-cytoscape \ No newline at end of file +st-cytoscape + +# Google gemini API +google-genai>=0.3.0 + +# OpenAi für OpenRouter +openai>=1.50.0 \ No newline at end of file