Merge pull request 'WP25a' (#20) from WP25a into main
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 3s

Reviewed-on: #20
This commit is contained in:
Lars 2026-01-02 13:55:09 +01:00
commit d41da670fc
20 changed files with 1160 additions and 347 deletions

View File

@ -1,11 +1,11 @@
""" """
FILE: app/core/ingestion/ingestion_processor.py FILE: app/core/ingestion/ingestion_processor.py
DESCRIPTION: Der zentrale IngestionService (Orchestrator). 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-15b: Two-Pass Workflow mit globalem Kontext-Cache.
WP-20/22: Cloud-Resilienz und Content-Lifecycle integriert. WP-20/22: Cloud-Resilienz und Content-Lifecycle integriert.
AUDIT v2.13.12: Synchronisierung der Profil-Auflösung mit Registry-Defaults. AUDIT v2.14.0: Synchronisierung der Profil-Auflösung mit MoE-Experten.
VERSION: 2.13.12 VERSION: 2.14.0 (WP-25a: MoE & Profile Support)
STATUS: Active STATUS: Active
""" """
import logging import logging
@ -55,11 +55,15 @@ class IngestionService:
# Synchronisierung der Konfiguration mit dem Instanz-Präfix # Synchronisierung der Konfiguration mit dem Instanz-Präfix
self.cfg.prefix = self.prefix self.cfg.prefix = self.prefix
self.client = get_client(self.cfg) self.client = get_client(self.cfg)
self.dim = self.settings.VECTOR_SIZE
self.registry = load_type_registry() self.registry = load_type_registry()
self.embedder = EmbeddingsClient() self.embedder = EmbeddingsClient()
self.llm = LLMService() 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 # Festlegen, welcher Hash für die Change-Detection maßgeblich ist
self.active_hash_mode = self.settings.CHANGE_DETECTION_MODE self.active_hash_mode = self.settings.CHANGE_DETECTION_MODE
self.batch_cache: Dict[str, NoteContext] = {} # WP-15b LocalBatchCache self.batch_cache: Dict[str, NoteContext] = {} # WP-15b LocalBatchCache
@ -155,24 +159,21 @@ class IngestionService:
edge_registry.ensure_latest() edge_registry.ensure_latest()
# Profil-Auflösung via Registry # 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") profile = note_pl.get("chunk_profile", "sliding_standard")
chunk_cfg = get_chunk_config_by_profile(self.registry, profile, note_type) chunk_cfg = get_chunk_config_by_profile(self.registry, profile, note_type)
enable_smart = chunk_cfg.get("enable_smart_edge_allocation", False) enable_smart = chunk_cfg.get("enable_smart_edge_allocation", False)
# WP-15b: Chunker-Aufruf bereitet den Candidate-Pool pro Chunk vor. # 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) 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: for ch in chunks:
filtered = [] filtered = []
for cand in getattr(ch, "candidate_pool", []): 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 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) filtered.append(cand)
else: else:
# Explizite Kanten (Wikilinks/Callouts) werden ungeprüft übernommen # Explizite Kanten (Wikilinks/Callouts) werden ungeprüft übernommen
@ -204,7 +205,6 @@ class IngestionService:
) )
# 4. DB Upsert via modularisierter Points-Logik # 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: if purge_before and old_payload:
purge_artifacts(self.client, self.prefix, note_id) purge_artifacts(self.client, self.prefix, note_id)

View File

@ -1,10 +1,16 @@
""" """
FILE: app/core/ingestion/ingestion_validation.py FILE: app/core/ingestion/ingestion_validation.py
DESCRIPTION: WP-15b semantische Validierung von Kanten gegen den LocalBatchCache. 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 import logging
from typing import Dict, Any from typing import Dict, Any, Optional
from app.core.parser import NoteContext from app.core.parser import NoteContext
# ENTSCHEIDENDER FIX: Import der neutralen Bereinigungs-Logik zur Vermeidung von Circular Imports # ENTSCHEIDENDER FIX: Import der neutralen Bereinigungs-Logik zur Vermeidung von Circular Imports
@ -17,11 +23,12 @@ async def validate_edge_candidate(
edge: Dict, edge: Dict,
batch_cache: Dict[str, NoteContext], batch_cache: Dict[str, NoteContext],
llm_service: Any, llm_service: Any,
provider: str provider: Optional[str] = None,
profile_name: str = "ingest_validator"
) -> bool: ) -> bool:
""" """
WP-15b: Validiert einen Kandidaten semantisch gegen das Ziel im Cache. 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_id = edge.get("to")
target_ctx = batch_cache.get(target_id) target_ctx = batch_cache.get(target_id)
@ -32,14 +39,16 @@ async def validate_edge_candidate(
target_ctx = batch_cache.get(base_id) target_ctx = batch_cache.get(base_id)
# Sicherheits-Fallback (Hard-Link Integrity) # Sicherheits-Fallback (Hard-Link Integrity)
# Explizite Wikilinks oder Callouts werden nicht durch das LLM verifiziert.
if not target_ctx: if not target_ctx:
logger.info(f" [VALIDATION SKIP] No context for '{target_id}' - allowing link.") logger.info(f" [VALIDATION SKIP] No context for '{target_id}' - allowing link.")
return True return True
# Prompt-Abruf (Nutzt Provider-String als Fallback-Key für die prompts.yaml)
template = llm_service.get_prompt("edge_validation", provider) template = llm_service.get_prompt("edge_validation", provider)
try: 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( prompt = template.format(
chunk_text=chunk_text[:1500], chunk_text=chunk_text[:1500],
target_title=target_ctx.title, target_title=target_ctx.title,
@ -47,8 +56,13 @@ async def validate_edge_candidate(
edge_kind=edge.get("kind", "related_to") edge_kind=edge.get("kind", "related_to")
) )
# Die Antwort vom Service anfordern # WP-25a: Profilbasierter Aufruf (Delegiert Fallbacks an den Service)
raw_response = await llm_service.generate_raw_response(prompt, priority="background") # 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 # WP-14 Fix: Zusätzliche Bereinigung zur Sicherstellung der Interpretierbarkeit
response = clean_llm_text(raw_response) 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.") logger.info(f"🚫 [REJECTED] Relation to '{target_id}' irrelevant for this chunk.")
return is_valid return is_valid
except Exception as e: 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 # Im Zweifel (Timeout/Fehler) erlauben wir die Kante, um Datenverlust zu vermeiden
return True return True

View File

@ -1,13 +1,16 @@
""" """
FILE: app/core/retrieval/decision_engine.py 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 Realisiert Multi-Stream Retrieval, Intent-basiertes Routing
und parallele Wissens-Synthese. und die neue Pre-Synthesis Kompression (Module A).
VERSION: 1.0.3 VERSION: 1.2.1 (WP-25a: Profile-Driven Orchestration & Optimized Cascade)
STATUS: Active STATUS: Active
FIX: FIX:
- WP-25 STREAM-TRACING: Kennzeichnung der Treffer mit ihrem Ursprungs-Stream. - WP-25a: Volle Integration der Profil-Kaskade (Delegation an LLMService v3.5.2).
- WP-25 ROBUSTNESS: Pre-Initialization der Stream-Variablen zur Vermeidung von KeyErrors. - 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 asyncio
import logging import logging
@ -32,7 +35,7 @@ class DecisionEngine:
self.config = self._load_engine_config() self.config = self._load_engine_config()
def _load_engine_config(self) -> Dict[str, Any]: 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") path = os.getenv("MINDNET_DECISION_CONFIG", "config/decision_engine.yaml")
if not os.path.exists(path): if not os.path.exists(path):
logger.error(f"❌ Decision Engine Config not found at {path}") logger.error(f"❌ Decision Engine Config not found at {path}")
@ -47,9 +50,9 @@ class DecisionEngine:
async def ask(self, query: str) -> str: async def ask(self, query: str) -> str:
""" """
Hauptmethode des MindNet Chats. 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) strategy_key = await self._determine_strategy(query)
strategies = self.config.get("strategies", {}) strategies = self.config.get("strategies", {})
@ -67,23 +70,29 @@ class DecisionEngine:
if not strategy: if not strategy:
return "Entschuldigung, meine Wissensbasis ist aktuell nicht konfiguriert." 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) 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) return await self._generate_final_answer(strategy_key, strategy, query, stream_results)
async def _determine_strategy(self, query: str) -> str: async def _determine_strategy(self, query: str) -> str:
"""Nutzt den LLM-Router zur Wahl der Such-Strategie.""" """Nutzt den LLM-Router zur Wahl der Such-Strategie via router_profile."""
prompt_key = self.config.get("settings", {}).get("router_prompt_key", "intent_router_v1") 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) router_prompt_template = self.llm_service.get_prompt(prompt_key)
if not router_prompt_template: if not router_prompt_template:
return "FACT_WHAT" return "FACT_WHAT"
full_prompt = router_prompt_template.format(query=query) full_prompt = router_prompt_template.format(query=query)
try: try:
# Der LLMService übernimmt hier über das Profil bereits die Fallback-Kaskade
response = await self.llm_service.generate_raw_response( 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() return str(response).strip().upper()
except Exception as e: except Exception as e:
@ -91,35 +100,85 @@ class DecisionEngine:
return "FACT_WHAT" return "FACT_WHAT"
async def _execute_parallel_streams(self, strategy: Dict, query: str) -> Dict[str, str]: 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", []) stream_keys = strategy.get("use_streams", [])
library = self.config.get("streams_library", {}) library = self.config.get("streams_library", {})
tasks = [] # Phase 1: Retrieval Tasks starten
retrieval_tasks = []
active_streams = [] active_streams = []
for key in stream_keys: for key in stream_keys:
stream_cfg = library.get(key) stream_cfg = library.get(key)
if stream_cfg: if stream_cfg:
active_streams.append(key) 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 = {} # Phase 2: Formatierung und optionale Kompression
for name, res in zip(active_streams, results): final_stream_tasks = []
for name, res in zip(active_streams, retrieval_results):
if isinstance(res, Exception): if isinstance(res, Exception):
logger.error(f"Stream '{name}' failed: {res}") logger.error(f"Stream '{name}' failed during retrieval: {res}")
mapped_results[name] = "[Fehler beim Abruf dieses Wissens-Streams]" async def _err(): return "[Fehler beim Abruf dieses Wissens-Streams]"
else: final_stream_tasks.append(_err())
mapped_results[name] = self._format_stream_context(res) 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: async def _run_single_stream(self, name: str, cfg: Dict, query: str) -> QueryResponse:
""" """Spezialisierte Graph-Suche mit Stream-Tracing (WP-25)."""
Bereitet eine spezialisierte Suche vor.
WP-25: Taggt die Treffer mit ihrem Ursprungs-Stream.
"""
transformed_query = cfg.get("query_template", "{query}").format(query=query) transformed_query = cfg.get("query_template", "{query}").format(query=query)
request = QueryRequest( request = QueryRequest(
@ -131,18 +190,16 @@ class DecisionEngine:
explain=True explain=True
) )
# Retrieval ausführen
response = await self.retriever.search(request) response = await self.retriever.search(request)
# WP-25: STREAM-TRACING # WP-25: STREAM-TRACING
# Markiere jeden Treffer mit dem Namen des Quell-Streams
for hit in response.results: for hit in response.results:
hit.stream_origin = name hit.stream_origin = name
return response return response
def _format_stream_context(self, response: QueryResponse) -> str: 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: if not response.results:
return "Keine spezifischen Informationen in diesem Stream gefunden." return "Keine spezifischen Informationen in diesem Stream gefunden."
@ -161,12 +218,13 @@ class DecisionEngine:
query: str, query: str,
stream_results: Dict[str, str] stream_results: Dict[str, str]
) -> str: ) -> str:
"""Führt die Synthese durch.""" """Führt die finale Synthese basierend auf dem Strategie-Profil durch."""
provider = strategy.get("preferred_provider") or self.settings.MINDNET_LLM_PROVIDER # WP-25a: Nutzt das llm_profile der Strategie
profile = strategy.get("llm_profile")
template_key = strategy.get("prompt_template", "rag_template") template_key = strategy.get("prompt_template", "rag_template")
template = self.llm_service.get_prompt(template_key, provider=provider) template = self.llm_service.get_prompt(template_key)
system_prompt = self.llm_service.get_prompt("system_prompt", provider=provider) system_prompt = self.llm_service.get_prompt("system_prompt")
# WP-25 ROBUSTNESS: Pre-Initialization # WP-25 ROBUSTNESS: Pre-Initialization
all_possible_streams = ["values_stream", "facts_stream", "biography_stream", "risk_stream", "tech_stream"] all_possible_streams = ["values_stream", "facts_stream", "biography_stream", "risk_stream", "tech_stream"]
@ -181,23 +239,21 @@ class DecisionEngine:
if prepend: if prepend:
final_prompt = f"{prepend}\n\n{final_prompt}" 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( response = await self.llm_service.generate_raw_response(
final_prompt, system=system_prompt, provider=provider, priority="realtime" final_prompt, system=system_prompt, profile_name=profile, 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"
)
return response return response
except KeyError as e: except KeyError as e:
logger.error(f"Template Variable mismatch in '{template_key}': Missing {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]) 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( return await self.llm_service.generate_raw_response(
f"Beantworte: {query}\n\nKontext:\n{fallback_context}", 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: except Exception as e:
logger.error(f"Final Synthesis failed: {e}") logger.error(f"Final Synthesis failed: {e}")

View File

@ -1,8 +1,9 @@
""" """
FILE: app/main.py 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. 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 STATUS: Active
DEPENDENCIES: app.config, app.routers.*, app.services.llm_service DEPENDENCIES: app.config, app.routers.*, app.services.llm_service
""" """
@ -32,63 +33,74 @@ except Exception:
from .core.logging_setup import setup_logging from .core.logging_setup import setup_logging
# Initialisierung noch VOR create_app() # Initialisierung des Loggings noch VOR create_app()
setup_logging() setup_logging()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# --- WP-25: Lifespan Management --- # --- WP-25a: Lifespan Management mit MoE Integritäts-Prüfung ---
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
""" """
Verwaltet den Lebenszyklus der Anwendung. Verwaltet den Lebenszyklus der Anwendung (Startup/Shutdown).
Führt Startup-Prüfungen durch und bereinigt Ressourcen beim Shutdown. Verifiziert die Verfügbarkeit der MoE-Experten-Profile und Strategien.
""" """
settings = get_settings() 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 # 1. Startup: Integritäts-Check der MoE Konfiguration
# Wir prüfen, ob die für die DecisionEngine kritischen Dateien vorhanden sind. # Wir prüfen die drei Säulen der Agentic-RAG Architektur.
decision_cfg = os.getenv("MINDNET_DECISION_CONFIG", "config/decision_engine.yaml") 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 prompts_cfg = settings.PROMPTS_PATH
if not os.path.exists(decision_cfg): missing_files = []
logger.error(f"❌ CRITICAL: Decision Engine config missing at {decision_cfg}") if not os.path.exists(decision_cfg): missing_files.append(decision_cfg)
if not os.path.exists(prompts_cfg): if not os.path.exists(profiles_cfg): missing_files.append(profiles_cfg)
logger.error(f"❌ CRITICAL: Prompts config missing at {prompts_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 yield
# 2. Shutdown: Ressourcen bereinigen # 2. Shutdown: Ressourcen bereinigen
logger.info("🛑 mindnet API: Shutting down...") logger.info("🛑 mindnet API: Shutting down...")
llm = LLMService() try:
await llm.close() llm = LLMService()
logger.info("✨ Cleanup complete. Goodbye.") await llm.close()
logger.info("✨ LLM resources cleaned up.")
except Exception as e:
logger.warning(f"⚠️ Error during LLMService cleanup: {e}")
logger.info("Goodbye.")
# --- App Factory --- # --- App Factory ---
def create_app() -> FastAPI: def create_app() -> FastAPI:
"""Initialisiert die FastAPI App mit WP-25 Erweiterungen.""" """Initialisiert die FastAPI App mit WP-25a Erweiterungen."""
app = FastAPI( app = FastAPI(
title="mindnet API", title="mindnet API",
version="1.0.0", # WP-25 Milestone version="1.1.0", # WP-25a Milestone
lifespan=lifespan, 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() s = get_settings()
# --- Globale Fehlerbehandlung (WP-25 Resilienz) --- # --- Globale Fehlerbehandlung (WP-25a Resilienz) ---
@app.exception_handler(Exception) @app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: 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) logger.error(f"❌ Unhandled Engine Error: {exc}", exc_info=True)
return JSONResponse( return JSONResponse(
status_code=500, status_code=500,
content={ 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__ "error_type": type(exc).__name__
} }
) )
@ -96,12 +108,13 @@ def create_app() -> FastAPI:
# Healthcheck # Healthcheck
@app.get("/healthz") @app.get("/healthz")
def healthz(): def healthz():
"""Bietet Statusinformationen über die Engine und Datenbank-Verbindung."""
return { return {
"status": "ok", "status": "ok",
"version": "1.0.0", "version": "1.1.0",
"qdrant": s.QDRANT_URL, "qdrant": s.QDRANT_URL,
"prefix": s.COLLECTION_PREFIX, "prefix": s.COLLECTION_PREFIX,
"agentic_mode": True "moe_enabled": True
} }
# Inkludieren der Router (100% Kompatibilität erhalten) # 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(graph_router, prefix="/graph", tags=["graph"])
app.include_router(tools_router, prefix="/tools", tags=["tools"]) app.include_router(tools_router, prefix="/tools", tags=["tools"])
app.include_router(feedback_router, prefix="/feedback", tags=["feedback"]) 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"]) app.include_router(ingest_router, prefix="/ingest", tags=["ingest"])
if admin_router: if admin_router:

View File

@ -1,15 +1,15 @@
""" """
FILE: app/routers/chat.py 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 Kombiniert die spezialisierte Interview-Logik und Keyword-Erkennung
mit der neuen Multi-Stream Orchestrierung der DecisionEngine. mit der neuen MoE-Orchestrierung und Pre-Synthesis Kompression.
VERSION: 3.0.2 VERSION: 3.0.4 (WP-25a: Optimized MoE & Cascade Delegation)
STATUS: Active STATUS: Active
FIX: FIX:
- 100% Wiederherstellung der v2.7.8 Logik (Interview, Schema-Resolution, Keywords). - WP-25a: Delegation der Fallback-Kaskade an den LLMService (v3.5.2).
- Integration der DecisionEngine für paralleles RAG-Retrieval. - WP-25a: Nutzung der zentralisierten Stream-Kompression der DecisionEngine (v1.2.1).
- Erhalt der Ollama Context-Throttling Parameter (WP-20). - WP-25a: Konsistente Nutzung von MoE-Profilen für Interview- und RAG-Modus.
- Beibehaltung der No-Retry Logik (max_retries=0) für Chat-Stabilität. - 100% Erhalt der v3.0.2 Logik (Interview, Schema-Resolution, FastPaths).
""" """
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
@ -19,6 +19,7 @@ import uuid
import logging import logging
import yaml import yaml
import os import os
import asyncio
from pathlib import Path from pathlib import Path
from app.config import get_settings from app.config import get_settings
@ -29,13 +30,13 @@ from app.services.feedback_service import log_search
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__) 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 _DECISION_CONFIG_CACHE = None
_TYPES_CONFIG_CACHE = None _TYPES_CONFIG_CACHE = None
def _load_decision_config() -> Dict[str, Any]: 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() settings = get_settings()
path = Path(settings.DECISION_CONFIG_PATH) path = Path(settings.DECISION_CONFIG_PATH)
try: try:
@ -47,7 +48,7 @@ def _load_decision_config() -> Dict[str, Any]:
return {"strategies": {}} return {"strategies": {}}
def _load_types_config() -> Dict[str, Any]: 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") path = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
try: try:
if os.path.exists(path): if os.path.exists(path):
@ -77,10 +78,7 @@ def get_decision_strategy(intent: str) -> Dict[str, Any]:
# --- EBENE 2: SPEZIAL-LOGIK (INTERVIEW & DETECTION) --- # --- EBENE 2: SPEZIAL-LOGIK (INTERVIEW & DETECTION) ---
def _detect_target_type(message: str, configured_schemas: Dict[str, Any]) -> str: def _detect_target_type(message: str, configured_schemas: Dict[str, Any]) -> str:
""" """WP-07: Identifiziert den gewünschten Notiz-Typ (Keyword-basiert)."""
WP-07: Identifiziert den gewünschten Notiz-Typ (Keyword-basiert).
100% identisch mit v2.7.8 zur Sicherstellung des Interview-Workflows.
"""
message_lower = message.lower() message_lower = message.lower()
types_cfg = get_types_config() types_cfg = get_types_config()
types_def = types_cfg.get("types", {}) types_def = types_cfg.get("types", {})
@ -110,17 +108,14 @@ def _detect_target_type(message: str, configured_schemas: Dict[str, Any]) -> str
return "default" return "default"
def _is_question(query: str) -> bool: 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() q = query.strip().lower()
if "?" in q: return True if "?" in q: return True
starters = ["wer", "wie", "was", "wo", "wann", "warum", "weshalb", "wozu", "welche", "bist du"] starters = ["wer", "wie", "was", "wo", "wann", "warum", "weshalb", "wozu", "welche", "bist du"]
return any(q.startswith(s + " ") for s in starters) return any(q.startswith(s + " ") for s in starters)
async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]: async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]:
""" """Hybrid Router: Keyword-Fast-Paths & DecisionEngine LLM Router."""
WP-25 Hybrid Router:
Nutzt erst Keyword-Fast-Paths (Router) und delegiert dann an die DecisionEngine.
"""
config = get_full_config() config = get_full_config()
strategies = config.get("strategies", {}) strategies = config.get("strategies", {})
query_lower = query.lower() 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: if kw.lower() in query_lower:
return "INTERVIEW", "Keyword (Interview)" 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) intent = await llm.decision_engine._determine_strategy(query)
return intent, "DecisionEngine (LLM)" return intent, "DecisionEngine (LLM)"
# --- EBENE 3: RETRIEVAL AGGREGATION --- # --- EBENE 3: RETRIEVAL AGGREGATION ---
def _collect_all_hits(stream_responses: Dict[str, Any]) -> List[QueryHit]: 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 = [] all_hits = []
seen_node_ids = set() seen_node_ids = set()
for _, response in stream_responses.items(): for _, response in stream_responses.items():
# In v3.0.4 sammeln wir die hits aus den QueryResponse Objekten
if hasattr(response, 'results'): if hasattr(response, 'results'):
for hit in response.results: for hit in response.results:
if hit.node_id not in seen_node_ids: if hit.node_id not in seen_node_ids:
@ -171,7 +167,7 @@ async def chat_endpoint(
start_time = time.time() start_time = time.time()
query_id = str(uuid.uuid4()) query_id = str(uuid.uuid4())
settings = get_settings() 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: try:
# 1. Intent Detection # 1. Intent Detection
@ -184,13 +180,14 @@ async def chat_endpoint(
sources_hits = [] sources_hits = []
answer_text = "" answer_text = ""
# 2. INTERVIEW MODE (Kompatibilität zu v2.7.8) # 2. INTERVIEW MODE (Bitgenaue WP-07 Logik)
if intent == "INTERVIEW": if intent == "INTERVIEW":
target_type = _detect_target_type(request.message, strategy.get("schemas", {})) target_type = _detect_target_type(request.message, strategy.get("schemas", {}))
types_cfg = get_types_config() types_cfg = get_types_config()
type_def = types_cfg.get("types", {}).get(target_type, {}) type_def = types_cfg.get("types", {}).get(target_type, {})
fields_list = type_def.get("schema", []) fields_list = type_def.get("schema", [])
# WP-07: Restaurierte Fallback Logik
if not fields_list: if not fields_list:
configured_schemas = strategy.get("schemas", {}) configured_schemas = strategy.get("schemas", {})
fallback = configured_schemas.get(target_type, configured_schemas.get("default", {})) fallback = configured_schemas.get(target_type, configured_schemas.get("default", {}))
@ -203,56 +200,50 @@ async def chat_endpoint(
.replace("{target_type}", target_type) \ .replace("{target_type}", target_type) \
.replace("{schema_fields}", fields_str) .replace("{schema_fields}", fields_str)
# WP-25a: MoE Call (Kaskade erfolgt intern im LLMService)
answer_text = await llm.generate_raw_response( answer_text = await llm.generate_raw_response(
final_prompt, system=llm.get_prompt("system_prompt"), 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 = [] sources_hits = []
# 3. RAG MODE (WP-25 Multi-Stream) # 3. RAG MODE (Optimierte MoE Orchestrierung)
else: 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", []) stream_keys = strategy.get("use_streams", [])
library = engine.config.get("streams_library", {}) library = engine.config.get("streams_library", {})
tasks = [] retrieval_tasks = []
active_streams = [] active_streams = []
for key in stream_keys: for key in stream_keys:
stream_cfg = library.get(key) if key in library:
if stream_cfg:
active_streams.append(key) active_streams.append(key)
tasks.append(engine._run_single_stream(key, stream_cfg, request.message)) retrieval_tasks.append(engine._run_single_stream(key, library[key], 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
responses = await asyncio.gather(*retrieval_tasks, return_exceptions=True)
for name, res in zip(active_streams, responses): for name, res in zip(active_streams, responses):
if not isinstance(res, Exception): if not isinstance(res, Exception):
raw_stream_map[name] = res raw_stream_map[name] = res
context_text = engine._format_stream_context(res)
# WP-20 Stability Fix: Throttling sources_hits = _collect_all_hits(raw_stream_map)
if provider == "ollama" and len(context_text) > max_chars:
context_text = context_text[:max_chars] + "\n[...]"
formatted_context_map[name] = context_text
# Phase C: Finale MoE Synthese
answer_text = await engine._generate_final_answer( answer_text = await engine._generate_final_answer(
intent, strategy, request.message, formatted_context_map intent, strategy, request.message, formatted_context_map
) )
sources_hits = _collect_all_hits(raw_stream_map)
duration_ms = int((time.time() - start_time) * 1000) duration_ms = int((time.time() - start_time) * 1000)
# Logging # Logging (WP-15)
try: try:
log_search( log_search(
query_id=query_id, query_text=request.message, results=sources_hits, 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 except: pass
@ -263,4 +254,4 @@ async def chat_endpoint(
except Exception as e: except Exception as e:
logger.error(f"❌ Chat Endpoint Failure: {e}", exc_info=True) 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 FILE: app/services/embeddings_client.py
DESCRIPTION: Unified Embedding Client. Nutzt Ollama API (HTTP). Ersetzt lokale sentence-transformers. DESCRIPTION: Unified Embedding Client. Nutzt MoE-Profile zur Modellsteuerung.
VERSION: 2.5.0 WP-25a: Integration der llm_profiles.yaml für konsistente Vektoren.
VERSION: 2.6.0 (WP-25a: MoE & Profile Support)
STATUS: Active STATUS: Active
DEPENDENCIES: httpx, requests, app.config DEPENDENCIES: httpx, requests, app.config, yaml
LAST_ANALYSIS: 2025-12-15
""" """
from __future__ import annotations from __future__ import annotations
import os import os
import logging import logging
import httpx import httpx
import requests # Für den synchronen Fallback import requests
from typing import List import yaml
from pathlib import Path
from typing import List, Dict, Any
from app.config import get_settings from app.config import get_settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class EmbeddingsClient: 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): def __init__(self):
self.settings = get_settings() self.settings = get_settings()
self.base_url = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434")
self.model = os.getenv("MINDNET_EMBEDDING_MODEL") # 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")
if not self.model: if not self.model:
self.model = os.getenv("MINDNET_LLM_MODEL", "phi3:mini") 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]: async def embed_query(self, text: str) -> List[float]:
"""Erzeugt einen Vektor für eine Suchanfrage."""
return await self._request_embedding(text) return await self._request_embedding(text)
async def embed_documents(self, texts: List[str]) -> List[List[float]]: async def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""Erzeugt Vektoren für einen Batch von Dokumenten."""
vectors = [] 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: async with httpx.AsyncClient(timeout=120.0) as client:
for text in texts: for text in texts:
vec = await self._request_embedding_with_client(client, text) vec = await self._request_embedding_with_client(client, text)
@ -42,18 +76,23 @@ class EmbeddingsClient:
return vectors return vectors
async def _request_embedding(self, text: str) -> List[float]: async def _request_embedding(self, text: str) -> List[float]:
"""Interner Request-Handler für Einzelabfragen."""
async with httpx.AsyncClient(timeout=30.0) as client: async with httpx.AsyncClient(timeout=30.0) as client:
return await self._request_embedding_with_client(client, text) return await self._request_embedding_with_client(client, text)
async def _request_embedding_with_client(self, client: httpx.AsyncClient, text: str) -> List[float]: 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" url = f"{self.base_url}/api/embeddings"
try: try:
# WP-25: Aktuell optimiert für Ollama-API Struktur
response = await client.post(url, json={"model": self.model, "prompt": text}) response = await client.post(url, json={"model": self.model, "prompt": text})
response.raise_for_status() response.raise_for_status()
return response.json().get("embedding", []) return response.json().get("embedding", [])
except Exception as e: except Exception as e:
logger.error(f"Async embedding failed: {e}") logger.error(f"Async embedding failed (Model: {self.model}): {e}")
return [] return []
# ============================================================================== # ==============================================================================
@ -62,27 +101,38 @@ class EmbeddingsClient:
def embed_text(text: str) -> List[float]: def embed_text(text: str) -> List[float]:
""" """
LEGACY/SYNC: Nutzt jetzt ebenfalls OLLAMA via 'requests'. LEGACY/SYNC: Nutzt ebenfalls die Profil-Logik für Konsistenz.
Ersetzt SentenceTransformers, um Dimensionskonflikte (768 vs 384) zu lösen. Ersetzt lokale sentence-transformers zur Vermeidung von Dimensionskonflikten.
""" """
if not text or not text.strip(): if not text or not text.strip():
return [] return []
base_url = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434") settings = get_settings()
model = os.getenv("MINDNET_EMBEDDING_MODEL")
# 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: if not model:
model = os.getenv("MINDNET_LLM_MODEL", "phi3:mini") model = os.getenv("MINDNET_LLM_MODEL", "phi3:mini")
url = f"{base_url}/api/embeddings" url = f"{base_url}/api/embeddings"
try: try:
# Synchroner Request (blockierend) # Synchroner Request via requests
response = requests.post(url, json={"model": model, "prompt": text}, timeout=30) response = requests.post(url, json={"model": model, "prompt": text}, timeout=30)
response.raise_for_status() response.raise_for_status()
data = response.json() return response.json().get("embedding", [])
return data.get("embedding", [])
except Exception as e: except Exception as e:
logger.error(f"Sync embedding (Ollama) failed: {e}") logger.error(f"Sync embedding failed (Model: {model}): {e}")
return [] return []

View File

@ -1,16 +1,14 @@
""" """
FILE: app/services/llm_service.py FILE: app/services/llm_service.py
DESCRIPTION: Hybrid-Client für Ollama, Google GenAI (Gemini) und OpenRouter. DESCRIPTION: Hybrid-Client für Ollama, Google GenAI (Gemini) und OpenRouter.
Verwaltet provider-spezifische Prompts und Background-Last. WP-25a: Implementierung der Mixture of Experts (MoE) Kaskaden-Steuerung.
WP-20: Optimiertes Fallback-Management zum Schutz von Cloud-Quoten. VERSION: 3.5.2 (WP-25a: MoE & Fallback Cascade Support)
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)
STATUS: Active STATUS: Active
FIX: FIX:
- Ingest-Stability: Entfernung des <5-Zeichen Guards (ermöglicht YES/NO Validierungen). - WP-25a: Implementierung der rekursiven Fallback-Kaskade via fallback_profile.
- OpenRouter-Fix: Sicherung gegen leere 'choices' zur Vermeidung von JSON-Errors. - WP-25a: Schutz gegen zirkuläre Profil-Referenzen (visited_profiles).
- Erhalt der vollständigen v3.3.9 Logik für Rate-Limits, Retries und Background-Tasks. - WP-25a: Erweitertes Logging für Tracing der Experten-Entscheidungen.
- Erhalt der Ingest-Stability (WP-25) und des Rate-Limit-Managements.
""" """
import httpx import httpx
import yaml import yaml
@ -19,28 +17,28 @@ import asyncio
import json import json
from google import genai from google import genai
from google.genai import types from google.genai import types
from openai import AsyncOpenAI # Für OpenRouter (OpenAI-kompatibel) from openai import AsyncOpenAI
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any, Literal from typing import Optional, Dict, Any, Literal
from app.config import get_settings 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 from app.core.registry import clean_llm_text
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class LLMService: class LLMService:
# GLOBALER SEMAPHOR für Hintergrund-Last Steuerung (WP-06)
_background_semaphore = None _background_semaphore = None
def __init__(self): def __init__(self):
self.settings = get_settings() self.settings = get_settings()
self.prompts = self._load_prompts() 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 self._decision_engine = None
# Initialisiere Semaphore einmalig auf Klassen-Ebene
if LLMService._background_semaphore is None: if LLMService._background_semaphore is None:
limit = getattr(self.settings, "BACKGROUND_LIMIT", 2) limit = getattr(self.settings, "BACKGROUND_LIMIT", 2)
logger.info(f"🚦 LLMService: Initializing Background Semaphore with limit: {limit}") logger.info(f"🚦 LLMService: Initializing Background Semaphore with limit: {limit}")
@ -52,10 +50,9 @@ class LLMService:
timeout=httpx.Timeout(self.settings.LLM_TIMEOUT) timeout=httpx.Timeout(self.settings.LLM_TIMEOUT)
) )
# 2. Google GenAI Client (Modern SDK) # 2. Google GenAI Client
self.google_client = None self.google_client = None
if self.settings.GOOGLE_API_KEY: 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( self.google_client = genai.Client(
api_key=self.settings.GOOGLE_API_KEY, api_key=self.settings.GOOGLE_API_KEY,
http_options={'api_version': 'v1'} http_options={'api_version': 'v1'}
@ -68,24 +65,20 @@ class LLMService:
self.openrouter_client = AsyncOpenAI( self.openrouter_client = AsyncOpenAI(
base_url="https://openrouter.ai/api/v1", base_url="https://openrouter.ai/api/v1",
api_key=self.settings.OPENROUTER_API_KEY, api_key=self.settings.OPENROUTER_API_KEY,
# Strikter Timeout für OpenRouter Free-Tier zur Vermeidung von Hangs.
timeout=45.0 timeout=45.0
) )
logger.info("🛰️ LLMService: OpenRouter Integration active.") logger.info("🛰️ LLMService: OpenRouter Integration active.")
@property @property
def decision_engine(self): def decision_engine(self):
"""Lazy Initialization der Decision Engine (WP-25)."""
if self._decision_engine is None: if self._decision_engine is None:
from app.core.retrieval.decision_engine import DecisionEngine from app.core.retrieval.decision_engine import DecisionEngine
self._decision_engine = DecisionEngine() self._decision_engine = DecisionEngine()
return self._decision_engine return self._decision_engine
def _load_prompts(self) -> dict: def _load_prompts(self) -> dict:
"""Lädt die Prompt-Konfiguration aus der YAML-Datei."""
path = Path(self.settings.PROMPTS_PATH) path = Path(self.settings.PROMPTS_PATH)
if not path.exists(): if not path.exists():
logger.error(f"❌ Prompts file not found at {path}")
return {} return {}
try: try:
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
@ -94,21 +87,27 @@ class LLMService:
logger.error(f"❌ Failed to load prompts: {e}") logger.error(f"❌ Failed to load prompts: {e}")
return {} 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: 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 active_provider = provider or self.settings.MINDNET_LLM_PROVIDER
data = self.prompts.get(key, "") data = self.prompts.get(key, "")
if isinstance(data, dict): if isinstance(data, dict):
val = data.get(active_provider, data.get("gemini", data.get("ollama", ""))) 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(val)
return str(data) return str(data)
async def generate_raw_response( async def generate_raw_response(
@ -123,35 +122,83 @@ class LLMService:
model_override: Optional[str] = None, model_override: Optional[str] = None,
json_schema: Optional[Dict[str, Any]] = None, json_schema: Optional[Dict[str, Any]] = None,
json_schema_name: str = "mindnet_json", 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: ) -> str:
""" """
Haupteinstiegspunkt für LLM-Anfragen. Haupteinstiegspunkt für LLM-Anfragen mit rekursiver Kaskaden-Logik.
WP-25 FIX: Schwellenwert entfernt, um kurze Ingest-Validierungen (YES/NO) zu unterstützen.
""" """
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
if priority == "background": # 1. Profil-Auflösung
async with LLMService._background_semaphore: 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, target_model,
json_schema, json_schema_name, strict_json_schema, target_temp
)
else:
res = await self._dispatch( res = await self._dispatch(
target_provider, prompt, system, force_json, target_provider, prompt, system, force_json,
max_retries, base_delay, model_override, max_retries, base_delay, target_model,
json_schema, json_schema_name, strict_json_schema 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
)
# 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": if not res and target_provider != "ollama":
logger.warning(f"⚠️ [WP-25] Empty response from {target_provider}. Falling back to OLLAMA.") logger.warning(f"⚠️ Empty response from {target_provider}. Triggering fallback chain.")
res = await self._execute_ollama(prompt, system, force_json, max_retries, base_delay) 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
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( async def _dispatch(
self, self,
@ -164,9 +211,10 @@ class LLMService:
model_override: Optional[str], model_override: Optional[str],
json_schema: Optional[Dict[str, Any]], json_schema: Optional[Dict[str, Any]],
json_schema_name: str, json_schema_name: str,
strict_json_schema: bool strict_json_schema: bool,
temperature: Optional[float] = None
) -> str: ) -> str:
"""Routet die Anfrage mit intelligenter Rate-Limit Erkennung.""" """Routet die Anfrage an den spezifischen Provider-Executor."""
rate_limit_attempts = 0 rate_limit_attempts = 0
max_rate_retries = min(max_retries, getattr(self.settings, "LLM_RATE_LIMIT_RETRIES", 3)) 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) wait_time = getattr(self.settings, "LLM_RATE_LIMIT_WAIT", 60.0)
@ -175,43 +223,41 @@ class LLMService:
try: try:
if provider == "openrouter" and self.openrouter_client: if provider == "openrouter" and self.openrouter_client:
return await self._execute_openrouter( return await self._execute_openrouter(
prompt=prompt, prompt=prompt, system=system, force_json=force_json,
system=system, model_override=model_override, json_schema=json_schema,
force_json=force_json, json_schema_name=json_schema_name, strict_json_schema=strict_json_schema,
model_override=model_override, temperature=temperature
json_schema=json_schema,
json_schema_name=json_schema_name,
strict_json_schema=strict_json_schema
) )
if provider == "gemini" and self.google_client: 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: except Exception as e:
err_str = str(e) err_str = str(e)
is_rate_limit = any(x in err_str for x in ["429", "RESOURCE_EXHAUSTED", "rate_limited", "Too Many Requests"]) # Rate-Limit Handling (429)
if any(x in err_str for x in ["429", "RESOURCE_EXHAUSTED", "rate_limited"]):
if is_rate_limit and rate_limit_attempts < max_rate_retries:
rate_limit_attempts += 1 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) await asyncio.sleep(wait_time)
continue continue
# Andere Fehler werden an generate_raw_response für die Kaskade gereicht
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)
raise e 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 model = model_override or self.settings.GEMINI_MODEL
clean_model = model.replace("models/", "") clean_model = model.replace("models/", "")
config = types.GenerateContentConfig( config_kwargs = {
system_instruction=system, "system_instruction": system,
response_mime_type="application/json" if force_json else "text/plain" "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( response = await asyncio.wait_for(
asyncio.to_thread( asyncio.to_thread(
self.google_client.models.generate_content, self.google_client.models.generate_content,
@ -222,53 +268,48 @@ class LLMService:
return response.text.strip() return response.text.strip()
async def _execute_openrouter( async def _execute_openrouter(
self, self, prompt: str, system: Optional[str], force_json: bool,
prompt: str, model_override: Optional[str], json_schema: Optional[Dict[str, Any]] = None,
system: Optional[str], json_schema_name: str = "mindnet_json", strict_json_schema: bool = True,
force_json: bool, temperature: Optional[float] = None
model_override: Optional[str],
json_schema: Optional[Dict[str, Any]] = None,
json_schema_name: str = "mindnet_json",
strict_json_schema: bool = True
) -> str: ) -> str:
"""OpenRouter API Integration. WP-25 FIX: Sicherung gegen leere 'choices'."""
model = model_override or self.settings.OPENROUTER_MODEL model = model_override or self.settings.OPENROUTER_MODEL
logger.info(f"🛰️ OpenRouter Call: Model='{model}' | Temp={temperature}")
messages = [] messages = []
if system: if system: messages.append({"role": "system", "content": system})
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt}) messages.append({"role": "user", "content": prompt})
kwargs: Dict[str, Any] = {} kwargs: Dict[str, Any] = {}
if temperature is not None:
kwargs["temperature"] = temperature
if force_json: if force_json:
if json_schema: if json_schema:
kwargs["response_format"] = { kwargs["response_format"] = {
"type": "json_schema", "type": "json_schema",
"json_schema": { "json_schema": {"name": json_schema_name, "strict": strict_json_schema, "schema": json_schema}
"name": json_schema_name, "strict": strict_json_schema, "schema": json_schema
}
} }
else: else:
kwargs["response_format"] = {"type": "json_object"} kwargs["response_format"] = {"type": "json_object"}
response = await self.openrouter_client.chat.completions.create( response = await self.openrouter_client.chat.completions.create(
model=model, model=model, messages=messages, **kwargs
messages=messages,
**kwargs
) )
# WP-25 FIX: Sicherung gegen leere Antwort-Arrays if not response.choices:
if not response.choices or len(response.choices) == 0:
logger.warning(f"🛰️ OpenRouter returned no choices for model {model}")
return "" return ""
return response.choices[0].message.content.strip() if response.choices[0].message.content else "" 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 = { payload = {
"model": self.settings.LLM_MODEL, "model": self.settings.LLM_MODEL,
"prompt": prompt, "prompt": prompt,
"stream": False, "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 force_json: payload["format"] = "json"
if system: payload["system"] = system if system: payload["system"] = system
@ -282,14 +323,12 @@ class LLMService:
except Exception as e: except Exception as e:
attempt += 1 attempt += 1
if attempt > max_retries: if attempt > max_retries:
logger.error(f"❌ Ollama request failed: {e}") logger.error(f"❌ Ollama final failure after {attempt} attempts: {e}")
raise e raise e
wait_time = base_delay * (2 ** (attempt - 1)) await asyncio.sleep(base_delay * (2 ** (attempt - 1)))
await asyncio.sleep(wait_time)
async def generate_rag_response(self, query: str, context_str: Optional[str] = None) -> str: async def generate_rag_response(self, query: str, context_str: Optional[str] = None) -> str:
"""WP-25: Orchestrierung via DecisionEngine.""" """WP-25: Orchestrierung via DecisionEngine."""
logger.info(f"🚀 [WP-25] Chat Query: {query[:50]}...")
return await self.decision_engine.ask(query) return await self.decision_engine.ask(query)
async def close(self): async def close(self):

View File

@ -1,28 +1,33 @@
# config/decision_engine.yaml # 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 # STATUS: Active
# DoD: # DESCRIPTION: Zentrale Orchestrierung der Multi-Stream-Engine.
# - Strikte Nutzung der Typen aus types.yaml (v2.7.0). # FIX:
# - Fix für Projekt-Klassifizierung via Keyword-Fast-Path (Auflösung Kollision). # - Auslagerung der LLM-Profile in llm_profiles.yaml zur zentralen Wartbarkeit.
# - 100% Erhalt aller Stream-Parameter und Edge-Boosts. # - 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: settings:
llm_fallback_enabled: true 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" 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" 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) --- # --- EBENE 1: STREAM-LIBRARY (Bausteine basierend auf types.yaml v2.7.0) ---
# Synchronisiert mit types.yaml v2.7.0
streams_library: streams_library:
values_stream: values_stream:
name: "Identität & Ethik" 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}" query_template: "Welche meiner Werte und Prinzipien betreffen: {query}"
# Nur Typen aus types.yaml
filter_types: ["value", "principle", "belief", "trait", "boundary", "need", "motivation"] filter_types: ["value", "principle", "belief", "trait", "boundary", "need", "motivation"]
top_k: 5 top_k: 5
edge_boosts: edge_boosts:
@ -32,8 +37,10 @@ streams_library:
facts_stream: facts_stream:
name: "Operative Realität" name: "Operative Realität"
llm_profile: "synthesis_pro"
compression_profile: "compression_fast"
compression_threshold: 3500
query_template: "Status, Ressourcen und Fakten zu: {query}" query_template: "Status, Ressourcen und Fakten zu: {query}"
# Nur Typen aus types.yaml
filter_types: ["project", "decision", "task", "goal", "event", "state"] filter_types: ["project", "decision", "task", "goal", "event", "state"]
top_k: 5 top_k: 5
edge_boosts: edge_boosts:
@ -43,8 +50,10 @@ streams_library:
biography_stream: biography_stream:
name: "Persönliche Erfahrung" 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?" query_template: "Welche Erlebnisse habe ich im Kontext von {query} gemacht?"
# Nur Typen aus types.yaml
filter_types: ["experience", "journal", "profile", "person"] filter_types: ["experience", "journal", "profile", "person"]
top_k: 3 top_k: 3
edge_boosts: edge_boosts:
@ -53,8 +62,10 @@ streams_library:
risk_stream: risk_stream:
name: "Risiko-Radar" name: "Risiko-Radar"
llm_profile: "synthesis_pro"
compression_profile: "compression_fast"
compression_threshold: 2500
query_template: "Gefahren, Hindernisse oder Risiken bei: {query}" query_template: "Gefahren, Hindernisse oder Risiken bei: {query}"
# Nur Typen aus types.yaml
filter_types: ["risk", "obstacle", "bias"] filter_types: ["risk", "obstacle", "bias"]
top_k: 3 top_k: 3
edge_boosts: edge_boosts:
@ -64,81 +75,59 @@ streams_library:
tech_stream: tech_stream:
name: "Wissen & Technik" name: "Wissen & Technik"
llm_profile: "tech_expert"
compression_profile: "compression_fast"
compression_threshold: 4500
query_template: "Inhaltliche Details und Definitionen zu: {query}" query_template: "Inhaltliche Details und Definitionen zu: {query}"
# Nur Typen aus types.yaml
filter_types: ["concept", "source", "glossary", "idea", "insight", "skill", "habit"] filter_types: ["concept", "source", "glossary", "idea", "insight", "skill", "habit"]
top_k: 5 top_k: 5
edge_boosts: edge_boosts:
uses: 2.5 uses: 2.5
implemented_in: 3.0 implemented_in: 3.0
# --- EBENE 2: STRATEGIEN (Komposition & Routing) --- # --- EBENE 2: STRATEGIEN (Finale Komposition via MoE-Profile) ---
# Orchestriert das Zusammenspiel der Streams basierend auf dem Intent.
strategies: strategies:
# Spezialisierte Fact-Strategie für zeitliche Fragen
FACT_WHEN: FACT_WHEN:
description: "Abfrage von exakten Zeitpunkten und Terminen." description: "Abfrage von exakten Zeitpunkten und Terminen."
preferred_provider: "openrouter" llm_profile: "synthesis_pro"
# FAST PATH: Harte Keywords für zeitliche Fragen
trigger_keywords: ["wann", "datum", "uhrzeit", "zeitpunkt"] trigger_keywords: ["wann", "datum", "uhrzeit", "zeitpunkt"]
use_streams: use_streams: ["facts_stream", "biography_stream", "tech_stream"]
- "facts_stream"
- "biography_stream"
- "tech_stream"
prompt_template: "fact_synthesis_v1" prompt_template: "fact_synthesis_v1"
# Spezialisierte Fact-Strategie für inhaltliche Fragen & Listen
FACT_WHAT: FACT_WHAT:
description: "Abfrage von Definitionen, Listen und Inhalten." description: "Abfrage von Definitionen, Listen und Inhalten."
preferred_provider: "openrouter" llm_profile: "synthesis_pro"
# FIX v3.1.6: "projekt" entfernt, um Kollision mit DECISION ("Soll ich Projekt...") zu vermeiden.
trigger_keywords: ["was ist", "welche sind", "liste", "übersicht", "zusammenfassung"] trigger_keywords: ["was ist", "welche sind", "liste", "übersicht", "zusammenfassung"]
use_streams: use_streams: ["facts_stream", "tech_stream", "biography_stream"]
- "facts_stream"
- "tech_stream"
- "biography_stream"
prompt_template: "fact_synthesis_v1" prompt_template: "fact_synthesis_v1"
# Entscheidungs-Frage
DECISION: DECISION:
description: "Der User sucht Rat, Strategie oder Abwägung." description: "Der User sucht Rat, Strategie oder Abwägung."
preferred_provider: "gemini" llm_profile: "synthesis_pro"
# FIX v3.1.6: Trigger erweitert, um "Soll ich... Projekt..." sicher zu fangen.
trigger_keywords: ["soll ich", "sollte ich", "entscheidung", "abwägen", "priorität", "empfehlung"] trigger_keywords: ["soll ich", "sollte ich", "entscheidung", "abwägen", "priorität", "empfehlung"]
use_streams: use_streams: ["values_stream", "facts_stream", "risk_stream"]
- "values_stream"
- "facts_stream"
- "risk_stream"
prompt_template: "decision_synthesis_v1" prompt_template: "decision_synthesis_v1"
prepend_instruction: | prepend_instruction: |
!!! ENTSCHEIDUNGS-MODUS (AGENTIC MULTI-STREAM) !!! !!! ENTSCHEIDUNGS-MODUS (AGENTIC MULTI-STREAM) !!!
Analysiere die Fakten vor dem Hintergrund meiner Werte und evaluiere die Risiken. 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. Wäge ab, ob das Vorhaben mit meiner langfristigen Identität kompatibel ist.
# Emotionale Reflexion
EMPATHY: EMPATHY:
description: "Reaktion auf emotionale Zustände." description: "Reaktion auf emotionale Zustände."
preferred_provider: "openrouter" llm_profile: "synthesis_pro"
trigger_keywords: ["fühle", "traurig", "glücklich", "stress", "angst"] trigger_keywords: ["fühle", "traurig", "glücklich", "stress", "angst"]
use_streams: use_streams: ["biography_stream", "values_stream"]
- "biography_stream"
- "values_stream"
prompt_template: "empathy_template" prompt_template: "empathy_template"
# Technischer Support
CODING: CODING:
description: "Technische Anfragen und Programmierung." description: "Technische Anfragen und Programmierung."
preferred_provider: "gemini" llm_profile: "tech_expert"
trigger_keywords: ["code", "python", "script", "bug", "syntax"] trigger_keywords: ["code", "python", "script", "bug", "syntax"]
use_streams: use_streams: ["tech_stream", "facts_stream"]
- "tech_stream"
- "facts_stream"
prompt_template: "technical_template" prompt_template: "technical_template"
# Eingabe-Modus (WP-07)
INTERVIEW: INTERVIEW:
description: "Der User möchte Wissen erfassen (Eingabemodus)." description: "Der User möchte Wissen erfassen (Eingabemodus)."
preferred_provider: "openrouter" llm_profile: "compression_fast"
use_streams: [] use_streams: []
prompt_template: "interview_template" 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 doc_type: glossary
audience: all audience: all
status: active status: active
version: 2.9.3 version: 3.0.0
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." 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 # 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. * **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. * **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. * **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 doc_type: concept
audience: architect, product_owner audience: architect, product_owner
scope: ai, router, personas, resilience, agentic_rag scope: ai, router, personas, resilience, agentic_rag, moe
status: active status: active
version: 2.9.3 version: 3.0.0
context: "Fachkonzept der hybriden KI-Persönlichkeit, Agentic Multi-Stream RAG, Provider-Kaskade und kognitiven Resilienz (Deep Fallback)." 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 # 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. 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. * **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 doc_type: technical_reference
audience: developer, architect 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 status: active
version: 2.9.3 version: 3.0.0
context: "Technische Implementierung des FastAPI-Routers, des hybriden LLMService (v3.4.2), WP-25 Agentic Multi-Stream RAG und WP-20 Resilienz-Logik." 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 # 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. * Wenn unklar: Anfrage an `DecisionEngine._determine_strategy()` zur LLM-basierten Klassifizierung.
* Nutzt `intent_router_v1` Prompt aus `prompts.yaml`. * 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**: 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`). * **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. * **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. * **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: 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. * **Stream-Tracing:** Jeder Treffer wird mit `stream_origin` markiert für Feedback-Optimierung.
* **Fehlerbehandlung:** Einzelne Stream-Fehler blockieren nicht die gesamte Anfrage. * **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`: 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). * **Pre-Initialization:** Alle möglichen Stream-Variablen werden vorab initialisiert (verhindert KeyErrors).
* **Provider-spezifische Templates:** Separate Versionen für Ollama, Gemini und OpenRouter. * **Provider-spezifische Templates:** Separate Versionen für Ollama, Gemini und OpenRouter.
**Synthese-Strategien:** **Synthese-Strategien (Profil-gesteuert):**
* **FACT_WHAT/FACT_WHEN:** Kombiniert Fakten, Biographie und Technik. * **FACT_WHAT/FACT_WHEN:** Nutzt `synthesis_pro` - Kombiniert Fakten, Biographie und Technik.
* **DECISION:** Wägt Fakten gegen Werte ab, evaluiert Risiken. * **DECISION:** Nutzt `synthesis_pro` - Wägt Fakten gegen Werte ab, evaluiert Risiken.
* **EMPATHY:** Fokus auf Biographie und Werte. * **EMPATHY:** Nutzt `synthesis_pro` - Fokus auf Biographie und Werte.
* **CODING:** Technik und Fakten. * **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: 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:** 2. **Multi-Stream Retrieval:**
* Parallele Abfragen in spezialisierten Streams via `DecisionEngine._execute_parallel_streams()`. * Parallele Abfragen in spezialisierten Streams via `DecisionEngine._execute_parallel_streams()`.
* Jeder Stream nutzt individuelle Filter, Edge-Boosts und Query-Templates. * 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:** 3. **Context Formatting:**
* Stream-Ergebnisse werden in formatierte Kontext-Strings umgewandelt. * Stream-Ergebnisse werden in formatierte Kontext-Strings umgewandelt.
* **Ollama Context-Throttling:** Kontext wird auf `MAX_OLLAMA_CHARS` begrenzt (Standard: 10.000). * **Ollama Context-Throttling:** Kontext wird auf `MAX_OLLAMA_CHARS` begrenzt (Standard: 10.000).

View File

@ -1,10 +1,10 @@
--- ---
doc_type: technical_reference doc_type: technical_reference
audience: developer, admin audience: developer, admin
scope: configuration, env, registry, scoring, resilience, modularization, agentic_rag scope: configuration, env, registry, scoring, resilience, modularization, agentic_rag, moe
status: active status: active
version: 2.9.3 version: 3.0.0
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." 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 # Konfigurations-Referenz
@ -329,6 +329,80 @@ Alle möglichen Stream-Variablen werden vorab initialisiert (verhindert KeyError
**Provider-spezifische Templates:** **Provider-spezifische Templates:**
Separate Versionen für Ollama, Gemini und OpenRouter. 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 Auszug aus der decision_engine.yaml
```yaml ```yaml

View File

@ -1,10 +1,10 @@
--- ---
doc_type: technical_reference doc_type: technical_reference
audience: developer, devops audience: developer, devops
scope: backend, ingestion, smart_edges, edge_registry, modularization scope: backend, ingestion, smart_edges, edge_registry, modularization, moe
status: active status: active
version: 2.13.12 version: 2.14.0
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." 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 # 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. * 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. * Die Namensauflösung erfolgt nun über das modularisierte `database`-Paket.
10. **Chunking anwenden:** Zerlegung des Textes basierend auf dem ermittelten Profil (siehe Kap. 3). 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. * 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]). * **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. * **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. 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`). * Jede Kante wird via `EdgeRegistry` normalisiert (z.B. `basiert_auf` -> `based_on`).
* Unbekannte Typen werden in `unknown_edges.jsonl` protokolliert. * Unbekannte Typen werden in `unknown_edges.jsonl` protokolliert.
14. **Default- & Strukturkanten:** Anwendung der `edge_defaults` und Erzeugung von Systemkanten (`belongs_to`, `next`, `prev`). 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. 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. **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. **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 doc_type: operations_manual
audience: admin, devops audience: admin, devops
scope: deployment, maintenance, backup, edge_registry scope: deployment, maintenance, backup, edge_registry, moe
status: active status: active
version: 2.7.0 version: 3.0.0
context: "Installationsanleitung, Systemd-Units und Wartungsprozesse für Mindnet v2.7." context: "Installationsanleitung, Systemd-Units und Wartungsprozesse für Mindnet v3.0.0 inklusive WP-25a Mixture of Experts (MoE) Konfiguration."
--- ---
# Admin Operations Guide # 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). 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) ### 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 ```bash
# Modelle laden # Modelle laden
@ -57,6 +57,14 @@ ollama pull nomic-embed-text
curl http://localhost:11434/api/generate -d '{"model": "phi3:mini", "prompt":"Hi"}' 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) ## 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 edge_defaults: ["blocks"] # Automatische Kante
detection_keywords: ["gefahr", "risiko"] 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 ```yaml
DECISION: DECISION:
use_streams: ["values_stream", "facts_stream", "risk_stream"] # WP-25: Multi-Stream 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 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 ### Workflow B: Graph-Farben ändern
1. Öffne `app/frontend/ui_config.py`. 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`. 3. **Self-Learning Loop:** Protokollierung unbekannter Kanten in `unknown_edges.jsonl`.
### WP-25: Agentic Multi-Stream RAG Orchestration ### 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. **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 - decision_engine.yaml v3.1.6: Multi-Stream Konfiguration
- prompts.yaml v3.1.2: Stream-Templates - prompts.yaml v3.1.2: Stream-Templates
**Ausblick (WP-25a):** **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.
- Pre-Synthesis: LLM-basierte Komprimierung überlanger Streams
**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 - Kontext-Budgeting: Intelligente Token-Verteilung
- Stream-specific Provider: Unterschiedliche KI-Modelle pro Wissensbereich - 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.