WP25a #20

Merged
Lars merged 7 commits from WP25a into main 2026-01-02 13:55:09 +01:00
20 changed files with 1160 additions and 347 deletions

View File

@ -1,11 +1,11 @@
"""
FILE: app/core/ingestion/ingestion_processor.py
DESCRIPTION: Der zentrale IngestionService (Orchestrator).
WP-14: Modularisierung der Datenbank-Ebene (app.core.database).
WP-25a: Integration der Mixture of Experts (MoE) Architektur.
WP-15b: Two-Pass Workflow mit globalem Kontext-Cache.
WP-20/22: Cloud-Resilienz und Content-Lifecycle integriert.
AUDIT v2.13.12: Synchronisierung der Profil-Auflösung mit Registry-Defaults.
VERSION: 2.13.12
AUDIT v2.14.0: Synchronisierung der Profil-Auflösung mit MoE-Experten.
VERSION: 2.14.0 (WP-25a: MoE & Profile Support)
STATUS: Active
"""
import logging
@ -55,11 +55,15 @@ class IngestionService:
# Synchronisierung der Konfiguration mit dem Instanz-Präfix
self.cfg.prefix = self.prefix
self.client = get_client(self.cfg)
self.dim = self.settings.VECTOR_SIZE
self.registry = load_type_registry()
self.embedder = EmbeddingsClient()
self.llm = LLMService()
# WP-25a: Auflösung der Dimension über das Embedding-Profil (MoE)
embed_cfg = self.llm.profiles.get("embedding_expert", {})
self.dim = embed_cfg.get("dimensions") or self.settings.VECTOR_SIZE
# Festlegen, welcher Hash für die Change-Detection maßgeblich ist
self.active_hash_mode = self.settings.CHANGE_DETECTION_MODE
self.batch_cache: Dict[str, NoteContext] = {} # WP-15b LocalBatchCache
@ -155,24 +159,21 @@ class IngestionService:
edge_registry.ensure_latest()
# Profil-Auflösung via Registry
# FIX: Wir nutzen das Profil, das bereits in make_note_payload unter
# Berücksichtigung der types.yaml (Registry) ermittelt wurde.
profile = note_pl.get("chunk_profile", "sliding_standard")
chunk_cfg = get_chunk_config_by_profile(self.registry, profile, note_type)
enable_smart = chunk_cfg.get("enable_smart_edge_allocation", False)
# WP-15b: Chunker-Aufruf bereitet den Candidate-Pool pro Chunk vor.
# assemble_chunks führt intern auch die Propagierung durch.
chunks = await assemble_chunks(note_id, body_text, note_type, config=chunk_cfg)
# Semantische Kanten-Validierung (Smart Edge Allocation)
# Semantische Kanten-Validierung (Smart Edge Allocation via MoE-Profil)
for ch in chunks:
filtered = []
for cand in getattr(ch, "candidate_pool", []):
# Nur global_pool Kandidaten (aus dem Pool am Ende) erfordern KI-Validierung
# WP-25a: Nutzt nun das spezialisierte Validierungs-Profil
if cand.get("provenance") == "global_pool" and enable_smart:
if await validate_edge_candidate(ch.text, cand, self.batch_cache, self.llm, self.settings.MINDNET_LLM_PROVIDER):
if await validate_edge_candidate(ch.text, cand, self.batch_cache, self.llm, profile_name="ingest_validator"):
filtered.append(cand)
else:
# Explizite Kanten (Wikilinks/Callouts) werden ungeprüft übernommen
@ -204,7 +205,6 @@ class IngestionService:
)
# 4. DB Upsert via modularisierter Points-Logik
# WICHTIG: Wenn sich der Inhalt geändert hat, löschen wir erst alle alten Fragmente.
if purge_before and old_payload:
purge_artifacts(self.client, self.prefix, note_id)

View File

@ -1,10 +1,16 @@
"""
FILE: app/core/ingestion/ingestion_validation.py
DESCRIPTION: WP-15b semantische Validierung von Kanten gegen den LocalBatchCache.
AUDIT v2.12.3: Integration der zentralen Text-Bereinigung (WP-14).
WP-25a: Integration der Mixture of Experts (MoE) Profil-Steuerung.
VERSION: 2.13.0 (WP-25a: MoE & Profile Support)
STATUS: Active
FIX:
- Umstellung auf generate_raw_response mit profile_name="ingest_validator".
- Automatische Nutzung der Fallback-Kaskade (Cloud -> Lokal) via LLMService.
- Erhalt der sparsamen LLM-Nutzung (Validierung nur für Kandidaten-Pool).
"""
import logging
from typing import Dict, Any
from typing import Dict, Any, Optional
from app.core.parser import NoteContext
# ENTSCHEIDENDER FIX: Import der neutralen Bereinigungs-Logik zur Vermeidung von Circular Imports
@ -17,11 +23,12 @@ async def validate_edge_candidate(
edge: Dict,
batch_cache: Dict[str, NoteContext],
llm_service: Any,
provider: str
provider: Optional[str] = None,
profile_name: str = "ingest_validator"
) -> bool:
"""
WP-15b: Validiert einen Kandidaten semantisch gegen das Ziel im Cache.
Nutzt clean_llm_text zur Entfernung von Steuerzeichen vor der Auswertung.
Nutzt das MoE-Profil 'ingest_validator' für deterministische YES/NO Prüfungen.
"""
target_id = edge.get("to")
target_ctx = batch_cache.get(target_id)
@ -32,14 +39,16 @@ async def validate_edge_candidate(
target_ctx = batch_cache.get(base_id)
# Sicherheits-Fallback (Hard-Link Integrity)
# Explizite Wikilinks oder Callouts werden nicht durch das LLM verifiziert.
if not target_ctx:
logger.info(f" [VALIDATION SKIP] No context for '{target_id}' - allowing link.")
return True
# Prompt-Abruf (Nutzt Provider-String als Fallback-Key für die prompts.yaml)
template = llm_service.get_prompt("edge_validation", provider)
try:
logger.info(f"⚖️ [VALIDATING] Relation '{edge.get('kind')}' -> '{target_id}'...")
logger.info(f"⚖️ [VALIDATING] Relation '{edge.get('kind')}' -> '{target_id}' (Profile: {profile_name})...")
prompt = template.format(
chunk_text=chunk_text[:1500],
target_title=target_ctx.title,
@ -47,8 +56,13 @@ async def validate_edge_candidate(
edge_kind=edge.get("kind", "related_to")
)
# Die Antwort vom Service anfordern
raw_response = await llm_service.generate_raw_response(prompt, priority="background")
# WP-25a: Profilbasierter Aufruf (Delegiert Fallbacks an den Service)
# Nutzt ingest_validator (Cloud Mistral/Gemini -> Local Phi3:mini Kaskade)
raw_response = await llm_service.generate_raw_response(
prompt,
priority="background",
profile_name=profile_name
)
# WP-14 Fix: Zusätzliche Bereinigung zur Sicherstellung der Interpretierbarkeit
response = clean_llm_text(raw_response)
@ -62,6 +76,6 @@ async def validate_edge_candidate(
logger.info(f"🚫 [REJECTED] Relation to '{target_id}' irrelevant for this chunk.")
return is_valid
except Exception as e:
logger.warning(f"⚠️ Validation error for {target_id}: {e}")
logger.warning(f"⚠️ Validation error for {target_id} using {profile_name}: {e}")
# Im Zweifel (Timeout/Fehler) erlauben wir die Kante, um Datenverlust zu vermeiden
return True

View File

@ -1,13 +1,16 @@
"""
FILE: app/core/retrieval/decision_engine.py
DESCRIPTION: Der Agentic Orchestrator für WP-25.
DESCRIPTION: Der Agentic Orchestrator für MindNet (WP-25a Edition).
Realisiert Multi-Stream Retrieval, Intent-basiertes Routing
und parallele Wissens-Synthese.
VERSION: 1.0.3
und die neue Pre-Synthesis Kompression (Module A).
VERSION: 1.2.1 (WP-25a: Profile-Driven Orchestration & Optimized Cascade)
STATUS: Active
FIX:
- WP-25 STREAM-TRACING: Kennzeichnung der Treffer mit ihrem Ursprungs-Stream.
- WP-25 ROBUSTNESS: Pre-Initialization der Stream-Variablen zur Vermeidung von KeyErrors.
- WP-25a: Volle Integration der Profil-Kaskade (Delegation an LLMService v3.5.2).
- WP-25a: Dynamische Nutzung des 'router_profile' für die Intent-Erkennung.
- WP-25a: Parallelisierte Kompression überlanger Wissens-Streams.
- WP-25: Beibehaltung von Stream-Tracing und Pre-Initialization Robustness.
- CLEANUP: Entfernung redundanter Fallback-Blocks (jetzt im LLMService).
"""
import asyncio
import logging
@ -32,7 +35,7 @@ class DecisionEngine:
self.config = self._load_engine_config()
def _load_engine_config(self) -> Dict[str, Any]:
"""Lädt die Multi-Stream Konfiguration (WP-25)."""
"""Lädt die Multi-Stream Konfiguration (WP-25/25a)."""
path = os.getenv("MINDNET_DECISION_CONFIG", "config/decision_engine.yaml")
if not os.path.exists(path):
logger.error(f"❌ Decision Engine Config not found at {path}")
@ -47,9 +50,9 @@ class DecisionEngine:
async def ask(self, query: str) -> str:
"""
Hauptmethode des MindNet Chats.
Orchestriert den gesamten Prozess: Routing -> Retrieval -> Synthese.
Orchestriert den agentischen Prozess: Routing -> Retrieval -> Kompression -> Synthese.
"""
# 1. Intent Recognition
# 1. Intent Recognition (Strategy Routing)
strategy_key = await self._determine_strategy(query)
strategies = self.config.get("strategies", {})
@ -67,23 +70,29 @@ class DecisionEngine:
if not strategy:
return "Entschuldigung, meine Wissensbasis ist aktuell nicht konfiguriert."
# 2. Multi-Stream Retrieval
# 2. Multi-Stream Retrieval & Pre-Synthesis (Parallel Tasks)
# WP-25a: Diese Methode übernimmt nun auch die Kompression.
stream_results = await self._execute_parallel_streams(strategy, query)
# 3. Synthese
# 3. Finale Synthese
return await self._generate_final_answer(strategy_key, strategy, query, stream_results)
async def _determine_strategy(self, query: str) -> str:
"""Nutzt den LLM-Router zur Wahl der Such-Strategie."""
prompt_key = self.config.get("settings", {}).get("router_prompt_key", "intent_router_v1")
"""Nutzt den LLM-Router zur Wahl der Such-Strategie via router_profile."""
settings_cfg = self.config.get("settings", {})
prompt_key = settings_cfg.get("router_prompt_key", "intent_router_v1")
# WP-25a: Nutzt das spezialisierte Profil für das Routing
router_profile = settings_cfg.get("router_profile")
router_prompt_template = self.llm_service.get_prompt(prompt_key)
if not router_prompt_template:
return "FACT_WHAT"
full_prompt = router_prompt_template.format(query=query)
try:
# Der LLMService übernimmt hier über das Profil bereits die Fallback-Kaskade
response = await self.llm_service.generate_raw_response(
full_prompt, max_retries=1, priority="realtime"
full_prompt, max_retries=1, priority="realtime", profile_name=router_profile
)
return str(response).strip().upper()
except Exception as e:
@ -91,35 +100,85 @@ class DecisionEngine:
return "FACT_WHAT"
async def _execute_parallel_streams(self, strategy: Dict, query: str) -> Dict[str, str]:
"""Führt Such-Streams gleichzeitig aus."""
"""
Führt Such-Streams aus und komprimiert überlange Ergebnisse (Pre-Synthesis).
WP-25a: MoE-Profile werden für die Kompression berücksichtigt.
"""
stream_keys = strategy.get("use_streams", [])
library = self.config.get("streams_library", {})
tasks = []
# Phase 1: Retrieval Tasks starten
retrieval_tasks = []
active_streams = []
for key in stream_keys:
stream_cfg = library.get(key)
if stream_cfg:
active_streams.append(key)
tasks.append(self._run_single_stream(key, stream_cfg, query))
retrieval_tasks.append(self._run_single_stream(key, stream_cfg, query))
results = await asyncio.gather(*tasks, return_exceptions=True)
# Ergebnisse sammeln (Exceptions werden als Objekte zurückgegeben)
retrieval_results = await asyncio.gather(*retrieval_tasks, return_exceptions=True)
mapped_results = {}
for name, res in zip(active_streams, results):
# Phase 2: Formatierung und optionale Kompression
final_stream_tasks = []
for name, res in zip(active_streams, retrieval_results):
if isinstance(res, Exception):
logger.error(f"Stream '{name}' failed: {res}")
mapped_results[name] = "[Fehler beim Abruf dieses Wissens-Streams]"
else:
mapped_results[name] = self._format_stream_context(res)
logger.error(f"Stream '{name}' failed during retrieval: {res}")
async def _err(): return "[Fehler beim Abruf dieses Wissens-Streams]"
final_stream_tasks.append(_err())
continue
return mapped_results
# Formatierung der Hits in Text
formatted_context = self._format_stream_context(res)
# WP-25a: Kompressions-Check (Inhaltsverdichtung)
stream_cfg = library.get(name, {})
threshold = stream_cfg.get("compression_threshold", 4000)
if len(formatted_context) > threshold:
logger.info(f"⚙️ [WP-25a] Compressing stream '{name}' ({len(formatted_context)} chars)...")
comp_profile = stream_cfg.get("compression_profile")
final_stream_tasks.append(
self._compress_stream_content(name, formatted_context, query, comp_profile)
)
else:
# Direkt-Übernahme als Coroutine für gather()
async def _direct(c=formatted_context): return c
final_stream_tasks.append(_direct())
# Finale Inhalte (evtl. komprimiert) parallel fertigstellen
final_contents = await asyncio.gather(*final_stream_tasks)
return dict(zip(active_streams, final_contents))
async def _compress_stream_content(self, stream_name: str, content: str, query: str, profile: Optional[str]) -> str:
"""
WP-25a Module A: Inhaltsverdichtung via Experten-Modell.
"""
compression_prompt = (
f"Du bist ein Wissens-Analyst. Reduziere den folgenden Wissens-Stream '{stream_name}' "
f"auf die Informationen, die für die Beantwortung der Frage '{query}' absolut notwendig sind.\n\n"
f"BEIBEHALTEN: Harte Fakten, Projektnamen, konkrete Werte und Quellenangaben.\n"
f"ENTFERNEN: Redundante Einleitungen, Füllwörter und irrelevante Details.\n\n"
f"STREAM-INHALT:\n{content}\n\n"
f"KOMPRIMIERTE ANALYSE:"
)
try:
summary = await self.llm_service.generate_raw_response(
compression_prompt,
profile_name=profile, # WP-25a: MoE Support
priority="background",
max_retries=1
)
return summary.strip() if (summary and len(summary.strip()) > 10) else content
except Exception as e:
logger.error(f"❌ Compression of {stream_name} failed: {e}")
return content
async def _run_single_stream(self, name: str, cfg: Dict, query: str) -> QueryResponse:
"""
Bereitet eine spezialisierte Suche vor.
WP-25: Taggt die Treffer mit ihrem Ursprungs-Stream.
"""
"""Spezialisierte Graph-Suche mit Stream-Tracing (WP-25)."""
transformed_query = cfg.get("query_template", "{query}").format(query=query)
request = QueryRequest(
@ -131,18 +190,16 @@ class DecisionEngine:
explain=True
)
# Retrieval ausführen
response = await self.retriever.search(request)
# WP-25: STREAM-TRACING
# Markiere jeden Treffer mit dem Namen des Quell-Streams
for hit in response.results:
hit.stream_origin = name
return response
def _format_stream_context(self, response: QueryResponse) -> str:
"""Wandelt QueryHits in Kontext-Strings um."""
"""Wandelt QueryHits in einen formatierten Kontext-String um."""
if not response.results:
return "Keine spezifischen Informationen in diesem Stream gefunden."
@ -161,12 +218,13 @@ class DecisionEngine:
query: str,
stream_results: Dict[str, str]
) -> str:
"""Führt die Synthese durch."""
provider = strategy.get("preferred_provider") or self.settings.MINDNET_LLM_PROVIDER
"""Führt die finale Synthese basierend auf dem Strategie-Profil durch."""
# WP-25a: Nutzt das llm_profile der Strategie
profile = strategy.get("llm_profile")
template_key = strategy.get("prompt_template", "rag_template")
template = self.llm_service.get_prompt(template_key, provider=provider)
system_prompt = self.llm_service.get_prompt("system_prompt", provider=provider)
template = self.llm_service.get_prompt(template_key)
system_prompt = self.llm_service.get_prompt("system_prompt")
# WP-25 ROBUSTNESS: Pre-Initialization
all_possible_streams = ["values_stream", "facts_stream", "biography_stream", "risk_stream", "tech_stream"]
@ -181,13 +239,10 @@ class DecisionEngine:
if prepend:
final_prompt = f"{prepend}\n\n{final_prompt}"
# WP-25a: MoE Call mit automatisierter Kaskade im LLMService
# (Frühere manuelle Fallback-Blocks wurden entfernt, da v3.5.2 dies intern löst)
response = await self.llm_service.generate_raw_response(
final_prompt, system=system_prompt, provider=provider, priority="realtime"
)
if not response or len(response.strip()) < 5:
return await self.llm_service.generate_raw_response(
final_prompt, system=system_prompt, provider="ollama", priority="realtime"
final_prompt, system=system_prompt, profile_name=profile, priority="realtime"
)
return response
@ -195,9 +250,10 @@ class DecisionEngine:
except KeyError as e:
logger.error(f"Template Variable mismatch in '{template_key}': Missing {e}")
fallback_context = "\n\n".join([v for v in stream_results.values() if v])
# WP-25a FIX: Nutzt auch im Fallback das Strategie-Profil für Konsistenz
return await self.llm_service.generate_raw_response(
f"Beantworte: {query}\n\nKontext:\n{fallback_context}",
system=system_prompt, priority="realtime"
system=system_prompt, priority="realtime", profile_name=profile
)
except Exception as e:
logger.error(f"Final Synthesis failed: {e}")

View File

@ -1,8 +1,9 @@
"""
FILE: app/main.py
DESCRIPTION: Bootstrap der FastAPI Anwendung für WP-25 (Agentic RAG).
DESCRIPTION: Bootstrap der FastAPI Anwendung für WP-25a (Agentic MoE).
Orchestriert Lifespan-Events, globale Fehlerbehandlung und Routing.
VERSION: 1.0.0 (WP-25 Release)
Prüft beim Start die Integrität der Mixture of Experts Konfiguration.
VERSION: 1.1.0 (WP-25a: MoE Integrity Check)
STATUS: Active
DEPENDENCIES: app.config, app.routers.*, app.services.llm_service
"""
@ -32,63 +33,74 @@ except Exception:
from .core.logging_setup import setup_logging
# Initialisierung noch VOR create_app()
# Initialisierung des Loggings noch VOR create_app()
setup_logging()
logger = logging.getLogger(__name__)
# --- WP-25: Lifespan Management ---
# --- WP-25a: Lifespan Management mit MoE Integritäts-Prüfung ---
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Verwaltet den Lebenszyklus der Anwendung.
Führt Startup-Prüfungen durch und bereinigt Ressourcen beim Shutdown.
Verwaltet den Lebenszyklus der Anwendung (Startup/Shutdown).
Verifiziert die Verfügbarkeit der MoE-Experten-Profile und Strategien.
"""
settings = get_settings()
logger.info("🚀 mindnet API: Starting up (WP-25 Agentic RAG Mode)...")
logger.info("🚀 mindnet API: Starting up (WP-25a MoE Mode)...")
# 1. Startup: Integritäts-Check der WP-25 Konfiguration
# Wir prüfen, ob die für die DecisionEngine kritischen Dateien vorhanden sind.
# 1. Startup: Integritäts-Check der MoE Konfiguration
# Wir prüfen die drei Säulen der Agentic-RAG Architektur.
decision_cfg = os.getenv("MINDNET_DECISION_CONFIG", "config/decision_engine.yaml")
profiles_cfg = getattr(settings, "LLM_PROFILES_PATH", "config/llm_profiles.yaml")
prompts_cfg = settings.PROMPTS_PATH
if not os.path.exists(decision_cfg):
logger.error(f"❌ CRITICAL: Decision Engine config missing at {decision_cfg}")
if not os.path.exists(prompts_cfg):
logger.error(f"❌ CRITICAL: Prompts config missing at {prompts_cfg}")
missing_files = []
if not os.path.exists(decision_cfg): missing_files.append(decision_cfg)
if not os.path.exists(profiles_cfg): missing_files.append(profiles_cfg)
if not os.path.exists(prompts_cfg): missing_files.append(prompts_cfg)
if missing_files:
logger.error(f"❌ CRITICAL: Missing MoE config files: {missing_files}")
else:
logger.info("✅ MoE Configuration files verified.")
yield
# 2. Shutdown: Ressourcen bereinigen
logger.info("🛑 mindnet API: Shutting down...")
try:
llm = LLMService()
await llm.close()
logger.info("✨ Cleanup complete. Goodbye.")
logger.info("✨ LLM resources cleaned up.")
except Exception as e:
logger.warning(f"⚠️ Error during LLMService cleanup: {e}")
logger.info("Goodbye.")
# --- App Factory ---
def create_app() -> FastAPI:
"""Initialisiert die FastAPI App mit WP-25 Erweiterungen."""
"""Initialisiert die FastAPI App mit WP-25a Erweiterungen."""
app = FastAPI(
title="mindnet API",
version="1.0.0", # WP-25 Milestone
version="1.1.0", # WP-25a Milestone
lifespan=lifespan,
description="Digital Twin Knowledge Engine mit Agentic Multi-Stream RAG."
description="Digital Twin Knowledge Engine mit Mixture of Experts Orchestration."
)
s = get_settings()
# --- Globale Fehlerbehandlung (WP-25 Resilienz) ---
# --- Globale Fehlerbehandlung (WP-25a Resilienz) ---
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""Fängt unerwartete Fehler in der Multi-Stream Kette ab."""
"""Fängt unerwartete Fehler in der MoE-Prozesskette ab."""
logger.error(f"❌ Unhandled Engine Error: {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={
"detail": "Ein interner Fehler ist aufgetreten. Die DecisionEngine konnte die Anfrage nicht finalisieren.",
"detail": "Ein interner Fehler ist in der MoE-Kette aufgetreten.",
"error_type": type(exc).__name__
}
)
@ -96,12 +108,13 @@ def create_app() -> FastAPI:
# Healthcheck
@app.get("/healthz")
def healthz():
"""Bietet Statusinformationen über die Engine und Datenbank-Verbindung."""
return {
"status": "ok",
"version": "1.0.0",
"version": "1.1.0",
"qdrant": s.QDRANT_URL,
"prefix": s.COLLECTION_PREFIX,
"agentic_mode": True
"moe_enabled": True
}
# Inkludieren der Router (100% Kompatibilität erhalten)
@ -109,7 +122,7 @@ def create_app() -> FastAPI:
app.include_router(graph_router, prefix="/graph", tags=["graph"])
app.include_router(tools_router, prefix="/tools", tags=["tools"])
app.include_router(feedback_router, prefix="/feedback", tags=["feedback"])
app.include_router(chat_router, prefix="/chat", tags=["chat"]) # Nutzt nun WP-25 DecisionEngine
app.include_router(chat_router, prefix="/chat", tags=["chat"]) # WP-25a Agentic Chat
app.include_router(ingest_router, prefix="/ingest", tags=["ingest"])
if admin_router:

View File

@ -1,15 +1,15 @@
"""
FILE: app/routers/chat.py
DESCRIPTION: Haupt-Chat-Interface (WP-25 Agentic Edition).
DESCRIPTION: Haupt-Chat-Interface (WP-25a Agentic Edition).
Kombiniert die spezialisierte Interview-Logik und Keyword-Erkennung
mit der neuen Multi-Stream Orchestrierung der DecisionEngine.
VERSION: 3.0.2
mit der neuen MoE-Orchestrierung und Pre-Synthesis Kompression.
VERSION: 3.0.4 (WP-25a: Optimized MoE & Cascade Delegation)
STATUS: Active
FIX:
- 100% Wiederherstellung der v2.7.8 Logik (Interview, Schema-Resolution, Keywords).
- Integration der DecisionEngine für paralleles RAG-Retrieval.
- Erhalt der Ollama Context-Throttling Parameter (WP-20).
- Beibehaltung der No-Retry Logik (max_retries=0) für Chat-Stabilität.
- WP-25a: Delegation der Fallback-Kaskade an den LLMService (v3.5.2).
- WP-25a: Nutzung der zentralisierten Stream-Kompression der DecisionEngine (v1.2.1).
- WP-25a: Konsistente Nutzung von MoE-Profilen für Interview- und RAG-Modus.
- 100% Erhalt der v3.0.2 Logik (Interview, Schema-Resolution, FastPaths).
"""
from fastapi import APIRouter, HTTPException, Depends
@ -19,6 +19,7 @@ import uuid
import logging
import yaml
import os
import asyncio
from pathlib import Path
from app.config import get_settings
@ -29,13 +30,13 @@ from app.services.feedback_service import log_search
router = APIRouter()
logger = logging.getLogger(__name__)
# --- EBENE 1: CONFIG LOADER & CACHING (Restauriert aus v2.7.8) ---
# --- EBENE 1: CONFIG LOADER & CACHING (WP-25 Standard) ---
_DECISION_CONFIG_CACHE = None
_TYPES_CONFIG_CACHE = None
def _load_decision_config() -> Dict[str, Any]:
"""Lädt die Strategie-Konfiguration (Kompatibilität zu WP-25)."""
"""Lädt die Strategie-Konfiguration."""
settings = get_settings()
path = Path(settings.DECISION_CONFIG_PATH)
try:
@ -47,7 +48,7 @@ def _load_decision_config() -> Dict[str, Any]:
return {"strategies": {}}
def _load_types_config() -> Dict[str, Any]:
"""Lädt die types.yaml für die Typerkennung im Interview-Modus."""
"""Lädt die types.yaml für die Typerkennung."""
path = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
try:
if os.path.exists(path):
@ -77,10 +78,7 @@ def get_decision_strategy(intent: str) -> Dict[str, Any]:
# --- EBENE 2: SPEZIAL-LOGIK (INTERVIEW & DETECTION) ---
def _detect_target_type(message: str, configured_schemas: Dict[str, Any]) -> str:
"""
WP-07: Identifiziert den gewünschten Notiz-Typ (Keyword-basiert).
100% identisch mit v2.7.8 zur Sicherstellung des Interview-Workflows.
"""
"""WP-07: Identifiziert den gewünschten Notiz-Typ (Keyword-basiert)."""
message_lower = message.lower()
types_cfg = get_types_config()
types_def = types_cfg.get("types", {})
@ -110,17 +108,14 @@ def _detect_target_type(message: str, configured_schemas: Dict[str, Any]) -> str
return "default"
def _is_question(query: str) -> bool:
"""Prüft, ob der Input eine Frage ist (W-Fragen Erkennung)."""
"""Prüft, ob der Input eine Frage ist."""
q = query.strip().lower()
if "?" in q: return True
starters = ["wer", "wie", "was", "wo", "wann", "warum", "weshalb", "wozu", "welche", "bist du"]
return any(q.startswith(s + " ") for s in starters)
async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]:
"""
WP-25 Hybrid Router:
Nutzt erst Keyword-Fast-Paths (Router) und delegiert dann an die DecisionEngine.
"""
"""Hybrid Router: Keyword-Fast-Paths & DecisionEngine LLM Router."""
config = get_full_config()
strategies = config.get("strategies", {})
query_lower = query.lower()
@ -140,17 +135,18 @@ async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]:
if kw.lower() in query_lower:
return "INTERVIEW", "Keyword (Interview)"
# 3. SLOW PATH: DecisionEngine LLM Router
# 3. SLOW PATH: DecisionEngine LLM Router (MoE-gesteuert)
intent = await llm.decision_engine._determine_strategy(query)
return intent, "DecisionEngine (LLM)"
# --- EBENE 3: RETRIEVAL AGGREGATION ---
def _collect_all_hits(stream_responses: Dict[str, Any]) -> List[QueryHit]:
"""Sammelt und dedupliziert Treffer aus allen parallelen Streams."""
"""Sammelt deduplizierte Treffer aus allen Streams für das Tracing."""
all_hits = []
seen_node_ids = set()
for _, response in stream_responses.items():
# In v3.0.4 sammeln wir die hits aus den QueryResponse Objekten
if hasattr(response, 'results'):
for hit in response.results:
if hit.node_id not in seen_node_ids:
@ -171,7 +167,7 @@ async def chat_endpoint(
start_time = time.time()
query_id = str(uuid.uuid4())
settings = get_settings()
logger.info(f"🚀 [WP-25] Chat request [{query_id}]: {request.message[:50]}...")
logger.info(f"🚀 [WP-25a] Chat request [{query_id}]: {request.message[:50]}...")
try:
# 1. Intent Detection
@ -184,13 +180,14 @@ async def chat_endpoint(
sources_hits = []
answer_text = ""
# 2. INTERVIEW MODE (Kompatibilität zu v2.7.8)
# 2. INTERVIEW MODE (Bitgenaue WP-07 Logik)
if intent == "INTERVIEW":
target_type = _detect_target_type(request.message, strategy.get("schemas", {}))
types_cfg = get_types_config()
type_def = types_cfg.get("types", {}).get(target_type, {})
fields_list = type_def.get("schema", [])
# WP-07: Restaurierte Fallback Logik
if not fields_list:
configured_schemas = strategy.get("schemas", {})
fallback = configured_schemas.get(target_type, configured_schemas.get("default", {}))
@ -203,56 +200,50 @@ async def chat_endpoint(
.replace("{target_type}", target_type) \
.replace("{schema_fields}", fields_str)
# WP-25a: MoE Call (Kaskade erfolgt intern im LLMService)
answer_text = await llm.generate_raw_response(
final_prompt, system=llm.get_prompt("system_prompt"),
priority="realtime", provider=strategy.get("preferred_provider"), max_retries=0
priority="realtime", profile_name="compression_fast", max_retries=0
)
sources_hits = []
# 3. RAG MODE (WP-25 Multi-Stream)
# 3. RAG MODE (Optimierte MoE Orchestrierung)
else:
# Phase A & B: Retrieval & Kompression (Delegation an Engine v1.2.1)
# Diese Methode gibt bereits die (evtl. komprimierten) Kontext-Strings zurück.
formatted_context_map = await engine._execute_parallel_streams(strategy, request.message)
# Erfassung der Quellen für das Tracing
raw_stream_map = {}
stream_keys = strategy.get("use_streams", [])
library = engine.config.get("streams_library", {})
tasks = []
retrieval_tasks = []
active_streams = []
for key in stream_keys:
stream_cfg = library.get(key)
if stream_cfg:
if key in library:
active_streams.append(key)
tasks.append(engine._run_single_stream(key, stream_cfg, request.message))
import asyncio
responses = await asyncio.gather(*tasks, return_exceptions=True)
raw_stream_map = {}
formatted_context_map = {}
max_chars = getattr(settings, "MAX_OLLAMA_CHARS", 10000)
provider = strategy.get("preferred_provider") or settings.MINDNET_LLM_PROVIDER
retrieval_tasks.append(engine._run_single_stream(key, library[key], request.message))
responses = await asyncio.gather(*retrieval_tasks, return_exceptions=True)
for name, res in zip(active_streams, responses):
if not isinstance(res, Exception):
raw_stream_map[name] = res
context_text = engine._format_stream_context(res)
# WP-20 Stability Fix: Throttling
if provider == "ollama" and len(context_text) > max_chars:
context_text = context_text[:max_chars] + "\n[...]"
formatted_context_map[name] = context_text
sources_hits = _collect_all_hits(raw_stream_map)
# Phase C: Finale MoE Synthese
answer_text = await engine._generate_final_answer(
intent, strategy, request.message, formatted_context_map
)
sources_hits = _collect_all_hits(raw_stream_map)
duration_ms = int((time.time() - start_time) * 1000)
# Logging
# Logging (WP-15)
try:
log_search(
query_id=query_id, query_text=request.message, results=sources_hits,
mode=f"wp25_{intent.lower()}", metadata={"strategy": intent, "source": intent_source}
mode=f"wp25a_{intent.lower()}", metadata={"strategy": intent, "source": intent_source}
)
except: pass
@ -263,4 +254,4 @@ async def chat_endpoint(
except Exception as e:
logger.error(f"❌ Chat Endpoint Failure: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Fehler bei der Verarbeitung.")
raise HTTPException(status_code=500, detail="Fehler bei der Verarbeitung der Anfrage.")

View File

@ -1,40 +1,74 @@
"""
FILE: app/services/embeddings_client.py
DESCRIPTION: Unified Embedding Client. Nutzt Ollama API (HTTP). Ersetzt lokale sentence-transformers.
VERSION: 2.5.0
DESCRIPTION: Unified Embedding Client. Nutzt MoE-Profile zur Modellsteuerung.
WP-25a: Integration der llm_profiles.yaml für konsistente Vektoren.
VERSION: 2.6.0 (WP-25a: MoE & Profile Support)
STATUS: Active
DEPENDENCIES: httpx, requests, app.config
LAST_ANALYSIS: 2025-12-15
DEPENDENCIES: httpx, requests, app.config, yaml
"""
from __future__ import annotations
import os
import logging
import httpx
import requests # Für den synchronen Fallback
from typing import List
import requests
import yaml
from pathlib import Path
from typing import List, Dict, Any
from app.config import get_settings
logger = logging.getLogger(__name__)
class EmbeddingsClient:
"""
Async Client für Embeddings via Ollama.
Async Client für Embeddings.
Steuerung erfolgt über das 'embedding_expert' Profil in llm_profiles.yaml.
"""
def __init__(self):
self.settings = get_settings()
# 1. MoE-Profil laden (WP-25a)
self.profile = self._load_embedding_profile()
# 2. Modell & URL auflösen
# Priorität: llm_profiles.yaml -> .env (Legacy) -> Fallback
self.model = self.profile.get("model") or os.getenv("MINDNET_EMBEDDING_MODEL")
provider = self.profile.get("provider", "ollama")
if provider == "ollama":
self.base_url = self.settings.OLLAMA_URL
else:
# Platzhalter für zukünftige Cloud-Embedding-Provider
self.base_url = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434")
self.model = os.getenv("MINDNET_EMBEDDING_MODEL")
if not self.model:
self.model = os.getenv("MINDNET_LLM_MODEL", "phi3:mini")
logger.warning(f"No MINDNET_EMBEDDING_MODEL set. Fallback to '{self.model}'.")
logger.warning(f"⚠️ Kein Embedding-Modell in Profil oder .env gefunden. Fallback auf '{self.model}'.")
else:
logger.info(f"🧬 Embedding-Experte aktiv: Model='{self.model}' via {provider}")
def _load_embedding_profile(self) -> Dict[str, Any]:
"""Lädt die Konfiguration für den embedding_expert."""
path_str = getattr(self.settings, "LLM_PROFILES_PATH", "config/llm_profiles.yaml")
path = Path(path_str)
if not path.exists():
return {}
try:
with open(path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
profiles = data.get("profiles", {})
return profiles.get("embedding_expert", {})
except Exception as e:
logger.error(f"❌ Failed to load embedding profile: {e}")
return {}
async def embed_query(self, text: str) -> List[float]:
"""Erzeugt einen Vektor für eine Suchanfrage."""
return await self._request_embedding(text)
async def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""Erzeugt Vektoren für einen Batch von Dokumenten."""
vectors = []
# Längeres Timeout für Batches
# Längeres Timeout für Batches (WP-20 Resilienz)
async with httpx.AsyncClient(timeout=120.0) as client:
for text in texts:
vec = await self._request_embedding_with_client(client, text)
@ -42,18 +76,23 @@ class EmbeddingsClient:
return vectors
async def _request_embedding(self, text: str) -> List[float]:
"""Interner Request-Handler für Einzelabfragen."""
async with httpx.AsyncClient(timeout=30.0) as client:
return await self._request_embedding_with_client(client, text)
async def _request_embedding_with_client(self, client: httpx.AsyncClient, text: str) -> List[float]:
if not text or not text.strip(): return []
"""Führt den HTTP-Call gegen die Embedding-API durch."""
if not text or not text.strip():
return []
url = f"{self.base_url}/api/embeddings"
try:
# WP-25: Aktuell optimiert für Ollama-API Struktur
response = await client.post(url, json={"model": self.model, "prompt": text})
response.raise_for_status()
return response.json().get("embedding", [])
except Exception as e:
logger.error(f"Async embedding failed: {e}")
logger.error(f"Async embedding failed (Model: {self.model}): {e}")
return []
# ==============================================================================
@ -62,27 +101,38 @@ class EmbeddingsClient:
def embed_text(text: str) -> List[float]:
"""
LEGACY/SYNC: Nutzt jetzt ebenfalls OLLAMA via 'requests'.
Ersetzt SentenceTransformers, um Dimensionskonflikte (768 vs 384) zu lösen.
LEGACY/SYNC: Nutzt ebenfalls die Profil-Logik für Konsistenz.
Ersetzt lokale sentence-transformers zur Vermeidung von Dimensionskonflikten.
"""
if not text or not text.strip():
return []
base_url = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434")
model = os.getenv("MINDNET_EMBEDDING_MODEL")
settings = get_settings()
# Schneller Profil-Lookup für Sync-Mode
path = Path(getattr(settings, "LLM_PROFILES_PATH", "config/llm_profiles.yaml"))
model = os.getenv("MINDNET_EMBEDDING_MODEL")
base_url = settings.OLLAMA_URL
if path.exists():
try:
with open(path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
prof = data.get("profiles", {}).get("embedding_expert", {})
if prof.get("model"):
model = prof["model"]
except: pass
# Fallback logik identisch zur Klasse
if not model:
model = os.getenv("MINDNET_LLM_MODEL", "phi3:mini")
url = f"{base_url}/api/embeddings"
try:
# Synchroner Request (blockierend)
# Synchroner Request via requests
response = requests.post(url, json={"model": model, "prompt": text}, timeout=30)
response.raise_for_status()
data = response.json()
return data.get("embedding", [])
return response.json().get("embedding", [])
except Exception as e:
logger.error(f"Sync embedding (Ollama) failed: {e}")
logger.error(f"Sync embedding failed (Model: {model}): {e}")
return []

View File

@ -1,16 +1,14 @@
"""
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.
WP-22/JSON: Optionales JSON-Schema + strict (für OpenRouter).
WP-25: Integration der DecisionEngine für Agentic Multi-Stream RAG.
VERSION: 3.4.2 (WP-25: Ingest-Stability Patch)
WP-25a: Implementierung der Mixture of Experts (MoE) Kaskaden-Steuerung.
VERSION: 3.5.2 (WP-25a: MoE & Fallback Cascade Support)
STATUS: Active
FIX:
- Ingest-Stability: Entfernung des <5-Zeichen Guards (ermöglicht YES/NO Validierungen).
- OpenRouter-Fix: Sicherung gegen leere 'choices' zur Vermeidung von JSON-Errors.
- Erhalt der vollständigen v3.3.9 Logik für Rate-Limits, Retries und Background-Tasks.
- WP-25a: Implementierung der rekursiven Fallback-Kaskade via fallback_profile.
- WP-25a: Schutz gegen zirkuläre Profil-Referenzen (visited_profiles).
- WP-25a: Erweitertes Logging für Tracing der Experten-Entscheidungen.
- Erhalt der Ingest-Stability (WP-25) und des Rate-Limit-Managements.
"""
import httpx
import yaml
@ -19,28 +17,28 @@ import asyncio
import json
from google import genai
from google.genai import types
from openai import AsyncOpenAI # Für OpenRouter (OpenAI-kompatibel)
from openai import AsyncOpenAI
from pathlib import Path
from typing import Optional, Dict, Any, Literal
from app.config import get_settings
# ENTSCHEIDENDER FIX: Import der neutralen Bereinigungs-Logik (WP-14)
# Import der neutralen Bereinigungs-Logik
from app.core.registry import clean_llm_text
logger = logging.getLogger(__name__)
class LLMService:
# GLOBALER SEMAPHOR für Hintergrund-Last Steuerung (WP-06)
_background_semaphore = None
def __init__(self):
self.settings = get_settings()
self.prompts = self._load_prompts()
# WP-25: Lazy Initialization der DecisionEngine zur Vermeidung von Circular Imports
# WP-25a: Zentrale Experten-Profile laden
self.profiles = self._load_llm_profiles()
self._decision_engine = None
# Initialisiere Semaphore einmalig auf Klassen-Ebene
if LLMService._background_semaphore is None:
limit = getattr(self.settings, "BACKGROUND_LIMIT", 2)
logger.info(f"🚦 LLMService: Initializing Background Semaphore with limit: {limit}")
@ -52,10 +50,9 @@ class LLMService:
timeout=httpx.Timeout(self.settings.LLM_TIMEOUT)
)
# 2. Google GenAI Client (Modern SDK)
# 2. Google GenAI Client
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'}
@ -68,24 +65,20 @@ class LLMService:
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.")
@property
def decision_engine(self):
"""Lazy Initialization der Decision Engine (WP-25)."""
if self._decision_engine is None:
from app.core.retrieval.decision_engine import DecisionEngine
self._decision_engine = DecisionEngine()
return self._decision_engine
def _load_prompts(self) -> dict:
"""Lädt die Prompt-Konfiguration aus der YAML-Datei."""
path = Path(self.settings.PROMPTS_PATH)
if not path.exists():
logger.error(f"❌ Prompts file not found at {path}")
return {}
try:
with open(path, "r", encoding="utf-8") as f:
@ -94,21 +87,27 @@ class LLMService:
logger.error(f"❌ Failed to load prompts: {e}")
return {}
def _load_llm_profiles(self) -> dict:
"""WP-25a: Lädt die zentralen MoE-Profile aus der llm_profiles.yaml."""
path_str = getattr(self.settings, "LLM_PROFILES_PATH", "config/llm_profiles.yaml")
path = Path(path_str)
if not path.exists():
logger.warning(f"⚠️ LLM Profiles file not found at {path}. System will use .env defaults.")
return {}
try:
with open(path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
return data.get("profiles", {})
except Exception as e:
logger.error(f"❌ Failed to load llm_profiles.yaml: {e}")
return {}
def get_prompt(self, key: str, provider: str = None) -> str:
"""
Hole provider-spezifisches Template mit intelligenter Text-Kaskade.
Kaskade: Gewählter Provider -> Gemini -> Ollama.
"""
active_provider = provider or self.settings.MINDNET_LLM_PROVIDER
data = self.prompts.get(key, "")
if isinstance(data, dict):
val = data.get(active_provider, data.get("gemini", data.get("ollama", "")))
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(
@ -123,36 +122,84 @@ class LLMService:
model_override: Optional[str] = None,
json_schema: Optional[Dict[str, Any]] = None,
json_schema_name: str = "mindnet_json",
strict_json_schema: bool = True
strict_json_schema: bool = True,
profile_name: Optional[str] = None,
visited_profiles: Optional[list] = None
) -> str:
"""
Haupteinstiegspunkt für LLM-Anfragen.
WP-25 FIX: Schwellenwert entfernt, um kurze Ingest-Validierungen (YES/NO) zu unterstützen.
Haupteinstiegspunkt für LLM-Anfragen mit rekursiver Kaskaden-Logik.
"""
target_provider = provider or self.settings.MINDNET_LLM_PROVIDER
visited_profiles = visited_profiles or []
target_provider = provider
target_model = model_override
target_temp = None
fallback_profile = None
# 1. Profil-Auflösung
if profile_name and self.profiles:
profile = self.profiles.get(profile_name)
if profile:
target_provider = profile.get("provider", target_provider)
target_model = profile.get("model", target_model)
target_temp = profile.get("temperature")
fallback_profile = profile.get("fallback_profile")
visited_profiles.append(profile_name)
logger.info(f"🎭 MoE Dispatch: Profil='{profile_name}' -> Provider='{target_provider}' | Model='{target_model}'")
else:
logger.warning(f"⚠️ Profil '{profile_name}' nicht in llm_profiles.yaml gefunden!")
# Fallback auf Standard-Provider falls nichts übergeben/definiert wurde
if not target_provider:
target_provider = self.settings.MINDNET_LLM_PROVIDER
logger.info(f" Kein Provider/Profil definiert. Nutze Default: {target_provider}")
# 2. Ausführung mit Fehler-Handling für Kaskade
try:
if priority == "background":
async with LLMService._background_semaphore:
res = await self._dispatch(
target_provider, prompt, system, force_json,
max_retries, base_delay, model_override,
json_schema, json_schema_name, strict_json_schema
max_retries, base_delay, target_model,
json_schema, json_schema_name, strict_json_schema, target_temp
)
else:
res = await self._dispatch(
target_provider, prompt, system, force_json,
max_retries, base_delay, model_override,
json_schema, json_schema_name, strict_json_schema
max_retries, base_delay, target_model,
json_schema, json_schema_name, strict_json_schema, target_temp
)
# WP-25 FIX: Nur noch auf absolut leere Antwort prüfen (ermöglicht YES/NO Antworten).
# Check auf leere Cloud-Antworten (WP-25 Stability)
if not res and target_provider != "ollama":
logger.warning(f"⚠️ [WP-25] Empty response from {target_provider}. Falling back to OLLAMA.")
res = await self._execute_ollama(prompt, system, force_json, max_retries, base_delay)
logger.warning(f"⚠️ Empty response from {target_provider}. Triggering fallback chain.")
raise ValueError(f"Empty response from {target_provider}")
# WP-14 Fix: Bereinige Text-Antworten vor Rückgabe
return clean_llm_text(res) if not force_json else res
except Exception as e:
logger.error(f"❌ Error during execution of profile '{profile_name}' ({target_provider}): {e}")
# 3. Kaskaden-Logik: Nächstes Profil in der Kette versuchen
if fallback_profile and fallback_profile not in visited_profiles:
logger.info(f"🔄 Switching to fallback profile: '{fallback_profile}'")
return await self.generate_raw_response(
prompt=prompt, system=system, force_json=force_json,
max_retries=max_retries, base_delay=base_delay,
priority=priority, provider=provider, model_override=model_override,
json_schema=json_schema, json_schema_name=json_schema_name,
strict_json_schema=strict_json_schema,
profile_name=fallback_profile,
visited_profiles=visited_profiles
)
# 4. Ultimativer Notanker: Falls alles fehlschlägt, direkt zu Ollama
if target_provider != "ollama" and self.settings.LLM_FALLBACK_ENABLED:
logger.warning(f"🚨 Kaskade erschöpft. Nutze finalen Ollama-Notanker.")
res = await self._execute_ollama(prompt, system, force_json, max_retries, base_delay)
return clean_llm_text(res) if not force_json else res
raise e
async def _dispatch(
self,
provider: str,
@ -164,9 +211,10 @@ class LLMService:
model_override: Optional[str],
json_schema: Optional[Dict[str, Any]],
json_schema_name: str,
strict_json_schema: bool
strict_json_schema: bool,
temperature: Optional[float] = None
) -> str:
"""Routet die Anfrage mit intelligenter Rate-Limit Erkennung."""
"""Routet die Anfrage an den spezifischen Provider-Executor."""
rate_limit_attempts = 0
max_rate_retries = min(max_retries, getattr(self.settings, "LLM_RATE_LIMIT_RETRIES", 3))
wait_time = getattr(self.settings, "LLM_RATE_LIMIT_WAIT", 60.0)
@ -175,43 +223,41 @@ class LLMService:
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
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,
temperature=temperature
)
if provider == "gemini" and self.google_client:
return await self._execute_google(prompt, system, force_json, model_override)
return await self._execute_google(prompt, system, force_json, model_override, temperature)
return await self._execute_ollama(prompt, system, force_json, max_retries, base_delay)
return await self._execute_ollama(prompt, system, force_json, max_retries, base_delay, temperature)
except Exception as e:
err_str = str(e)
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 Handling (429)
if any(x in err_str for x in ["429", "RESOURCE_EXHAUSTED", "rate_limited"]):
rate_limit_attempts += 1
logger.warning(f"⏳ Rate Limit from {provider}. Attempt {rate_limit_attempts}. Waiting {wait_time}s...")
logger.warning(f"⏳ Rate Limit {provider}. Attempt {rate_limit_attempts}. Wait {wait_time}s.")
await asyncio.sleep(wait_time)
continue
if self.settings.LLM_FALLBACK_ENABLED and provider != "ollama":
logger.warning(f"🔄 Provider {provider} failed ({err_str}). Falling back to OLLAMA.")
return await self._execute_ollama(prompt, system, force_json, max_retries, base_delay)
# Andere Fehler werden an generate_raw_response für die Kaskade gereicht
raise e
async def _execute_google(self, prompt, system, force_json, model_override):
async def _execute_google(self, prompt, system, force_json, model_override, temperature):
model = model_override or self.settings.GEMINI_MODEL
clean_model = model.replace("models/", "")
config = types.GenerateContentConfig(
system_instruction=system,
response_mime_type="application/json" if force_json else "text/plain"
)
config_kwargs = {
"system_instruction": system,
"response_mime_type": "application/json" if force_json else "text/plain"
}
if temperature is not None:
config_kwargs["temperature"] = temperature
config = types.GenerateContentConfig(**config_kwargs)
response = await asyncio.wait_for(
asyncio.to_thread(
self.google_client.models.generate_content,
@ -222,53 +268,48 @@ class LLMService:
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
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,
temperature: Optional[float] = None
) -> str:
"""OpenRouter API Integration. WP-25 FIX: Sicherung gegen leere 'choices'."""
model = model_override or self.settings.OPENROUTER_MODEL
logger.info(f"🛰️ OpenRouter Call: Model='{model}' | Temp={temperature}")
messages = []
if system:
messages.append({"role": "system", "content": system})
if system: messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt})
kwargs: Dict[str, Any] = {}
if temperature is not None:
kwargs["temperature"] = temperature
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
}
"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
model=model, messages=messages, **kwargs
)
# WP-25 FIX: Sicherung gegen leere Antwort-Arrays
if not response.choices or len(response.choices) == 0:
logger.warning(f"🛰️ OpenRouter returned no choices for model {model}")
if not response.choices:
return ""
return response.choices[0].message.content.strip() if response.choices[0].message.content else ""
async def _execute_ollama(self, prompt, system, force_json, max_retries, base_delay):
async def _execute_ollama(self, prompt, system, force_json, max_retries, base_delay, temperature=None):
# Nutzt Profil-Temperatur oder strikte Defaults für lokale Hardware-Schonung
effective_temp = temperature if temperature is not None else (0.1 if force_json else 0.7)
payload = {
"model": self.settings.LLM_MODEL,
"prompt": prompt,
"stream": False,
"options": {"temperature": 0.1 if force_json else 0.7, "num_ctx": 8192}
"options": {"temperature": effective_temp, "num_ctx": 8192}
}
if force_json: payload["format"] = "json"
if system: payload["system"] = system
@ -282,14 +323,12 @@ class LLMService:
except Exception as e:
attempt += 1
if attempt > max_retries:
logger.error(f"❌ Ollama request failed: {e}")
logger.error(f"❌ Ollama final failure after {attempt} attempts: {e}")
raise e
wait_time = base_delay * (2 ** (attempt - 1))
await asyncio.sleep(wait_time)
await asyncio.sleep(base_delay * (2 ** (attempt - 1)))
async def generate_rag_response(self, query: str, context_str: Optional[str] = None) -> str:
"""WP-25: Orchestrierung via DecisionEngine."""
logger.info(f"🚀 [WP-25] Chat Query: {query[:50]}...")
return await self.decision_engine.ask(query)
async def close(self):

View File

@ -1,28 +1,33 @@
# config/decision_engine.yaml
# VERSION: 3.1.6 (WP-25: Multi-Stream Agentic RAG - Final Release)
# VERSION: 3.2.2 (WP-25a: Decoupled MoE Logic)
# STATUS: Active
# DoD:
# - Strikte Nutzung der Typen aus types.yaml (v2.7.0).
# - Fix für Projekt-Klassifizierung via Keyword-Fast-Path (Auflösung Kollision).
# - 100% Erhalt aller Stream-Parameter und Edge-Boosts.
# DESCRIPTION: Zentrale Orchestrierung der Multi-Stream-Engine.
# FIX:
# - Auslagerung der LLM-Profile in llm_profiles.yaml zur zentralen Wartbarkeit.
# - Integration von compression_thresholds zur Inhaltsverdichtung (WP-25a).
# - 100% Erhalt aller WP-25 Edge-Boosts und Filter-Typen (v3.1.6).
version: 3.1
version: 3.2
settings:
llm_fallback_enabled: true
# "auto" nutzt den in MINDNET_LLM_PROVIDER gesetzten Standard.
# "auto" nutzt den globalen Default-Provider aus der .env
router_provider: "auto"
# Verweist auf das Template in prompts.yaml
# Verweis auf den Intent-Klassifizierer in der prompts.yaml
router_prompt_key: "intent_router_v1"
# Pfad zur neuen Experten-Konfiguration (WP-25a Architektur-Cleanliness)
profiles_config_path: "config/llm_profiles.yaml"
router_profile: "compression_fast"
# --- EBENE 1: STREAM-LIBRARY (Bausteine basierend auf types.yaml) ---
# Synchronisiert mit types.yaml v2.7.0
# --- EBENE 1: STREAM-LIBRARY (Bausteine basierend auf types.yaml v2.7.0) ---
streams_library:
values_stream:
name: "Identität & Ethik"
# Referenz auf Experten-Profil (z.B. lokal via Ollama für Privacy)
llm_profile: "identity_safe"
compression_profile: "identity_safe"
compression_threshold: 2500
query_template: "Welche meiner Werte und Prinzipien betreffen: {query}"
# Nur Typen aus types.yaml
filter_types: ["value", "principle", "belief", "trait", "boundary", "need", "motivation"]
top_k: 5
edge_boosts:
@ -32,8 +37,10 @@ streams_library:
facts_stream:
name: "Operative Realität"
llm_profile: "synthesis_pro"
compression_profile: "compression_fast"
compression_threshold: 3500
query_template: "Status, Ressourcen und Fakten zu: {query}"
# Nur Typen aus types.yaml
filter_types: ["project", "decision", "task", "goal", "event", "state"]
top_k: 5
edge_boosts:
@ -43,8 +50,10 @@ streams_library:
biography_stream:
name: "Persönliche Erfahrung"
llm_profile: "synthesis_pro"
compression_profile: "compression_fast"
compression_threshold: 3000
query_template: "Welche Erlebnisse habe ich im Kontext von {query} gemacht?"
# Nur Typen aus types.yaml
filter_types: ["experience", "journal", "profile", "person"]
top_k: 3
edge_boosts:
@ -53,8 +62,10 @@ streams_library:
risk_stream:
name: "Risiko-Radar"
llm_profile: "synthesis_pro"
compression_profile: "compression_fast"
compression_threshold: 2500
query_template: "Gefahren, Hindernisse oder Risiken bei: {query}"
# Nur Typen aus types.yaml
filter_types: ["risk", "obstacle", "bias"]
top_k: 3
edge_boosts:
@ -64,81 +75,59 @@ streams_library:
tech_stream:
name: "Wissen & Technik"
llm_profile: "tech_expert"
compression_profile: "compression_fast"
compression_threshold: 4500
query_template: "Inhaltliche Details und Definitionen zu: {query}"
# Nur Typen aus types.yaml
filter_types: ["concept", "source", "glossary", "idea", "insight", "skill", "habit"]
top_k: 5
edge_boosts:
uses: 2.5
implemented_in: 3.0
# --- EBENE 2: STRATEGIEN (Komposition & Routing) ---
# Orchestriert das Zusammenspiel der Streams basierend auf dem Intent.
# --- EBENE 2: STRATEGIEN (Finale Komposition via MoE-Profile) ---
strategies:
# Spezialisierte Fact-Strategie für zeitliche Fragen
FACT_WHEN:
description: "Abfrage von exakten Zeitpunkten und Terminen."
preferred_provider: "openrouter"
# FAST PATH: Harte Keywords für zeitliche Fragen
llm_profile: "synthesis_pro"
trigger_keywords: ["wann", "datum", "uhrzeit", "zeitpunkt"]
use_streams:
- "facts_stream"
- "biography_stream"
- "tech_stream"
use_streams: ["facts_stream", "biography_stream", "tech_stream"]
prompt_template: "fact_synthesis_v1"
# Spezialisierte Fact-Strategie für inhaltliche Fragen & Listen
FACT_WHAT:
description: "Abfrage von Definitionen, Listen und Inhalten."
preferred_provider: "openrouter"
# FIX v3.1.6: "projekt" entfernt, um Kollision mit DECISION ("Soll ich Projekt...") zu vermeiden.
llm_profile: "synthesis_pro"
trigger_keywords: ["was ist", "welche sind", "liste", "übersicht", "zusammenfassung"]
use_streams:
- "facts_stream"
- "tech_stream"
- "biography_stream"
use_streams: ["facts_stream", "tech_stream", "biography_stream"]
prompt_template: "fact_synthesis_v1"
# Entscheidungs-Frage
DECISION:
description: "Der User sucht Rat, Strategie oder Abwägung."
preferred_provider: "gemini"
# FIX v3.1.6: Trigger erweitert, um "Soll ich... Projekt..." sicher zu fangen.
llm_profile: "synthesis_pro"
trigger_keywords: ["soll ich", "sollte ich", "entscheidung", "abwägen", "priorität", "empfehlung"]
use_streams:
- "values_stream"
- "facts_stream"
- "risk_stream"
use_streams: ["values_stream", "facts_stream", "risk_stream"]
prompt_template: "decision_synthesis_v1"
prepend_instruction: |
!!! ENTSCHEIDUNGS-MODUS (AGENTIC MULTI-STREAM) !!!
Analysiere die Fakten vor dem Hintergrund meiner Werte und evaluiere die Risiken.
Wäge ab, ob das Vorhaben mit meiner langfristigen Identität kompatibel ist.
# Emotionale Reflexion
EMPATHY:
description: "Reaktion auf emotionale Zustände."
preferred_provider: "openrouter"
llm_profile: "synthesis_pro"
trigger_keywords: ["fühle", "traurig", "glücklich", "stress", "angst"]
use_streams:
- "biography_stream"
- "values_stream"
use_streams: ["biography_stream", "values_stream"]
prompt_template: "empathy_template"
# Technischer Support
CODING:
description: "Technische Anfragen und Programmierung."
preferred_provider: "gemini"
llm_profile: "tech_expert"
trigger_keywords: ["code", "python", "script", "bug", "syntax"]
use_streams:
- "tech_stream"
- "facts_stream"
use_streams: ["tech_stream", "facts_stream"]
prompt_template: "technical_template"
# Eingabe-Modus (WP-07)
INTERVIEW:
description: "Der User möchte Wissen erfassen (Eingabemodus)."
preferred_provider: "openrouter"
llm_profile: "compression_fast"
use_streams: []
prompt_template: "interview_template"

64
config/llm_profiles.yaml Normal file
View File

@ -0,0 +1,64 @@
# config/llm_profiles.yaml
# VERSION: 1.3.0 (WP-25a: Global MoE & Fallback Cascade)
# STATUS: Active
# DESCRIPTION: Zentrale Definition der LLM-Rollen inkl. Ausfall-Logik (Kaskade).
profiles:
# --- CHAT & SYNTHESE ---
# Der "Architekt": Hochwertige Synthese. Fällt bei Fehlern auf den Backup-Cloud-Experten zurück.
synthesis_pro:
provider: "openrouter"
model: "google/gemini-2.0-flash-exp:free"
temperature: 0.7
fallback_profile: "synthesis_backup"
# Der "Vize": Leistungsstarkes Modell bei einem anderen Provider (Resilienz).
synthesis_backup:
provider: "openrouter"
model: "meta-llama/llama-3.3-70b-instruct:free"
temperature: 0.5
fallback_profile: "identity_safe" # Letzte Instanz: Lokal
# Der "Ingenieur": Fachspezialist für Code. Nutzt bei Ausfall den Generalisten.
tech_expert:
provider: "openrouter"
model: "qwen/qwen-2.5-vl-7b-instruct:free"
temperature: 0.3
fallback_profile: "synthesis_pro"
# Der "Dampfhammer": Schnell für Routing und Zusammenfassungen.
compression_fast:
provider: "openrouter"
model: "mistralai/mistral-7b-instruct:free"
temperature: 0.1
fallback_profile: "identity_safe"
# --- INGESTION EXPERTEN ---
# Spezialist für die Extraktion komplexer Datenstrukturen aus Dokumenten.
ingest_extractor:
provider: "openrouter"
model: "mistralai/mistral-7b-instruct:free"
temperature: 0.2
fallback_profile: "synthesis_backup"
# Spezialist für binäre Prüfungen (YES/NO). Muss extrem deterministisch sein.
ingest_validator:
provider: "openrouter"
model: "mistralai/mistral-7b-instruct:free"
temperature: 0.0
fallback_profile: "compression_fast"
# --- LOKALER ANKER & PRIVACY ---
# Der "Wächter": Lokales Modell für maximale Privatsphäre. Ende der Kaskade.
identity_safe:
provider: "ollama"
model: "phi3:mini"
temperature: 0.2
# Kein fallback_profile definiert = Terminaler Endpunkt
# --- EMBEDDING EXPERTE ---
# Zentralisierung des Embedding-Modells zur Entfernung aus der .env.
embedding_expert:
provider: "ollama"
model: "nomic-embed-text"
dimensions: 768

View File

@ -2,8 +2,8 @@
doc_type: glossary
audience: all
status: active
version: 2.9.3
context: "Zentrales Glossar für Mindnet v2.9.3. Enthält Definitionen zu Hybrid-Cloud Resilienz, WP-14 Modularisierung, WP-15b Two-Pass Ingestion, WP-15c Multigraph-Support, WP-25 Agentic Multi-Stream RAG und Mistral-safe Parsing."
version: 3.0.0
context: "Zentrales Glossar für Mindnet v3.0.0. Enthält Definitionen zu Hybrid-Cloud Resilienz, WP-14 Modularisierung, WP-15b Two-Pass Ingestion, WP-15c Multigraph-Support, WP-25 Agentic Multi-Stream RAG, WP-25a Mixture of Experts (MoE) und Mistral-safe Parsing."
---
# Mindnet Glossar
@ -55,3 +55,8 @@ context: "Zentrales Glossar für Mindnet v2.9.3. Enthält Definitionen zu Hybrid
* **Deep-Link / Section-basierter Link:** Ein Link wie `[[Note#Section]]`, der auf einen spezifischen Abschnitt innerhalb einer Note verweist. Seit v2.9.1 wird dieser in `target_id="Note"` und `target_section="Section"` aufgeteilt, um "Phantom-Knoten" zu vermeiden und Multigraph-Support zu ermöglichen.
* **Atomic Section Logic (v3.9.9):** Chunking-Verfahren, das Sektions-Überschriften und deren Inhalte atomar in Chunks hält (Pack-and-Carry-Over). Verhindert, dass Überschriften über Chunk-Grenzen hinweg getrennt werden.
* **Registry-First Profiling (v2.13.12):** Hierarchische Auflösung des Chunking-Profils: Frontmatter > types.yaml Typ-Config > Global Defaults. Stellt sicher, dass Note-Typen automatisch das korrekte Profil erhalten.
* **Mixture of Experts (MoE) - WP-25a:** Profilbasierte Experten-Architektur, bei der jede Systemaufgabe (Synthese, Ingestion-Validierung, Routing, Kompression) einem dedizierten Profil zugewiesen wird, das Modell, Provider und Parameter unabhängig von der globalen Konfiguration definiert.
* **LLM-Profil:** Zentrale Definition in `llm_profiles.yaml`, die Provider, Modell, Temperature und Fallback-Profil für eine spezifische Aufgabe festlegt (z.B. `synthesis_pro`, `tech_expert`, `ingest_validator`).
* **Fallback-Kaskade (WP-25a):** Rekursive Fallback-Logik, bei der bei Fehlern automatisch auf das `fallback_profile` umgeschaltet wird, bis der terminale Endpunkt (`identity_safe`) erreicht wird. Schutz gegen Zirkel-Referenzen via `visited_profiles`-Tracking.
* **Pre-Synthesis Kompression (WP-25a):** Asynchrone Verdichtung überlanger Wissens-Streams vor der Synthese, um Token-Verbrauch zu reduzieren und die Synthese zu beschleunigen. Nutzt `compression_profile` (z.B. `compression_fast`).
* **Profilgesteuerte Validierung (WP-25a):** Semantische Kanten-Validierung in der Ingestion erfolgt zwingend über das MoE-Profil `ingest_validator` (Temperature 0.0 für Determinismus), unabhängig von der globalen Provider-Konfiguration.

View File

@ -1,10 +1,10 @@
---
doc_type: concept
audience: architect, product_owner
scope: ai, router, personas, resilience, agentic_rag
scope: ai, router, personas, resilience, agentic_rag, moe
status: active
version: 2.9.3
context: "Fachkonzept der hybriden KI-Persönlichkeit, Agentic Multi-Stream RAG, Provider-Kaskade und kognitiven Resilienz (Deep Fallback)."
version: 3.0.0
context: "Fachkonzept der hybriden KI-Persönlichkeit, Agentic Multi-Stream RAG, WP-25a Mixture of Experts (MoE), Provider-Kaskade und kognitiven Resilienz (Deep Fallback)."
---
# Konzept: KI-Persönlichkeit & Router
@ -59,13 +59,45 @@ Jeder Treffer wird mit `stream_origin` markiert, um Feedback-Optimierung pro Wis
---
## 2. Die hybride LLM-Landschaft (Resilienz-Kaskade)
## 2. Mixture of Experts (MoE) Architektur (WP-25a)
Ein intelligenter Zwilling muss jederzeit verfügbar sein. Mindnet v2.8.1 nutzt eine **dreistufige Kaskade**, um Intelligenz, Kosten und Verfügbarkeit zu optimieren:
Seit WP-25a nutzt MindNet eine **profilbasierte Experten-Steuerung** statt einer globalen Provider-Konfiguration. Jede Systemaufgabe wird einem dedizierten Profil zugewiesen, das Modell, Provider und Parameter unabhängig definiert.
1. **Stufe 1: Cloud-Speed (Turbo-Mode):** Primäre Wahl für komplexe Extraktionsaufgaben und schnelle RAG-Antworten mittels OpenRouter (Mistral-7B) oder Google Gemini (2.5-flash-lite).
### 2.1 Experten-Profile
**Zentrale Registry (`llm_profiles.yaml`):**
* **`synthesis_pro`:** Hochwertige Synthese für Chat-Antworten (Cloud)
* **`tech_expert`:** Fachspezialist für Code & Technik (Claude 3.5 Sonnet)
* **`compression_fast`:** Schnelle Kompression & Routing (Mistral 7B)
* **`ingest_validator`:** Deterministische Validierung (Temperature 0.0)
* **`identity_safe`:** Lokaler Anker (Ollama/Phi-3) für maximale Privacy
**Vorteile:**
* **Aufgabenspezifische Optimierung:** Jede Aufgabe nutzt das optimale Modell
* **Hardware-Optimierung:** Lokaler Anker für kleine Hardware-Umgebungen
* **Wartbarkeit:** Zentrale Konfiguration statt verstreuter ENV-Variablen
### 2.2 Rekursive Fallback-Kaskade
Die Profile implementieren eine **automatische Fallback-Logik**:
1. **Primäres Profil:** System versucht das angeforderte Profil (z.B. `synthesis_pro`)
2. **Fallback-Level 1:** Bei Fehler → `fallback_profile` (z.B. `synthesis_backup`)
3. **Fallback-Level 2:** Bei weiterem Fehler → nächster Fallback (z.B. `identity_safe`)
4. **Terminaler Endpunkt:** `identity_safe` hat keinen Fallback (lokales Modell als letzte Instanz)
**Schutzmechanismen:**
* **Zirkuläre Referenzen:** `visited_profiles`-Tracking verhindert Endlosschleifen
* **Background-Semaphore:** Parallele Tasks werden gedrosselt
### 2.3 Die hybride LLM-Landschaft (Legacy & MoE)
Ein intelligenter Zwilling muss jederzeit verfügbar sein. Seit WP-25a wird die Resilienz durch die **MoE Fallback-Kaskade** gewährleistet:
1. **Stufe 1: Cloud-Experten:** Spezialisierte Profile für verschiedene Aufgaben (z.B. `synthesis_pro`, `tech_expert`)
2. **Stufe 2: Quoten-Resilienz:** Erkennt das System eine Drosselung durch Cloud-Provider (HTTP 429), pausiert es kontrolliert (`LLM_RATE_LIMIT_WAIT`), führt automatisierte Retries durch und schützt so den laufenden Prozess.
3. **Stufe 3: Deep Fallback & lokale Souveränität (Ollama):** * **Technischer Fallback:** Schlagen alle Cloud-Versuche fehl, übernimmt das lokale Modell (Phi-3).
3. **Stufe 3: Lokale Souveränität (Ollama):**
* **Technischer Fallback:** Schlagen alle Cloud-Versuche fehl, übernimmt das lokale Modell (Phi-3) via `identity_safe` Profil.
* **Kognitiver Fallback (v2.11.14):** Liefert die Cloud zwar technisch eine Antwort, verweigert aber inhaltlich die Verarbeitung (Silent Refusal/Policy Violation), wird ein **Deep Fallback** erzwungen, um die Datenintegrität lokal zu retten.

View File

@ -1,10 +1,10 @@
---
doc_type: technical_reference
audience: developer, architect
scope: backend, chat, llm_service, traffic_control, resilience, agentic_rag
scope: backend, chat, llm_service, traffic_control, resilience, agentic_rag, moe
status: active
version: 2.9.3
context: "Technische Implementierung des FastAPI-Routers, des hybriden LLMService (v3.4.2), WP-25 Agentic Multi-Stream RAG und WP-20 Resilienz-Logik."
version: 3.0.0
context: "Technische Implementierung des FastAPI-Routers, des hybriden LLMService (v3.5.2), WP-25 Agentic Multi-Stream RAG, WP-25a Mixture of Experts (MoE) und WP-20 Resilienz-Logik."
---
# Chat Backend & Agentic Multi-Stream RAG
@ -28,7 +28,31 @@ Der Router nutzt einen **Hybrid-Modus** mit Keyword-Fast-Path und LLM-Slow-Path:
* Wenn unklar: Anfrage an `DecisionEngine._determine_strategy()` zur LLM-basierten Klassifizierung.
* Nutzt `intent_router_v1` Prompt aus `prompts.yaml`.
### 1.2 Prompt-Auflösung (Bulletproof Resolution)
### 1.2 Mixture of Experts (MoE) Architektur (WP-25a)
Seit WP-25a nutzt MindNet eine **profilbasierte Experten-Steuerung** statt einer globalen Provider-Konfiguration. Jede Systemaufgabe wird einem dedizierten Profil zugewiesen:
**Profil-Registry (`llm_profiles.yaml`):**
* **`synthesis_pro`:** Hochwertige Synthese für Chat-Antworten
* **`tech_expert`:** Fachspezialist für Code & Technik
* **`compression_fast`:** Schnelle Kompression & Routing
* **`ingest_validator`:** Deterministische Validierung (Temperature 0.0)
* **`identity_safe`:** Lokaler Anker (Ollama/Phi-3) für maximale Privacy
**Rekursive Fallback-Kaskade:**
Der `LLMService` (v3.5.2) implementiert eine automatische Fallback-Logik:
1. Versucht primäres Profil (z.B. `synthesis_pro`)
2. Bei Fehler → `fallback_profile` (z.B. `synthesis_backup`)
3. Bei weiterem Fehler → nächster Fallback (z.B. `identity_safe`)
4. Schutz gegen Zirkel-Referenzen via `visited_profiles`-Tracking
**Integration:**
* **Intent-Routing:** Nutzt `router_profile` (z.B. `compression_fast`)
* **Stream-Kompression:** Nutzt `compression_profile` pro Stream
* **Synthese:** Nutzt `llm_profile` aus Strategie-Konfiguration
* **Ingestion:** Nutzt `ingest_validator` für binäre Validierungen
### 1.3 Prompt-Auflösung (Bulletproof Resolution)
Um Kompatibilitätsprobleme mit verschachtelten YAML-Prompts zu vermeiden, nutzt der Router die Methode `llm.get_prompt()`. Diese implementiert eine **Provider-Kaskade**:
* **Spezifischer Provider:** Das System sucht zuerst nach einem Prompt für den aktiv konfigurierten Provider (z.B. `openrouter`).
@ -36,7 +60,7 @@ Um Kompatibilitätsprobleme mit verschachtelten YAML-Prompts zu vermeiden, nutzt
* **Basis-Fallback:** Als letzte Instanz wird das `ollama`-Template geladen.
* **String-Garantie:** Die Methode garantiert die Rückgabe eines Strings (selbst bei verschachtelten YAML-Dicts), was 500-Fehler bei String-Operationen wie `.replace()` oder `.format()` verhindert.
### 1.2 Multi-Stream Retrieval (WP-25)
### 1.4 Multi-Stream Retrieval (WP-25)
Anstelle einer einzelnen Suche führt die `DecisionEngine` nun **parallele Abfragen** in spezialisierten Streams aus:
@ -57,7 +81,22 @@ Anstelle einer einzelnen Suche führt die `DecisionEngine` nun **parallele Abfra
* **Stream-Tracing:** Jeder Treffer wird mit `stream_origin` markiert für Feedback-Optimierung.
* **Fehlerbehandlung:** Einzelne Stream-Fehler blockieren nicht die gesamte Anfrage.
### 1.3 Wissens-Synthese (WP-25)
### 1.5 Pre-Synthesis Kompression (WP-25a)
Wissens-Streams, die den Schwellenwert (`compression_threshold`) überschreiten, werden **asynchron verdichtet**, bevor sie die Synthese erreichen:
**Kompression-Logik:**
* **Schwellenwert:** Konfigurierbar pro Stream (z.B. 2500 Zeichen für Values Stream)
* **Profil:** Nutzt `compression_profile` (z.B. `compression_fast` für schnelle Zusammenfassung)
* **Parallelisierung:** Mehrere Streams können gleichzeitig komprimiert werden
* **Fehlerbehandlung:** Kompressions-Fehler blockieren nicht die Synthese (Original-Content wird verwendet)
**Vorteile:**
* Reduziert Token-Verbrauch bei langen Streams
* Beschleunigt Synthese durch kürzere Kontexte
* Erhält Relevanz durch intelligente Zusammenfassung
### 1.6 Wissens-Synthese (WP-25/25a)
Die Zusammenführung der Daten erfolgt über spezialisierte Templates in der `prompts.yaml`:
@ -66,20 +105,29 @@ Die Zusammenführung der Daten erfolgt über spezialisierte Templates in der `pr
* **Pre-Initialization:** Alle möglichen Stream-Variablen werden vorab initialisiert (verhindert KeyErrors).
* **Provider-spezifische Templates:** Separate Versionen für Ollama, Gemini und OpenRouter.
**Synthese-Strategien:**
* **FACT_WHAT/FACT_WHEN:** Kombiniert Fakten, Biographie und Technik.
* **DECISION:** Wägt Fakten gegen Werte ab, evaluiert Risiken.
* **EMPATHY:** Fokus auf Biographie und Werte.
* **CODING:** Technik und Fakten.
**Synthese-Strategien (Profil-gesteuert):**
* **FACT_WHAT/FACT_WHEN:** Nutzt `synthesis_pro` - Kombiniert Fakten, Biographie und Technik.
* **DECISION:** Nutzt `synthesis_pro` - Wägt Fakten gegen Werte ab, evaluiert Risiken.
* **EMPATHY:** Nutzt `synthesis_pro` - Fokus auf Biographie und Werte.
* **CODING:** Nutzt `tech_expert` - Spezialisiertes Modell für Code & Technik.
### 1.4 RAG Flow (Technisch)
**Profil-Auflösung:**
Jede Strategie kann ein individuelles `llm_profile` definieren. Fehlt diese Angabe, wird `synthesis_pro` als Standard verwendet.
### 1.7 RAG Flow (Technisch - WP-25a)
Wenn der Intent nicht `INTERVIEW` ist, wird folgender Flow ausgeführt:
1. **Intent Detection:** Hybrid Router klassifiziert die Anfrage.
1. **Intent Detection:** Hybrid Router klassifiziert die Anfrage via `router_profile` (z.B. `compression_fast`).
2. **Multi-Stream Retrieval:**
* Parallele Abfragen in spezialisierten Streams via `DecisionEngine._execute_parallel_streams()`.
* Jeder Stream nutzt individuelle Filter, Edge-Boosts und Query-Templates.
3. **Pre-Synthesis Kompression (WP-25a):**
* Streams über `compression_threshold` werden via `compression_profile` verdichtet.
* Parallelisierung über `asyncio.gather()` für mehrere Streams gleichzeitig.
4. **Wissens-Synthese:**
* Strategie-spezifisches `llm_profile` (z.B. `tech_expert` für CODING) steuert die finale Antwortgenerierung.
* Fallback-Kaskade bei Fehlern (automatisch via LLMService).
3. **Context Formatting:**
* Stream-Ergebnisse werden in formatierte Kontext-Strings umgewandelt.
* **Ollama Context-Throttling:** Kontext wird auf `MAX_OLLAMA_CHARS` begrenzt (Standard: 10.000).

View File

@ -1,10 +1,10 @@
---
doc_type: technical_reference
audience: developer, admin
scope: configuration, env, registry, scoring, resilience, modularization, agentic_rag
scope: configuration, env, registry, scoring, resilience, modularization, agentic_rag, moe
status: active
version: 2.9.3
context: "Umfassende Referenztabellen für Umgebungsvariablen (inkl. Hybrid-Cloud & WP-76), YAML-Konfigurationen, Edge Registry Struktur und WP-25 Multi-Stream RAG unter Berücksichtigung von WP-14."
version: 3.0.0
context: "Umfassende Referenztabellen für Umgebungsvariablen (inkl. Hybrid-Cloud & WP-76), YAML-Konfigurationen, Edge Registry Struktur, WP-25 Multi-Stream RAG und WP-25a Mixture of Experts (MoE) unter Berücksichtigung von WP-14."
---
# Konfigurations-Referenz
@ -329,6 +329,80 @@ Alle möglichen Stream-Variablen werden vorab initialisiert (verhindert KeyError
**Provider-spezifische Templates:**
Separate Versionen für Ollama, Gemini und OpenRouter.
---
## 6. LLM Profile Registry (`llm_profiles.yaml` v1.3.0)
Seit WP-25a nutzt MindNet eine **Mixture of Experts (MoE)** Architektur mit profilbasierter Experten-Steuerung. Jede Systemaufgabe (Synthese, Ingestion-Validierung, Routing, Kompression) wird einem dedizierten Profil zugewiesen, das Modell, Provider und Parameter unabhängig von der globalen Konfiguration definiert.
### 6.1 Profil-Struktur
Jedes Profil definiert:
* **`provider`:** Cloud-Provider (`openrouter`, `gemini`, `ollama`)
* **`model`:** Spezifisches Modell (z.B. `mistralai/mistral-7b-instruct:free`)
* **`temperature`:** Kreativität/Determinismus (0.0 = deterministisch, 1.0 = kreativ)
* **`fallback_profile`:** Optional: Name des Fallback-Profils bei Fehlern
* **`dimensions`:** Optional: Für Embedding-Profile (z.B. 768 für nomic-embed-text)
**Beispiel:**
```yaml
synthesis_pro:
provider: "openrouter"
model: "gemini-1.5-mistralai/mistral-7b-instruct:free"
temperature: 0.7
fallback_profile: "synthesis_backup"
```
### 6.2 Verfügbare Experten-Profile
| Profil | Provider | Modell | Temperature | Fallback | Zweck |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **`synthesis_pro`** | openrouter | gemini-1.5-mistralai/mistral-7b-instruct:free | 0.7 | `synthesis_backup` | Hochwertige Synthese (Chat-Antworten) |
| **`synthesis_backup`** | openrouter | mistralai/mistral-large | 0.5 | `identity_safe` | Backup-Cloud-Experte (Resilienz) |
| **`tech_expert`** | openrouter | anthropic/claude-3.5-sonnet | 0.3 | `synthesis_pro` | Fachspezialist für Code & Technik |
| **`compression_fast`** | openrouter | mistralai/mistral-7b-instruct:free | 0.1 | `identity_safe` | Schnelle Kompression & Routing |
| **`ingest_extractor`** | openrouter | mistralai/mistral-7b-instruct:free | 0.2 | `synthesis_backup` | Extraktion komplexer Datenstrukturen |
| **`ingest_validator`** | openrouter | mistralai/mistral-7b-instruct:free | 0.0 | `compression_fast` | Binäre Prüfungen (YES/NO, deterministisch) |
| **`identity_safe`** | ollama | phi3:mini | 0.2 | *(kein Fallback)* | Lokaler Anker & Privacy (terminaler Endpunkt) |
| **`embedding_expert`** | ollama | nomic-embed-text | - | - | Embedding-Modell (dimensions: 768) |
### 6.3 Fallback-Kaskade (WP-25a)
Die Profile implementieren eine **rekursive Fallback-Kaskade**:
1. **Primäres Profil:** System versucht das angeforderte Profil (z.B. `synthesis_pro`)
2. **Fallback-Level 1:** Bei Fehler → `fallback_profile` (z.B. `synthesis_backup`)
3. **Fallback-Level 2:** Bei weiterem Fehler → nächster Fallback (z.B. `identity_safe`)
4. **Terminaler Endpunkt:** `identity_safe` hat keinen Fallback (lokales Modell als letzte Instanz)
**Schutzmechanismen:**
* **Zirkuläre Referenzen:** `visited_profiles`-Tracking verhindert Endlosschleifen
* **Background-Semaphore:** Parallele Tasks werden gedrosselt (konfigurierbar via `BACKGROUND_LIMIT`)
### 6.4 Integration in Decision Engine
Die `decision_engine.yaml` referenziert Profile über:
* **`router_profile`:** Profil für Intent-Erkennung (z.B. `compression_fast`)
* **`llm_profile`:** Profil für Strategie-spezifische Synthese (z.B. `tech_expert` für CODING)
* **`compression_profile`:** Profil für Stream-Kompression (z.B. `compression_fast`)
**Stream-Konfiguration:**
```yaml
values_stream:
llm_profile: "identity_safe" # Lokales Modell für Privacy
compression_profile: "identity_safe"
compression_threshold: 2500
```
### 6.5 Environment-Variablen
| Variable | Default | Beschreibung |
| :--- | :--- | :--- |
| `MINDNET_LLM_PROFILES_PATH` | `config/llm_profiles.yaml` | Pfad zur Profil-Registry |
**Hinweis:** Die `.env` Variablen `MINDNET_LLM_PROVIDER`, `MINDNET_LLM_MODEL` etc. dienen nur noch als Fallback, wenn kein Profil angegeben wird.
---
Auszug aus der decision_engine.yaml
```yaml

View File

@ -1,10 +1,10 @@
---
doc_type: technical_reference
audience: developer, devops
scope: backend, ingestion, smart_edges, edge_registry, modularization
scope: backend, ingestion, smart_edges, edge_registry, modularization, moe
status: active
version: 2.13.12
context: "Detaillierte technische Beschreibung der Import-Pipeline, Two-Pass-Workflow (WP-15b) und modularer Datenbank-Architektur (WP-14). Integriert Mistral-safe Parsing und Deep Fallback."
version: 2.14.0
context: "Detaillierte technische Beschreibung der Import-Pipeline, Two-Pass-Workflow (WP-15b), modularer Datenbank-Architektur (WP-14) und WP-25a profilgesteuerte Validierung. Integriert Mistral-safe Parsing und Deep Fallback."
---
# Ingestion Pipeline & Smart Processing
@ -50,9 +50,11 @@ Der Prozess ist **asynchron**, **idempotent** und wird nun in zwei logische Durc
* Bei Änderungen löscht `purge_artifacts()` via `app.core.ingestion.ingestion_db` alle alten Chunks und Edges der Note.
* Die Namensauflösung erfolgt nun über das modularisierte `database`-Paket.
10. **Chunking anwenden:** Zerlegung des Textes basierend auf dem ermittelten Profil (siehe Kap. 3).
11. **Smart Edge Allocation & Semantic Validation (WP-15b):**
11. **Smart Edge Allocation & Semantic Validation (WP-15b / WP-25a):**
* Der `SemanticAnalyzer` schlägt Kanten-Kandidaten vor.
* **Validierung:** Jeder Kandidat wird durch das LLM semantisch gegen das Ziel im **LocalBatchCache** geprüft.
* **Validierung (WP-25a):** Jeder Kandidat wird durch das LLM semantisch gegen das Ziel im **LocalBatchCache** geprüft.
* **Profilgesteuerte Validierung:** Nutzt das MoE-Profil `ingest_validator` (Temperature 0.0 für maximale Determinismus).
* **Fallback-Kaskade:** Bei Fehlern erfolgt automatischer Fallback via `fallback_profile` (z.B. `compression_fast``identity_safe`).
* **Traffic Control:** Nutzung der neutralen `clean_llm_text` Funktion zur Bereinigung von Steuerzeichen (<s>, [OUT]).
* **Deep Fallback (v2.11.14):** Erkennt "Silent Refusals". Liefert die Cloud keine verwertbaren Kanten, wird ein lokaler Fallback via Ollama erzwungen.
12. **Inline-Kanten finden:** Parsing von `[[rel:...]]` und Callouts.
@ -60,7 +62,9 @@ Der Prozess ist **asynchron**, **idempotent** und wird nun in zwei logische Durc
* Jede Kante wird via `EdgeRegistry` normalisiert (z.B. `basiert_auf` -> `based_on`).
* Unbekannte Typen werden in `unknown_edges.jsonl` protokolliert.
14. **Default- & Strukturkanten:** Anwendung der `edge_defaults` und Erzeugung von Systemkanten (`belongs_to`, `next`, `prev`).
15. **Embedding (Async):** Generierung der Vektoren via `nomic-embed-text` (768 Dimensionen).
15. **Embedding (Async - WP-25a):** Generierung der Vektoren via `embedding_expert` Profil aus `llm_profiles.yaml`.
* **Profil-Auflösung:** Das `EmbeddingsClient` lädt Modell und Dimensionen direkt aus dem Profil (z.B. `nomic-embed-text`, 768 Dimensionen).
* **Konsolidierung:** Entfernung der Embedding-Konfiguration aus der `.env` zugunsten zentraler Profil-Registry.
16. **Database Sync (WP-14):** Batch-Upsert aller Points in die Collections `{prefix}_chunks` und `{prefix}_edges` über die zentrale Infrastruktur.
---
@ -189,4 +193,6 @@ Kanten werden nach Vertrauenswürdigkeit (`provenance`) priorisiert. Die höhere
**2. Mistral-safe Parsing:** Automatisierte Bereinigung von LLM-Antworten in `ingestion_validation.py`. Stellt sicher, dass semantische Entscheidungen ("YES"/"NO") nicht durch technische Header verfälscht werden.
**3. Profilgesteuerte Validierung (WP-25a):** Die semantische Kanten-Validierung erfolgt zwingend über das MoE-Profil `ingest_validator` (Temperature 0.0 für Determinismus). Dies gewährleistet konsistente binäre Entscheidungen (YES/NO) unabhängig von der globalen Provider-Konfiguration.
**3. Purge Integrity:** Validierung, dass vor jedem Upsert alle assoziierten Artefakte in den Collections `{prefix}_chunks` und `{prefix}_edges` gelöscht wurden, um Daten-Duplikate zu vermeiden.

View File

@ -1,10 +1,10 @@
---
doc_type: operations_manual
audience: admin, devops
scope: deployment, maintenance, backup, edge_registry
scope: deployment, maintenance, backup, edge_registry, moe
status: active
version: 2.7.0
context: "Installationsanleitung, Systemd-Units und Wartungsprozesse für Mindnet v2.7."
version: 3.0.0
context: "Installationsanleitung, Systemd-Units und Wartungsprozesse für Mindnet v3.0.0 inklusive WP-25a Mixture of Experts (MoE) Konfiguration."
---
# Admin Operations Guide
@ -46,7 +46,7 @@ Um Abstürze der Vektordatenbank bei einer hohen Anzahl an Collections (z. B. du
Hintergrund: Qdrant öffnet für jedes Segment einer Collection mehrere Dateien. Ohne diese Erhöhung führt das Standard-Linux-Limit (1024) zum Absturz mit dem Fehler os error 24 (Too many open files).
### 1.3 Ollama (Modelle)
**Wichtig:** Seit v2.4 ist `nomic-embed-text` Pflicht für Embeddings.
**Wichtig:** Seit v2.4 ist `nomic-embed-text` Pflicht für Embeddings. Seit WP-25a wird die Modell-Konfiguration zentral über `llm_profiles.yaml` gesteuert.
```bash
# Modelle laden
@ -57,6 +57,14 @@ ollama pull nomic-embed-text
curl http://localhost:11434/api/generate -d '{"model": "phi3:mini", "prompt":"Hi"}'
```
**WP-25a: LLM-Profil-Konfiguration**
Die LLM-Steuerung erfolgt nun primär über `config/llm_profiles.yaml` statt ENV-Variablen:
* **Zentrale Registry:** Alle Experten-Profile (Synthese, Validierung, Kompression) sind in einer Datei definiert
* **Fallback-Kaskade:** Automatische Resilienz bei Provider-Fehlern
* **ENV-Variablen:** `MINDNET_LLM_PROVIDER`, `MINDNET_LLM_MODEL` etc. dienen nur noch als Fallback
Siehe [Konfigurations-Referenz](../03_Technical_References/03_tech_configuration.md#6-llm-profile-registry-llm_profilesyaml-v130) für Details.
---
## 2. Deployment (Systemd Services)

View File

@ -393,13 +393,24 @@ Mindnet lernt nicht durch Training (Fine-Tuning), sondern durch **Konfiguration*
edge_defaults: ["blocks"] # Automatische Kante
detection_keywords: ["gefahr", "risiko"]
```
2. **Strategie (`config/decision_engine.yaml` v3.1.6, WP-25):**
2. **Strategie (`config/decision_engine.yaml` v3.2.2, WP-25/25a):**
```yaml
DECISION:
use_streams: ["values_stream", "facts_stream", "risk_stream"] # WP-25: Multi-Stream
llm_profile: "synthesis_pro" # WP-25a: MoE-Profil für Synthese
inject_types: ["value", "risk"] # Legacy: Fallback für nicht-Stream-Typen
```
*Ergebnis (WP-25):* Wenn der Intent `DECISION` erkannt wird, führt das System parallele Abfragen in Values, Facts und Risk Streams aus und synthetisiert die Ergebnisse.
*Ergebnis (WP-25/25a):* Wenn der Intent `DECISION` erkannt wird, führt das System parallele Abfragen in Values, Facts und Risk Streams aus, komprimiert überlange Streams via `compression_profile` und synthetisiert die Ergebnisse mit dem `synthesis_pro` Profil.
3. **LLM-Profil (`config/llm_profiles.yaml` v1.3.0, WP-25a):**
```yaml
synthesis_pro:
provider: "openrouter"
model: "gemini-1.5-mistralai/mistral-7b-instruct:free"
temperature: 0.7
fallback_profile: "synthesis_backup"
```
*Ergebnis (WP-25a):* Zentrale Steuerung von Provider, Modell und Temperature pro Aufgabe. Automatische Fallback-Kaskade bei Fehlern.
### Workflow B: Graph-Farben ändern
1. Öffne `app/frontend/ui_config.py`.

View File

@ -194,7 +194,10 @@ Der bisherige WP-15 Ansatz litt unter Halluzinationen (erfundene Kantentypen), h
3. **Self-Learning Loop:** Protokollierung unbekannter Kanten in `unknown_edges.jsonl`.
### WP-25: Agentic Multi-Stream RAG Orchestration
**Status:** ✅ Fertig (v2.9.3)
**Status:** ✅ Fertig (v3.0.0)
### WP-25a: Mixture of Experts (MoE) & Fallback-Kaskade
**Status:** ✅ Fertig (v3.0.0)
**Ergebnis:** Transformation von Mindnet von einer klassischen, linearen RAG-Architektur zu einer **Agentic Multi-Stream Engine**. Das System agiert nun als intelligenter Orchestrator, der Nutzeranfragen analysiert, in parallele Wissens-Streams aufteilt und diese zu einer kontextreichen, wertebasierten Antwort synthetisiert.
@ -212,8 +215,26 @@ Der bisherige WP-15 Ansatz litt unter Halluzinationen (erfundene Kantentypen), h
- decision_engine.yaml v3.1.6: Multi-Stream Konfiguration
- prompts.yaml v3.1.2: Stream-Templates
**Ausblick (WP-25a):**
- Pre-Synthesis: LLM-basierte Komprimierung überlanger Streams
**Ergebnis (WP-25a):** Transformation von MindNet von einer provider-basierten Steuerung auf eine **profilbasierte Experten-Architektur (Mixture of Experts)**. Jede Systemaufgabe wird einem dedizierten Profil zugewiesen, das Modell, Provider und Parameter unabhängig definiert.
**Kern-Features:**
1. **Experten-Steuerung:** Zentrale Profile-Registry (`llm_profiles.yaml`) für alle LLM-Aufgaben
2. **Rekursive Fallback-Kaskade:** Automatische Resilienz bei Provider-Fehlern mit Schutz gegen Zirkel-Referenzen
3. **Pre-Synthesis Kompression:** Asynchrone Verdichtung überlanger Wissens-Streams vor der Synthese
4. **Profilgesteuerte Ingestion:** Deterministische Validierung via `ingest_validator` (Temperature 0.0)
5. **Embedding-Konsolidierung:** Zentrale Steuerung des Embedding-Modells über `embedding_expert` Profil
6. **Startup-Schutz:** Validierung der YAML-Konfigurationen beim Booten
**Technische Details:**
- LLM Service v3.5.2: Rekursive Fallback-Kaskade
- Decision Engine v1.2.1: Profile-Driven Orchestration
- Ingestion Processor v2.14.0: Profilgesteuerte Validierung
- Embeddings Client v2.6.0: Profil-basierte Modell-Auflösung
- llm_profiles.yaml v1.3.0: Zentrale Experten-Registry
- decision_engine.yaml v3.2.2: Decoupled MoE Logic
**Ausblick (WP-25b):**
- Prompt-Orchestration & Model-Specific Tuning
- Kontext-Budgeting: Intelligente Token-Verteilung
- Stream-specific Provider: Unterschiedliche KI-Modelle pro Wissensbereich
---

View File

@ -0,0 +1,103 @@
# Branch Merge Commit: WP-25a
**Branch:** `WP25a`
**Target:** `main`
**Version:** v3.1.0
**Date:** 2026-01-02
---
## Commit Message
```
feat: Mixture of Experts (MoE) & Fallback-Kaskade (v3.0.0)
### Mixture of Experts (MoE) Architektur
- Übergang von provider-basierter zu profilbasierter Experten-Steuerung
- Zentrale Experten-Registry (`llm_profiles.yaml` v1.3.0)
- Aufgabenspezifische Profile: synthesis_pro, tech_expert, compression_fast, ingest_validator, identity_safe, embedding_expert
- Hardware-Optimierung: Lokaler Anker (Ollama/Phi-3) für maximale Privacy
### Rekursive Fallback-Kaskade
- Implementierung in `app/services/llm_service.py` (v3.5.2)
- Automatische Fallback-Logik bei Provider-Fehlern
- Schutz gegen Zirkel-Referenzen via `visited_profiles`-Tracking
- Background-Semaphore für parallele Tasks
### Pre-Synthesis Kompression (Module A)
- Asynchrone Verdichtung überlanger Wissens-Streams
- Konfigurierbare Schwellenwerte pro Stream (`compression_threshold`)
- Profil-gesteuerte Kompression via `compression_profile`
- Parallelisierung über `asyncio.gather()`
### Profilgesteuerte Ingestion
- Semantische Kanten-Validierung via `ingest_validator` (Temperature 0.0)
- Embedding-Konsolidierung über `embedding_expert` Profil
- Entfernung der Embedding-Konfiguration aus `.env`
### Startup-Schutz & Audit-Fixes
- Validierung von `llm_profiles.yaml` und `decision_engine.yaml` beim Booten
- Behebung der Sicherheitslücke in `DecisionEngine` (Fallback-Aufrufe nutzen nun `profile_name`)
- Circular Import Fix: Ingestion-Module nutzen neutrale `app.core.registry`
### Code-Komponenten
- `app/services/llm_service.py`: v3.5.2 (Rekursive Fallback-Kaskade)
- `app/core/retrieval/decision_engine.py`: v1.2.1 (Profile-Driven Orchestration)
- `app/core/ingestion/ingestion_processor.py`: v2.14.0 (Profilgesteuerte Validierung)
- `app/core/ingestion/ingestion_validation.py`: v2.13.0 (MoE-Profil Integration)
- `app/services/embeddings_client.py`: v2.6.0 (Profil-basierte Modell-Auflösung)
- `app/main.py`: v1.1.0 (Startup-Validierung)
### Konfiguration
- `config/llm_profiles.yaml`: v1.3.0 (Zentrale Experten-Registry)
- `config/decision_engine.yaml`: v3.2.2 (Decoupled MoE Logic, compression_thresholds)
### Dokumentation
- `03_tech_configuration.md`: llm_profiles.yaml Dokumentation
- `03_tech_chat_backend.md`: MoE Architektur und Fallback-Kaskade
- `02_concept_ai_personality.md`: Mixture of Experts Konzept
- `03_tech_ingestion_pipeline.md`: Profilgesteuerte Validierung
- `00_glossary.md`: Neue Begriffe (MoE, Profile, Fallback-Kaskade)
- `05_developer_guide.md`: Teach-the-AI mit Profilen
- `04_admin_operations.md`: Konfigurations-Updates
- `06_active_roadmap.md`: WP25a als abgeschlossen markiert
### Breaking Changes
- Keine Breaking Changes für Endbenutzer
- ENV-Variablen `MINDNET_LLM_PROVIDER`, `MINDNET_LLM_MODEL` etc. dienen nur noch als Fallback
- Neue Konfigurationsdatei `llm_profiles.yaml` ist erforderlich (Startup-Validierung)
### Migration
- Administratoren müssen `config/llm_profiles.yaml` erstellen
- System startet nicht, wenn `llm_profiles.yaml` fehlt oder ungültig ist
- ENV-Variablen bleiben als Fallback erhalten
---
**Status:** ✅ WP-25a ist zu 100% implementiert und audit-geprüft.
**Nächster Schritt:** WP-25b (Prompt-Orchestration & Model-Specific Tuning).
```
---
## Zusammenfassung
Dieser Merge führt die **Mixture of Experts (MoE) Architektur** in MindNet ein. Das System nutzt nun eine profilbasierte Experten-Steuerung statt einer globalen Provider-Konfiguration. Jede Systemaufgabe wird einem dedizierten Profil zugewiesen, das Modell, Provider und Parameter unabhängig definiert.
**Kern-Features:**
- Zentrale Experten-Registry (`llm_profiles.yaml`)
- Rekursive Fallback-Kaskade mit Schutz gegen Zirkel-Referenzen
- Pre-Synthesis Kompression für überlange Wissens-Streams
- Profilgesteuerte Ingestion mit deterministischer Validierung
- Startup-Schutz und Audit-Fixes
**Technische Integrität:**
- Alle LLM-Aufrufe nutzen nun die Profilsteuerung
- Startup-Validierung verhindert fehlerhafte Konfigurationen
- Circular Import Fix verbessert Wartbarkeit
**Dokumentation:**
- Vollständige Aktualisierung aller relevanten Dokumente
- Neue Begriffe im Glossar
- Konfigurations-Referenz erweitert
- Developer Guide aktualisiert

View File

@ -0,0 +1,186 @@
# MindNet v3.0.0 - Release Notes: WP-25a
**Release Date:** 2026-01-02
**Type:** Feature Release - Mixture of Experts (MoE) & Fallback-Kaskade
**Version:** 3.1.0 (WP-25a)
---
## 🎯 Überblick
Mit WP-25a wurde MindNet von einer provider-basierten Steuerung auf eine **profilbasierte Experten-Architektur (Mixture of Experts)** umgestellt. Jede Systemaufgabe (Synthese, Ingestion-Validierung, Routing, Kompression) wird nun einem dedizierten Profil zugewiesen, das Modell, Provider und Parameter unabhängig von der globalen Konfiguration definiert.
Diese Version markiert einen fundamentalen Architektur-Sprung: Von einer monolithischen LLM-Konfiguration hin zu einer modularen, aufgabenspezifischen Experten-Steuerung mit automatischer Resilienz.
---
## ✨ Neue Features
### 1. Mixture of Experts (MoE) Architektur
**Zentrale Experten-Steuerung (`llm_profiles.yaml` v1.3.0):**
* Einführung von Experten-Profilen für spezifische Aufgaben:
* `synthesis_pro`: Hochwertige Synthese für Chat-Antworten
* `tech_expert`: Fachspezialist für Code & Technik (Claude 3.5 Sonnet)
* `compression_fast`: Schnelle Kompression & Routing (Mistral 7B)
* `ingest_validator`: Deterministische Validierung (Temperature 0.0)
* `identity_safe`: Lokaler Anker (Ollama/Phi-3) für maximale Privacy
* `embedding_expert`: Zentrale Steuerung des Embedding-Modells
**Vorteile:**
* **Aufgabenspezifische Optimierung:** Jede Aufgabe nutzt das optimale Modell
* **Hardware-Optimierung:** Lokaler Anker für kleine Hardware-Umgebungen
* **Wartbarkeit:** Zentrale Konfiguration statt verstreuter ENV-Variablen
### 2. Rekursive Fallback-Kaskade
**Implementierung (`app/services/llm_service.py` v3.5.2):**
* Automatische Fallback-Logik bei Provider-Fehlern:
1. Versucht primäres Profil (z.B. `synthesis_pro`)
2. Bei Fehler → `fallback_profile` (z.B. `synthesis_backup`)
3. Bei weiterem Fehler → nächster Fallback (z.B. `identity_safe`)
4. Terminaler Endpunkt: `identity_safe` hat keinen Fallback (lokales Modell)
**Schutzmechanismen:**
* **Zirkuläre Referenzen:** `visited_profiles`-Tracking verhindert Endlosschleifen
* **Background-Semaphore:** Parallele Tasks werden gedrosselt (konfigurierbar via `BACKGROUND_LIMIT`)
### 3. Pre-Synthesis Kompression (Module A)
**Implementierung (`app/core/retrieval/decision_engine.py` v1.2.1):**
* Wissens-Streams, die den Schwellenwert (`compression_threshold`) überschreiten, werden **asynchron verdichtet**, bevor sie die Synthese erreichen
* **Konfigurierbar pro Stream:** Z.B. 2500 Zeichen für Values Stream, 3500 für Facts Stream
* **Profil-gesteuert:** Nutzt `compression_profile` (z.B. `compression_fast` für schnelle Zusammenfassung)
* **Parallelisierung:** Mehrere Streams können gleichzeitig komprimiert werden
**Vorteile:**
* Reduziert Token-Verbrauch bei langen Streams
* Beschleunigt Synthese durch kürzere Kontexte
* Erhält Relevanz durch intelligente Zusammenfassung
### 4. Profilgesteuerte Ingestion (Wissens-Gatekeeper)
**Implementierung:**
* `app/core/ingestion/ingestion_processor.py` v2.14.0
* `app/core/ingestion/ingestion_validation.py` v2.13.0
**Features:**
* Semantische Kanten-Validierung erfolgt zwingend über das MoE-Profil `ingest_validator` (Temperature 0.0 für Determinismus)
* **Embedding-Konsolidierung:** Der `EmbeddingsClient` (v2.6.0) bezieht Modellvorgaben und Dimensionen direkt aus dem Profil `embedding_expert`
* Entfernung der Embedding-Konfiguration aus der `.env` zugunsten zentraler Profil-Registry
### 5. Startup-Schutz & Audit-Fixes
**Implementierung (`app/main.py` v1.1.0):**
* Verifiziert beim Booten die Existenz und Validität von `llm_profiles.yaml` und `decision_engine.yaml`
* **Audit-Fix:** Behebung einer Sicherheitslücke in der `DecisionEngine`, durch die Fallback-Aufrufe bei Template-Fehlern die Profilsteuerung umgangen hätten
* **Circular Import Fix:** Ingestion-Module nutzen nun die neutrale `app.core.registry` für Text-Bereinigung und Registry-Lookups
---
## 🔧 Technische Änderungen
### Konfigurationsdateien
**Neue Datei:**
* `config/llm_profiles.yaml` v1.3.0: Zentrale Experten-Registry
**Aktualisierte Dateien:**
* `config/decision_engine.yaml` v3.2.2: Decoupled MoE Logic, Integration von `compression_thresholds`
### Code-Komponenten
| Komponente | Version | Änderungen |
| :--- | :--- | :--- |
| `app/services/llm_service.py` | v3.5.2 | Rekursive Fallback-Kaskade, Profil-Auflösung |
| `app/core/retrieval/decision_engine.py` | v1.2.1 | Profile-Driven Orchestration, Pre-Synthesis Kompression |
| `app/core/ingestion/ingestion_processor.py` | v2.14.0 | Profilgesteuerte Validierung |
| `app/core/ingestion/ingestion_validation.py` | v2.13.0 | MoE-Profil `ingest_validator` |
| `app/services/embeddings_client.py` | v2.6.0 | Profil-basierte Modell-Auflösung |
| `app/main.py` | v1.1.0 | Startup-Validierung der YAML-Dateien |
### Environment-Variablen
**Neue Variable:**
* `MINDNET_LLM_PROFILES_PATH`: Pfad zur Profil-Registry (Default: `config/llm_profiles.yaml`)
**Legacy (nur noch Fallback):**
* `MINDNET_LLM_PROVIDER`, `MINDNET_LLM_MODEL`, etc. dienen nur noch als Fallback, wenn kein Profil angegeben wird
---
## 🐛 Behobene Probleme
- ✅ **Behoben:** Sicherheitslücke in der `DecisionEngine` - Fallback-Aufrufe nutzen nun korrekt `profile_name`
- ✅ **Behoben:** Circular Import zwischen Ingestion-Modulen und Registry
- ✅ **Behoben:** Fehlende Startup-Validierung der YAML-Konfigurationen
---
## 📚 Dokumentation
**Aktualisierte Dokumente:**
- ✅ `03_tech_configuration.md`: llm_profiles.yaml Dokumentation
- ✅ `03_tech_chat_backend.md`: MoE Architektur und Fallback-Kaskade
- ✅ `02_concept_ai_personality.md`: Mixture of Experts Konzept
- ✅ `03_tech_ingestion_pipeline.md`: Profilgesteuerte Validierung
- ✅ `00_glossary.md`: Neue Begriffe (MoE, Profile, Fallback-Kaskade)
- ✅ `05_developer_guide.md`: Teach-the-AI mit Profilen
- ✅ `04_admin_operations.md`: Konfigurations-Updates
- ✅ `06_active_roadmap.md`: WP25a als abgeschlossen markiert
---
## 🚀 Migration & Upgrade
### Für Administratoren
1. **Neue Konfigurationsdatei erstellen:**
```bash
cp config/llm_profiles.yaml.example config/llm_profiles.yaml
# Anpassen nach Bedarf
```
2. **Startup-Validierung prüfen:**
```bash
# System startet nicht, wenn llm_profiles.yaml fehlt oder ungültig ist
python3 -m app.main
```
3. **ENV-Variablen (optional):**
Die `.env` Variablen `MINDNET_LLM_PROVIDER`, `MINDNET_LLM_MODEL` etc. dienen nur noch als Fallback. Die primäre Steuerung erfolgt über `llm_profiles.yaml`.
### Für Entwickler
**API-Änderungen:**
* `LLMService.generate_raw_response()` unterstützt nun `profile_name` Parameter
* Fallback-Kaskade erfolgt automatisch bei Fehlern
* `visited_profiles`-Tracking verhindert Zirkel-Referenzen
**Konfiguration:**
* Neue Profile können in `llm_profiles.yaml` definiert werden
* `decision_engine.yaml` referenziert Profile über `router_profile`, `llm_profile`, `compression_profile`
---
## 🔮 Ausblick (WP-25b: Prompt-Orchestration & Model-Specific Tuning)
- Pre-Synthesis: LLM-basierte Komprimierung überlanger Streams (✅ bereits implementiert)
- Kontext-Budgeting: Intelligente Token-Verteilung
- Stream-specific Provider: Unterschiedliche KI-Modelle pro Wissensbereich
- Prompt-Orchestration: Dynamische Prompt-Anpassung basierend auf Profil und Kontext
---
## 📊 Metriken & Performance
**Erwartete Verbesserungen:**
* **Resilienz:** Automatische Fallback-Kaskade reduziert Ausfallzeiten
* **Token-Effizienz:** Pre-Synthesis Kompression reduziert Token-Verbrauch bei langen Streams
* **Determinismus:** Profilgesteuerte Validierung (Temperature 0.0) erhöht Konsistenz
* **Wartbarkeit:** Zentrale Profil-Registry vereinfacht Konfigurations-Management
---
**Status:** ✅ WP-25a ist zu 100% implementiert und audit-geprüft.
**Nächster Schritt:** WP-25b (Prompt-Orchestration & Model-Specific Tuning).

View File

@ -0,0 +1,103 @@
# Audit: LLM-Profilsteuerung Integration (WP-25a)
**Datum:** 2025-01-XX
**Version:** WP-25a
**Status:** ✅ Abgeschlossen mit Verbesserungen
## Zusammenfassung
Dieses Audit prüft die Vollständigkeit der neuen LLM-Profilsteuerung (MoE - Mixture of Experts) und identifiziert alle Stellen, die die zentrale Steuerung umgehen könnten.
## Gefundene Probleme & Lösungen
### ✅ Problem 1: Fehlendes `profile_name` im Fallback-Code
**Datei:** `app/core/retrieval/decision_engine.py` (Zeile 253-255)
**Problem:** Der Fallback-Aufruf in `_generate_final_answer` nutzte kein `profile_name`, wodurch die Profilsteuerung umgangen wurde.
**Lösung:** ✅ Behoben - Nutzt nun `profile_name=profile` für Konsistenz.
### ⚠️ Problem 2: Ungenutztes Profil `ingest_extractor`
**Datei:** `config/llm_profiles.yaml`
**Problem:** Das Profil `ingest_extractor` ist definiert, wird aber nirgendwo im Code verwendet.
**Status:** ⚠️ Offene Lücke - Profil ist für zukünftige Extraktions-Aufgaben vorgesehen, aktuell nicht benötigt.
### ✅ Problem 3: Externes Script umgeht Steuerung
**Datei:** `scripts/ollama_tool_runner.py`
**Problem:** Script macht direkte Ollama-Aufrufe ohne LLMService.
**Status:** ✅ Akzeptabel - Dies ist ein externes Test-/Demo-Script, kein Teil der Hauptanwendung.
## Vollständige Prüfung aller LLM-Aufrufe
### ✅ Korrekt implementiert (nutzen Profilsteuerung):
1. **`app/core/ingestion/ingestion_validation.py`**
- ✅ Nutzt `profile_name="ingest_validator"` (Zeile 61-64)
- ✅ Delegiert Fallback-Kaskade an LLMService
2. **`app/core/retrieval/decision_engine.py`**
- ✅ `_determine_strategy()`: Nutzt `router_profile` (Zeile 94-96)
- ✅ `_compress_stream_content()`: Nutzt `compression_profile` (Zeile 169-174)
- ✅ `_generate_final_answer()`: Nutzt `llm_profile` aus Strategie (Zeile 244-246)
- ✅ **BEHOBEN:** Fallback nutzt nun auch `profile_name` (Zeile 253-256)
3. **`app/routers/chat.py`**
- ✅ Interview-Modus: Nutzt `profile_name="compression_fast"` (Zeile 204-207)
- ✅ RAG-Modus: Delegiert an DecisionEngine (nutzt Strategie-Profile)
4. **`app/services/embeddings_client.py`**
- ✅ Nutzt `embedding_expert` Profil aus `llm_profiles.yaml` (Zeile 29-47)
- ✅ Konsistente Modellsteuerung für Embeddings
5. **`app/services/llm_service.py`**
- ✅ Zentrale Implementierung der Profilsteuerung
- ✅ Rekursive Fallback-Kaskade implementiert
- ✅ Schutz gegen zirkuläre Referenzen (`visited_profiles`)
### ✅ Keine LLM-Aufrufe (korrekt):
1. **`app/routers/ingest.py`**
- Nutzt nur IngestionService (der wiederum LLMService nutzt)
2. **`app/services/discovery.py`**
- Nutzt nur Retrieval, keine LLM-Aufrufe
3. **`app/frontend/ui_api.py`**
- Macht nur HTTP-Requests zu API-Endpunkten
## Konfigurationsprüfung
### ✅ `config/llm_profiles.yaml`
- ✅ Alle benötigten Profile definiert:
- `synthesis_pro` - Hauptsynthese
- `synthesis_backup` - Backup-Synthese
- `tech_expert` - Code/Technik
- `compression_fast` - Kompression/Routing
- `ingest_validator` - Validierung (YES/NO)
- `ingest_extractor` - Extraktion (aktuell ungenutzt)
- `identity_safe` - Lokaler Privacy-Anker
- `embedding_expert` - Embeddings
- ✅ Fallback-Kaskaden korrekt definiert
- ✅ Temperaturen angemessen gesetzt
### ✅ `config/decision_engine.yaml`
- ✅ Nutzt `router_profile` für Intent-Erkennung
- ✅ Strategien referenzieren `llm_profile`
- ✅ Streams nutzen `compression_profile`
## Empfehlungen
### Sofort umsetzbar:
1. ✅ **BEHOBEN:** Fallback in DecisionEngine nutzt nun Profilsteuerung
### Zukünftige Verbesserungen:
1. **`ingest_extractor` Profil:** Wenn Extraktions-Aufgaben hinzukommen, sollte dieses Profil genutzt werden
2. **Monitoring:** Logging erweitern, um Profil-Nutzung zu tracken
3. **Dokumentation:** Profil-Auswahl-Logik in Entwickler-Dokumentation aufnehmen
## Fazit
**Die LLM-Profilsteuerung ist vollständig integriert.**
**Alle kritischen LLM-Aufrufe nutzen die zentrale Steuerung.**
**Ein kleiner Bug wurde behoben (Fallback ohne Profil).**
⚠️ **Ein Profil (`ingest_extractor`) ist definiert, aber aktuell ungenutzt - dies ist akzeptabel für zukünftige Features.**
Die Architektur ist robust und folgt dem MoE-Prinzip konsequent.