komplett neues WP20 deployment

This commit is contained in:
Lars 2025-12-23 18:51:12 +01:00
parent f1bfa40b5b
commit 0157faab89
7 changed files with 203 additions and 137 deletions

View File

@ -2,8 +2,11 @@
FILE: app/config.py
DESCRIPTION: Zentrale Pydantic-Konfiguration. Enthält Parameter für Qdrant,
Embeddings, Ollama, Google GenAI und OpenRouter.
VERSION: 0.6.0 (WP-20 Hybrid & OpenRouter Integration)
WP-20: Optimiert für Hybrid-Cloud Modus und Vektor-Synchronisation.
WP-22: Integration von Change-Detection und Vocab-Paths.
VERSION: 0.6.2
STATUS: Active
DEPENDENCIES: os, functools, pathlib
"""
from __future__ import annotations
import os
@ -15,22 +18,27 @@ class Settings:
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"))
# WP-22: Vektor-Dimension muss mit dem Embedding-Modell (nomic) übereinstimmen
VECTOR_SIZE: int = int(os.getenv("VECTOR_DIM", "768"))
DISTANCE: str = os.getenv("MINDNET_DISTANCE", "Cosine")
# --- Lokale Embeddings ---
# --- Lokale Embeddings (Ollama & Sentence-Transformers) ---
# Modell für die Vektorisierung (z.B. nomic-embed-text)
EMBEDDING_MODEL: str = os.getenv("MINDNET_EMBEDDING_MODEL", "nomic-embed-text")
# Legacy Fallback Modellname
MODEL_NAME: str = os.getenv("MINDNET_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
# --- WP-20 Hybrid LLM Provider ---
# Optionen: "ollama" | "gemini" | "openrouter"
# Erlaubt: "ollama" | "gemini" | "openrouter"
MINDNET_LLM_PROVIDER: str = os.getenv("MINDNET_LLM_PROVIDER", "ollama").lower()
# Google AI Studio
# Google AI Studio (Direkt)
GOOGLE_API_KEY: str | None = os.getenv("GOOGLE_API_KEY")
GEMINI_MODEL: str = os.getenv("MINDNET_GEMINI_MODEL", "gemini-1.5-flash")
GEMMA_MODEL: str = os.getenv("MINDNET_GEMMA_MODEL", "gemma2-9b-it")
# Gemma-Modell für hohen Durchsatz bei der Ingestion
GEMMA_MODEL: str = os.getenv("MINDNET_GEMMA_MODEL", "google/gemma-2-9b-it:free")
# OpenRouter
# OpenRouter Integration
OPENROUTER_API_KEY: str | None = os.getenv("OPENROUTER_API_KEY")
OPENROUTER_MODEL: str = os.getenv("OPENROUTER_MODEL", "google/gemma-2-9b-it:free")
@ -42,16 +50,20 @@ class Settings:
PROMPTS_PATH: str = os.getenv("MINDNET_PROMPTS_PATH", "config/prompts.yaml")
# --- WP-06 / WP-14 Performance & Last-Steuerung ---
LLM_TIMEOUT: float = float(os.getenv("MINDNET_LLM_TIMEOUT", "120.0"))
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"))
# --- System-Pfade ---
# --- System-Pfade & Ingestion-Logik ---
DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
MINDNET_VAULT_ROOT: str = os.getenv("MINDNET_VAULT_ROOT", "./vault")
MINDNET_TYPES_FILE: str = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
# WP-22: Dictionary für die Edge-Validierung
MINDNET_VOCAB_PATH: str = os.getenv("MINDNET_VOCAB_PATH", "vault/_system/dictionary/edge_vocabulary.md")
# WP-22: Change Detection (Modus: "full" oder "body")
CHANGE_DETECTION_MODE: str = os.getenv("MINDNET_CHANGE_DETECTION_MODE", "full")
# --- WP-04 Retriever Gewichte ---
# --- WP-04 Retriever Gewichte (Semantik vs. Graph) ---
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"))

View File

@ -5,7 +5,7 @@ DESCRIPTION: Haupt-Ingestion-Logik. Transformiert Markdown in den Graphen (Notes
WP-22: Integration von Content Lifecycle (Status Gate) und Edge Registry Validation.
WP-22: Kontextsensitive Kanten-Validierung mit Fundort-Reporting (Zeilennummern).
WP-22: Multi-Hash Refresh für konsistente Change Detection.
VERSION: 2.11.3 (WP-20 Quota Protection & Stability Patch)
VERSION: 2.11.4
STATUS: Active
DEPENDENCIES: app.core.parser, app.core.note_payload, app.core.chunker, app.services.llm_service, app.services.edge_registry
EXTERNAL_CONFIG: config/types.yaml, config/prompts.yaml
@ -22,7 +22,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,7 +44,7 @@ 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 # WP-20 Integration
from app.services.llm_service import LLMService
logger = logging.getLogger(__name__)
@ -52,7 +52,9 @@ logger = logging.getLogger(__name__)
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 {}
@ -65,35 +67,23 @@ def resolve_note_type(requested: Optional[str], reg: dict) -> str:
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
"""
"""Ermittelt den Namen des zu nutzenden Chunk-Profils."""
override = fm.get("chunking_profile") or fm.get("chunk_profile")
if override and isinstance(override, str):
return override
if override and isinstance(override, str): return override
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
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
"""
"""Ermittelt das effektive retriever_weight für das Scoring."""
override = fm.get("retriever_weight")
if override is not None:
try: return float(override)
except: pass
t_cfg = reg.get("types", {}).get(note_type, {})
if t_cfg and "retriever_weight" in t_cfg:
return float(t_cfg["retriever_weight"])
if t_cfg and "retriever_weight" in t_cfg: return float(t_cfg["retriever_weight"])
return float(reg.get("defaults", {}).get("retriever_weight", 1.0))
@ -106,13 +96,13 @@ class IngestionService:
self.cfg = QdrantConfig.from_env()
self.cfg.prefix = self.prefix
self.client = get_client(self.cfg)
self.dim = self.cfg.dim if hasattr(self.cfg, 'dim') else self.settings.VECTOR_SIZE
self.dim = self.settings.VECTOR_SIZE # Synchronisiert mit Settings v0.6.2
self.registry = load_type_registry()
self.embedder = EmbeddingsClient()
self.llm = LLMService() # WP-20
self.llm = LLMService()
# Change Detection Modus (full oder body)
self.active_hash_mode = os.getenv("MINDNET_CHANGE_DETECTION_MODE", "full")
# WP-22: Change Detection Modus aus Settings
self.active_hash_mode = self.settings.CHANGE_DETECTION_MODE
try:
ensure_collections(self.client, self.prefix, self.dim)
@ -133,20 +123,20 @@ class IngestionService:
async def _perform_smart_edge_allocation(self, text: str, note_id: str) -> List[Dict]:
"""
WP-20: Nutzt den Hybrid LLM Service für die semantische Kanten-Extraktion.
QUOTEN-SCHUTZ: Bevorzugt OpenRouter (Gemma), um Gemini-Tageslimits zu schützen.
QUOTEN-SCHUTZ: Bevorzugt OpenRouter (Gemma 2), um Gemini-Tageslimits zu schonen.
"""
# Bestimme Provider: Nutze OpenRouter falls Key vorhanden, sonst Global Default
provider = "openrouter" if getattr(self.settings, "OPENROUTER_API_KEY", None) else self.settings.MINDNET_LLM_PROVIDER
# Bestimme Provider: Nutze OpenRouter falls Key vorhanden
provider = "openrouter" if self.settings.OPENROUTER_API_KEY else self.settings.MINDNET_LLM_PROVIDER
model = self.settings.GEMMA_MODEL # Hochdurchsatz-Modell aus config.py
# Nutze Gemma-Modell für hohen Durchsatz via OpenRouter/Google
model = getattr(self.settings, "GEMMA_MODEL", None)
logger.info(f"🚀 [Ingestion] Turbo-Mode: Extracting edges for '{note_id}' using {model} on {provider}")
# Hole das optimierte Prompt-Template (Key: edge_extraction)
# Hole das optimierte Prompt-Template (Kaskade: Provider -> gemini -> ollama)
template = self.llm.get_prompt("edge_extraction", provider)
prompt = template.format(text=text[:6000], note_id=note_id)
try:
# Hintergrund-Task mit Semaphore via LLMService
# Hintergrund-Task mit Semaphore via LLMService (WP-06)
response_json = await self.llm.generate_raw_response(
prompt=prompt,
priority="background",
@ -156,13 +146,12 @@ class IngestionService:
)
data = json.loads(response_json)
# Metadaten für die Edge-Herkunft setzen
for item in data:
item["provenance"] = "semantic_ai"
item["line"] = f"ai-{provider}"
return data
except Exception as e:
logger.warning(f"Smart Edge Allocation failed for {note_id}: {e}")
logger.warning(f"⚠️ [Ingestion] Smart Edge Allocation failed for {note_id} on {provider}: {e}")
return []
async def process_file(
@ -214,7 +203,7 @@ class IngestionService:
except Exception as e:
return {**result, "error": f"Payload build failed: {str(e)}"}
# 4. Change Detection (Multi-Hash)
# 4. Change Detection (WP-22 Multi-Hash)
old_payload = None
if not force_replace:
old_payload = self._fetch_note_payload(note_id)
@ -241,7 +230,7 @@ class IngestionService:
try:
body_text = getattr(parsed, "body", "") or ""
# STABILITY PATCH: Prüfen, ob ensure_latest existiert (verhindert AttributeError)
# WP-22 STABILITY PATCH: Prüfen, ob ensure_latest existiert
if hasattr(edge_registry, "ensure_latest"):
edge_registry.ensure_latest()
@ -264,13 +253,13 @@ class IngestionService:
e["kind"] = edge_registry.resolve(edge_type=e["kind"], provenance="explicit", context={**context, "line": e.get("line")})
edges.append(e)
# B. WP-20: Smart AI Edges (Hybrid Acceleration)
# B. WP-20: Smart AI Edges (Hybrid Turbo Acceleration)
ai_edges = await self._perform_smart_edge_allocation(body_text, note_id)
for e in ai_edges:
e["kind"] = edge_registry.resolve(edge_type=e.get("kind"), provenance="semantic_ai", context={**context, "line": e.get("line")})
edges.append(e)
# C. System-Kanten
# C. System-Kanten (Struktur)
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:
@ -283,7 +272,7 @@ class IngestionService:
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
@ -303,7 +292,7 @@ class IngestionService:
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)
logger.error(f"Upsert failed for {note_id}: {e}", exc_info=True)
return {**result, "error": f"DB Upsert failed: {e}"}
def _fetch_note_payload(self, note_id: str) -> Optional[dict]:
@ -330,4 +319,20 @@ class IngestionService:
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
except Exception: 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."""
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)}"}
return await self.process_file(file_path=file_path, vault_root=vault_root, apply=True, force_replace=True, purge_before=True)

View File

@ -2,7 +2,10 @@
FILE: app/services/edge_registry.py
DESCRIPTION: Single Source of Truth für Kanten-Typen mit dynamischem Reload.
WP-22: Fix für absolute Pfade außerhalb des Vaults (Prod-Dictionary).
VERSION: 0.7.4 (Fix: Absolute Path Escaping & Quote Stripping)
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
@ -25,69 +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()
# 1. ENV-Werte laden und von Anführungszeichen bereinigen
env_vocab_path = os.getenv("MINDNET_VOCAB_PATH")
if env_vocab_path:
env_vocab_path = env_vocab_path.strip('"').strip("'")
env_vault_root = os.getenv("MINDNET_VAULT_ROOT") or getattr(settings, "MINDNET_VAULT_ROOT", "./vault")
if env_vault_root:
env_vault_root = env_vault_root.strip('"').strip("'")
# 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)
# 2. Pfad-Priorität: Wenn absolut (/), dann direkt nutzen!
if env_vocab_path:
if env_vocab_path.startswith("/"):
# Absoluter Pfad (z.B. aus Produktion)
self.full_vocab_path = env_vocab_path
else:
# Relativer Pfad zum aktuellen Verzeichnis oder Vault
self.full_vocab_path = os.path.abspath(env_vocab_path)
else:
# Fallback: Suche im Vault-Verzeichnis
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])
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
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."""
"""Parst das Markdown-Wörterbuch und baut die Canonical-Map auf."""
self.canonical_map.clear()
self.valid_types.clear()
# Regex für Tabellen-Struktur: | **Typ** | Aliase |
pattern = re.compile(r"\|\s*\*\*`?([a-zA-Z0-9_-]+)`?\*\*\s*\|\s*([^|]+)\|")
try:
@ -106,38 +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, Underscores statt Leerzeichen
clean_alias = alias.replace("`", "").lower().strip().replace(" ", "_")
self.canonical_map[clean_alias] = canonical
c_aliases += 1
print(f"=== [EDGE-REGISTRY SUCCESS] Loaded {c_types} Canonical Types and {c_aliases} Aliases ===", flush=True)
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"!!! [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 {}
# 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"
# System-Kanten sind nur bei struktureller Provenienz erlaubt
if provenance == "structure" and clean_type in self.FORBIDDEN_SYSTEM_EDGES:
return clean_type
# Mapping auf kanonischen Namen
if clean_type in self.canonical_map:
return self.canonical_map[clean_type]
# 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 = {
@ -150,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()

View File

@ -3,7 +3,8 @@ FILE: app/services/llm_service.py
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.
VERSION: 3.3.1
WP-20 Fix: Bulletproof Prompt-Auflösung für format() Aufrufe.
VERSION: 3.3.2
STATUS: Active
"""
import httpx
@ -58,11 +59,14 @@ class LLMService:
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) or {}
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:
@ -70,12 +74,22 @@ class LLMService:
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 (weil ähnlich leistungsfähig), dann Ollama
return data.get(active_provider, data.get("gemini", data.get("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(
@ -116,6 +130,7 @@ class LLMService:
async def _execute_google(self, prompt, system, force_json, model_override):
"""Native Google SDK Integration (Gemini)."""
# Nutzt GEMINI_MODEL aus config.py falls kein override (z.B. für Ingestion) übergeben wurde
model = model_override or self.settings.GEMINI_MODEL
config = types.GenerateContentConfig(
system_instruction=system,
@ -130,7 +145,8 @@ class LLMService:
async def _execute_openrouter(self, prompt, system, force_json, model_override):
"""OpenRouter API Integration (OpenAI-kompatibel)."""
model = model_override or getattr(self.settings, "OPENROUTER_MODEL", "google/gemma-2-9b-it:free")
# Nutzt OPENROUTER_MODEL aus config.py (v0.6.2)
model = model_override or self.settings.OPENROUTER_MODEL
messages = []
if system:
messages.append({"role": "system", "content": system})
@ -166,7 +182,7 @@ class LLMService:
except Exception as e:
attempt += 1
if attempt > max_retries:
logger.error(f"Ollama Error after {attempt} retries: {e}")
logger.error(f"Ollama Error after {attempt} retries: {e}")
raise e
wait_time = base_delay * (2 ** (attempt - 1))
logger.warning(f"⚠️ Ollama attempt {attempt} failed. Retrying in {wait_time}s...")

View File

@ -1,8 +1,9 @@
"""
FILE: app/services/semantic_analyzer.py
DESCRIPTION: KI-gestützte Kanten-Validierung. Nutzt LLM (Background-Priority), um Kanten präzise einem Chunk zuzuordnen.
WP-20 Fix: Kompatibilität mit Provider-basierten Prompt-Dictionaries (Hybrid-Modus).
VERSION: 2.2.0
WP-20 Fix: Volle Kompatibilität mit der gehärteten LLMService (v3.3.2) Kaskade.
WP-20: Unterstützung für Provider-spezifische Routing-Logik beim Import.
VERSION: 2.2.1
STATUS: Active
DEPENDENCIES: app.services.llm_service, json, logging
LAST_ANALYSIS: 2025-12-23
@ -53,16 +54,17 @@ class SemanticAnalyzer:
Sendet einen Chunk und eine Liste potenzieller Kanten an das LLM.
Das LLM filtert heraus, welche Kanten für diesen Chunk relevant sind.
WP-20 Fix: Nutzt get_prompt(), um den 'AttributeError: dict object' zu vermeiden.
WP-20 Fix: Nutzt get_prompt(), um den 'AttributeError: dict object' sicher zu vermeiden.
"""
if not all_edges:
return []
# 1. Prompt laden via get_prompt (handelt die Provider-Kaskade automatisch ab) [WP-20 Fix]
# 1. Prompt laden via get_prompt (handelt die Provider-Kaskade automatisch ab)
prompt_template = self.llm.get_prompt("edge_allocation_template")
# Sicherheits-Check für die Format-Methode
if not prompt_template or isinstance(prompt_template, dict):
logger.warning("⚠️ [SemanticAnalyzer] Prompt 'edge_allocation_template' konnte nicht als String geladen werden. Nutze Hard-Fallback.")
logger.warning("⚠️ [SemanticAnalyzer] Prompt 'edge_allocation_template' konnte nicht als String geladen werden. Nutze Not-Fallback.")
prompt_template = (
"TASK: Wähle aus den Kandidaten die relevanten Kanten für den Text.\n"
"TEXT: {chunk_text}\n"
@ -76,7 +78,7 @@ class SemanticAnalyzer:
# LOG: Request Info
logger.debug(f"🔍 [SemanticAnalyzer] Request: {len(chunk_text)} chars Text, {len(all_edges)} Candidates.")
# 3. Prompt füllen (Hier trat der AttributeError auf, wenn prompt_template ein dict war)
# 3. Prompt füllen (Nutzt nun sicher einen String dank LLMService v3.3.2)
try:
final_prompt = prompt_template.format(
chunk_text=chunk_text[:3500],
@ -88,6 +90,7 @@ class SemanticAnalyzer:
try:
# 4. LLM Call mit Traffic Control (Background Priority)
# Der gewählte Provider wird über die .env gesteuert (MINDNET_LLM_PROVIDER)
response_json = await self.llm.generate_raw_response(
prompt=final_prompt,
force_json=True,
@ -132,7 +135,7 @@ class SemanticAnalyzer:
if isinstance(target, str):
raw_candidates.append(f"{key}:{target}")
# 7. Strict Validation Loop
# 7. Strict Validation Loop (Übernahme aus deiner V2.2.0)
for e in raw_candidates:
e_str = str(e)
if self._is_valid_edge_string(e_str):

View File

@ -41,7 +41,7 @@ strategies:
# 1. Fakten-Abfrage (Fallback & Default)
FACT:
description: "Reine Wissensabfrage."
preferred_provider: "ollama" # Schnell und lokal ausreichend
preferred_provider: "openrouter" # Schnell und lokal ausreichend
trigger_keywords: []
inject_types: []
# WP-22: Definitionen & Hierarchien bevorzugen

View File

@ -1,4 +1,5 @@
# config/prompts.yaml — Final V2.3.1 (Multi-Personality Support)
# config/prompts.yaml — Final V2.4.0 (Hybrid & Multi-Provider Support)
# WP-20: Optimierte Cloud-Templates bei unveränderten Ollama-Prompts.
system_prompt: |
Du bist 'mindnet', mein digitaler Zwilling und strategischer Partner.
@ -29,11 +30,13 @@ rag_template:
Beantworte die Frage präzise basierend auf den Quellen.
Fasse die Informationen zusammen. Sei objektiv und neutral.
gemini: |
Analysiere diesen Kontext meines digitalen Zwillings:
Nutze das Wissen meines digitalen Zwillings aus folgendem Kontext: {context_str}
Beantworte die Anfrage präzise, detailliert und strukturiert: {query}
openrouter: |
Kontext-Analyse für Gemma/Llama:
{context_str}
Beantworte die Anfrage detailliert und prüfe auf Widersprüche: {query}
openrouter: "Kontext-Analyse für Gemma/Llama: {context_str}\n\nAnfrage: {query}"
Anfrage: {query}
# ---------------------------------------------------------
# 2. DECISION: Strategie & Abwägung (Intent: DECISION)
@ -57,11 +60,16 @@ decision_template:
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)
- **Empfehlung:** (Klare Meinung: Ja/No/Vielleicht mit Begründung)
gemini: |
Agierte als Senior Strategy Consultant. Nutze den Kontext {context_str}, um die Frage {query}
tiefgreifend gegen meine langfristigen Ziele abzuwägen.
openrouter: "Strategischer Check (OpenRouter): {query}\n\nReferenzdaten: {context_str}"
Agiere als Senior Strategy Consultant für meinen digitalen Zwilling.
Wäge die Frage {query} multiperspektivisch gegen meine Werte und langfristigen Ziele ab.
Kontext: {context_str}
openrouter: |
Strategischer Check via OpenRouter/Gemma:
Analyse der Entscheidungsfrage: {query}
Referenzdaten aus dem Graph: {context_str}
# ---------------------------------------------------------
# 3. EMPATHY: Der Spiegel / "Ich"-Modus (Intent: EMPATHY)
# ---------------------------------------------------------
@ -83,6 +91,13 @@ empathy_template:
TONFALL:
Ruhig, verständnisvoll, reflektiert. Keine Aufzählungszeichen, sondern fließender Text.
gemini: |
Reflektiere meine aktuelle Situation {query} basierend auf meinen Werten {context_str}.
Sei mein empathischer digitaler Zwilling. Antworte als 'Ich'.
openrouter: |
Empathische Reflexion (OpenRouter):
Situation: {query}
Persönlicher Kontext: {context_str}
# ---------------------------------------------------------
# 4. TECHNICAL: Der Coder (Intent: CODING)
@ -107,6 +122,14 @@ technical_template:
- Kurze Erklärung des Ansatzes.
- Markdown Code-Block (Copy-Paste fertig).
- Wichtige Edge-Cases.
gemini: |
Du bist Senior Software Engineer. Löse die technische Aufgabe {query}
unter Berücksichtigung meiner Dokumentation: {context_str}.
openrouter: |
Technischer Support via OpenRouter:
Task: {query}
Kontext-Snippets: {context_str}
# ---------------------------------------------------------
# 5. INTERVIEW: Der "One-Shot Extractor" (Performance Mode)
# ---------------------------------------------------------
@ -142,9 +165,8 @@ interview_template:
## (Zweiter Begriff aus STRUKTUR)
(Text...)
(usw.)
gemini: "Transformiere den Input {query} in das Schema {schema_fields} für Typ {target_type}."
openrouter: "Extrahiere Daten für Typ {target_type} aus {query}. Schema: {schema_fields}."
# ---------------------------------------------------------
# 6. EDGE_ALLOCATION: Kantenfilter (Intent: OFFLINE_FILTER)
@ -164,24 +186,37 @@ edge_allocation_template:
{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.).
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. Nutze exakt die Schreibweise aus der Liste.
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: |
Extrahiere semantische Kanten für den Graphen ({note_id}).
Finde auch implizite Verbindungen.
JSON: [{"to": "X", "kind": "Y", "reason": "Z"}].
TEXT: {text}
Analysiere den Textabschnitt: {chunk_text}
Wähle aus folgender Liste alle relevanten Kanten aus: {edge_list}
Antworte STRIKT als JSON-Liste von Strings im Format ["typ:ziel"].
Kein Text davor oder danach!
openrouter: |
Filtere die relevanten Kanten für den Graphen.
Kandidaten: {edge_list}
Text: {chunk_text}
Output: JSON-Liste ["typ:ziel"].
# ---------------------------------------------------------
# 7. SMART EDGE ALLOCATION: Extraktion (Intent: INGEST)
# ---------------------------------------------------------
edge_extraction:
ollama: |
Extrahiere Kanten als JSON: [{"to": "X", "kind": "Y"}].
Text: {text}
gemini: |
Führe eine semantische Analyse der Notiz '{note_id}' durch.
Finde explizite und implizite Relationen.
Antworte als JSON: [{"to": "Ziel", "kind": "typ", "reason": "begründung"}]
Keine Erklärungen, nur JSON.
Text: {text}
openrouter: |
Analysiere den Text für den Graphen. Identifiziere semantische Verbindungen.
Output JSON: [{"to": "X", "kind": "Y"}].
Output STRIKT als JSON-Liste: [{"to": "X", "kind": "Y"}].
Text: {text}