Merge branch 'WP22'
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 3s

This commit is contained in:
Lars 2025-12-19 09:53:47 +01:00
commit 7f707cffb9
18 changed files with 1290 additions and 607 deletions

View File

@ -1,11 +1,13 @@
""" """
FILE: app/core/ingestion.py FILE: app/core/ingestion.py
DESCRIPTION: Haupt-Ingestion-Logik. DESCRIPTION: Haupt-Ingestion-Logik. Transformiert Markdown in den Graphen (Notes, Chunks, Edges).
FIX: Korrekte Priorisierung von Frontmatter für chunk_profile und retriever_weight. FIX: Korrekte Priorisierung von Frontmatter für chunk_profile und retriever_weight.
Lade Chunk-Config basierend auf dem effektiven Profil, nicht nur dem Notiz-Typ. Lade Chunk-Config basierend auf dem effektiven Profil, nicht nur dem Notiz-Typ.
VERSION: 2.7.0 (Fix: Frontmatter Overrides & Config Loading) WP-22: Integration von Content Lifecycle (Status Gate) und Edge Registry Validation.
WP-22: Multi-Hash Refresh für konsistente Change Detection.
VERSION: 2.8.6 (WP-22 Lifecycle & Registry)
STATUS: Active STATUS: Active
DEPENDENCIES: app.core.parser, app.core.note_payload, app.core.chunker, app.core.derive_edges, app.core.qdrant*, app.services.embeddings_client DEPENDENCIES: app.core.parser, app.core.note_payload, app.core.chunker, app.core.derive_edges, app.core.qdrant*, app.services.embeddings_client, app.services.edge_registry
EXTERNAL_CONFIG: config/types.yaml EXTERNAL_CONFIG: config/types.yaml
""" """
import os import os
@ -39,11 +41,13 @@ from app.core.qdrant_points import (
) )
from app.services.embeddings_client import EmbeddingsClient from app.services.embeddings_client import EmbeddingsClient
from app.services.edge_registry import registry as edge_registry
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# --- Helper --- # --- Helper ---
def load_type_registry(custom_path: Optional[str] = None) -> dict: def load_type_registry(custom_path: Optional[str] = None) -> dict:
"""Lädt die types.yaml zur Steuerung der typ-spezifischen Ingestion."""
import yaml import yaml
path = custom_path or os.getenv("MINDNET_TYPES_FILE", "config/types.yaml") path = custom_path or os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
if not os.path.exists(path): return {} if not os.path.exists(path): return {}
@ -52,14 +56,15 @@ def load_type_registry(custom_path: Optional[str] = None) -> dict:
except Exception: return {} except Exception: return {}
def resolve_note_type(requested: Optional[str], reg: dict) -> str: def resolve_note_type(requested: Optional[str], reg: dict) -> str:
"""Bestimmt den finalen Notiz-Typ (Fallback auf 'concept')."""
types = reg.get("types", {}) types = reg.get("types", {})
if requested and requested in types: return requested if requested and requested in types: return requested
return "concept" return "concept"
def effective_chunk_profile_name(fm: dict, note_type: str, reg: dict) -> str: def effective_chunk_profile_name(fm: dict, note_type: str, reg: dict) -> str:
""" """
Ermittelt den Namen des Chunk-Profils. Ermittelt den Namen des zu nutzenden Chunk-Profils.
Prio: 1. Frontmatter -> 2. Type-Config -> 3. Default Priorität: 1. Frontmatter Override -> 2. Type Config -> 3. Global Default
""" """
# 1. Frontmatter Override # 1. Frontmatter Override
override = fm.get("chunking_profile") or fm.get("chunk_profile") override = fm.get("chunking_profile") or fm.get("chunk_profile")
@ -77,8 +82,8 @@ def effective_chunk_profile_name(fm: dict, note_type: str, reg: dict) -> str:
def effective_retriever_weight(fm: dict, note_type: str, reg: dict) -> float: def effective_retriever_weight(fm: dict, note_type: str, reg: dict) -> float:
""" """
Ermittelt das Retriever Weight. Ermittelt das effektive retriever_weight für das Scoring.
Prio: 1. Frontmatter -> 2. Type-Config -> 3. Default Priorität: 1. Frontmatter Override -> 2. Type Config -> 3. Global Default
""" """
# 1. Frontmatter Override # 1. Frontmatter Override
override = fm.get("retriever_weight") override = fm.get("retriever_weight")
@ -107,7 +112,7 @@ class IngestionService:
self.registry = load_type_registry() self.registry = load_type_registry()
self.embedder = EmbeddingsClient() self.embedder = EmbeddingsClient()
# ACTIVE HASH MODE aus ENV lesen (Default: full) # Change Detection Modus (full oder body)
self.active_hash_mode = os.getenv("MINDNET_CHANGE_DETECTION_MODE", "full") self.active_hash_mode = os.getenv("MINDNET_CHANGE_DETECTION_MODE", "full")
try: try:
@ -117,20 +122,13 @@ class IngestionService:
logger.warning(f"DB init warning: {e}") logger.warning(f"DB init warning: {e}")
def _get_chunk_config_by_profile(self, profile_name: str, note_type: str) -> Dict[str, Any]: def _get_chunk_config_by_profile(self, profile_name: str, note_type: str) -> Dict[str, Any]:
""" """Holt die Chunker-Parameter (max, target, overlap) für ein spezifisches Profil."""
Lädt die konkrete Config (target, max, overlap) für einen Profilnamen.
"""
# Suche direkt in den definierten Profilen der Registry
profiles = self.registry.get("chunking_profiles", {}) profiles = self.registry.get("chunking_profiles", {})
if profile_name in profiles: if profile_name in profiles:
cfg = profiles[profile_name].copy() cfg = profiles[profile_name].copy()
# Tuple-Fix für Overlap (wie in chunker.py)
if "overlap" in cfg and isinstance(cfg["overlap"], list): if "overlap" in cfg and isinstance(cfg["overlap"], list):
cfg["overlap"] = tuple(cfg["overlap"]) cfg["overlap"] = tuple(cfg["overlap"])
return cfg return cfg
# Fallback: Wenn Profilname unbekannt, nutze Standard für den Typ via Chunker
logger.warning(f"Profile '{profile_name}' not found in registry. Falling back to type defaults.")
return get_chunk_config(note_type) return get_chunk_config(note_type)
async def process_file( async def process_file(
@ -144,7 +142,10 @@ class IngestionService:
hash_source: str = "parsed", hash_source: str = "parsed",
hash_normalize: str = "canonical" hash_normalize: str = "canonical"
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""
Verarbeitet eine Markdown-Datei und schreibt sie in den Graphen.
Folgt dem 14-Schritte-Workflow.
"""
result = {"path": file_path, "status": "skipped", "changed": False, "error": None} result = {"path": file_path, "status": "skipped", "changed": False, "error": None}
# 1. Parse & Frontmatter Validation # 1. Parse & Frontmatter Validation
@ -157,21 +158,25 @@ class IngestionService:
logger.error(f"Validation failed for {file_path}: {e}") logger.error(f"Validation failed for {file_path}: {e}")
return {**result, "error": f"Validation failed: {str(e)}"} return {**result, "error": f"Validation failed: {str(e)}"}
# 2. Type & Config Resolution (FIXED) # --- WP-22: Content Lifecycle Gate (Teil A) ---
# Wir ermitteln erst den Typ status = fm.get("status", "draft").lower().strip()
# Hard Skip für System- oder Archiv-Dateien
if status in ["system", "template", "archive", "hidden"]:
logger.info(f"Skipping file {file_path} (Status: {status})")
return {**result, "status": "skipped", "reason": f"lifecycle_status_{status}"}
# 2. Type & Config Resolution
note_type = resolve_note_type(fm.get("type"), self.registry) note_type = resolve_note_type(fm.get("type"), self.registry)
fm["type"] = note_type fm["type"] = note_type
# Dann ermitteln wir die effektiven Werte unter Berücksichtigung des Frontmatters!
# Hier lag der Fehler: Vorher wurde einfach überschrieben.
effective_profile = effective_chunk_profile_name(fm, note_type, self.registry) effective_profile = effective_chunk_profile_name(fm, note_type, self.registry)
effective_weight = effective_retriever_weight(fm, note_type, self.registry) effective_weight = effective_retriever_weight(fm, note_type, self.registry)
# Wir schreiben die effektiven Werte zurück ins FM, damit note_payload sie sicher hat
fm["chunk_profile"] = effective_profile fm["chunk_profile"] = effective_profile
fm["retriever_weight"] = effective_weight fm["retriever_weight"] = effective_weight
# 3. Build Note Payload # 3. Build Note Payload (Inkl. Multi-Hash für WP-22)
try: try:
note_pl = make_note_payload( note_pl = make_note_payload(
parsed, parsed,
@ -183,9 +188,11 @@ class IngestionService:
# Text Body Fallback # Text Body Fallback
if not note_pl.get("fulltext"): note_pl["fulltext"] = getattr(parsed, "body", "") or "" if not note_pl.get("fulltext"): note_pl["fulltext"] = getattr(parsed, "body", "") or ""
# Update Payload with explicit effective values (Sicherheit) # Sicherstellen der effektiven Werte im Payload
note_pl["retriever_weight"] = effective_weight note_pl["retriever_weight"] = effective_weight
note_pl["chunk_profile"] = effective_profile note_pl["chunk_profile"] = effective_profile
# WP-22: Status speichern
note_pl["status"] = status
note_id = note_pl["note_id"] note_id = note_pl["note_id"]
except Exception as e: except Exception as e:
@ -198,6 +205,7 @@ class IngestionService:
old_payload = self._fetch_note_payload(note_id) old_payload = self._fetch_note_payload(note_id)
has_old = old_payload is not None has_old = old_payload is not None
# Prüfung gegen den aktuell konfigurierten Hash-Modus (body oder full)
check_key = f"{self.active_hash_mode}:{hash_source}:{hash_normalize}" check_key = f"{self.active_hash_mode}:{hash_source}:{hash_normalize}"
old_hashes = (old_payload or {}).get("hashes") old_hashes = (old_payload or {}).get("hashes")
@ -217,17 +225,16 @@ class IngestionService:
if not apply: if not apply:
return {**result, "status": "dry-run", "changed": True, "note_id": note_id} return {**result, "status": "dry-run", "changed": True, "note_id": note_id}
# 5. Processing # 5. Processing (Chunking, Embedding, Edge Generation)
try: try:
body_text = getattr(parsed, "body", "") or "" body_text = getattr(parsed, "body", "") or ""
# FIX: Wir laden jetzt die Config für das SPEZIFISCHE Profil # Konfiguration für das spezifische Profil laden
# (z.B. wenn User "sliding_short" wollte, laden wir dessen Params)
chunk_config = self._get_chunk_config_by_profile(effective_profile, note_type) chunk_config = self._get_chunk_config_by_profile(effective_profile, note_type)
chunks = await assemble_chunks(fm["id"], body_text, fm["type"], config=chunk_config) chunks = await assemble_chunks(fm["id"], body_text, fm["type"], config=chunk_config)
# chunk_payloads werden mit den aktualisierten FM-Werten gebaut # Chunks mit Metadaten anreichern
chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text) chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text)
vecs = [] vecs = []
@ -244,32 +251,47 @@ class IngestionService:
logger.error(f"Embedding failed: {e}") logger.error(f"Embedding failed: {e}")
raise RuntimeError(f"Embedding failed: {e}") raise RuntimeError(f"Embedding failed: {e}")
# Kanten generieren
try: try:
edges = build_edges_for_note( raw_edges = build_edges_for_note(
note_id, note_id,
chunk_pls, chunk_pls,
note_level_references=note_pl.get("references", []), note_level_references=note_pl.get("references", []),
include_note_scope_refs=note_scope_refs include_note_scope_refs=note_scope_refs
) )
except TypeError: except TypeError:
edges = build_edges_for_note(note_id, chunk_pls) raw_edges = build_edges_for_note(note_id, chunk_pls)
# --- WP-22: Edge Registry Validation (Teil B) ---
edges = []
if raw_edges:
for edge in raw_edges:
original_kind = edge.get("kind", "related_to")
# Normalisierung über die Registry (Alias-Auflösung)
canonical_kind = edge_registry.resolve(original_kind)
edge["kind"] = canonical_kind
edges.append(edge)
except Exception as e: except Exception as e:
logger.error(f"Processing failed: {e}", exc_info=True) logger.error(f"Processing failed: {e}", exc_info=True)
return {**result, "error": f"Processing failed: {str(e)}"} return {**result, "error": f"Processing failed: {str(e)}"}
# 6. Upsert # 6. Upsert in Qdrant
try: try:
# Alte Fragmente löschen, um "Geister-Chunks" zu vermeiden
if purge_before and has_old: if purge_before and has_old:
self._purge_artifacts(note_id) self._purge_artifacts(note_id)
# Note Metadaten
n_name, n_pts = points_for_note(self.prefix, note_pl, None, self.dim) n_name, n_pts = points_for_note(self.prefix, note_pl, None, self.dim)
upsert_batch(self.client, n_name, n_pts) upsert_batch(self.client, n_name, n_pts)
# Chunks (Vektoren)
if chunk_pls and vecs: if chunk_pls and vecs:
c_name, c_pts = points_for_chunks(self.prefix, chunk_pls, vecs) c_name, c_pts = points_for_chunks(self.prefix, chunk_pls, vecs)
upsert_batch(self.client, c_name, c_pts) upsert_batch(self.client, c_name, c_pts)
# Kanten
if edges: if edges:
e_name, e_pts = points_for_edges(self.prefix, edges) e_name, e_pts = points_for_edges(self.prefix, edges)
upsert_batch(self.client, e_name, e_pts) upsert_batch(self.client, e_name, e_pts)
@ -286,8 +308,8 @@ class IngestionService:
logger.error(f"Upsert failed: {e}", exc_info=True) logger.error(f"Upsert failed: {e}", exc_info=True)
return {**result, "error": f"DB Upsert failed: {e}"} return {**result, "error": f"DB Upsert failed: {e}"}
# ... (Restliche Methoden wie _fetch_note_payload bleiben unverändert) ...
def _fetch_note_payload(self, note_id: str) -> Optional[dict]: def _fetch_note_payload(self, note_id: str) -> Optional[dict]:
"""Holt das aktuelle Payload einer Note aus Qdrant."""
from qdrant_client.http import models as rest from qdrant_client.http import models as rest
col = f"{self.prefix}_notes" col = f"{self.prefix}_notes"
try: try:
@ -297,6 +319,7 @@ class IngestionService:
except: return None except: return None
def _artifacts_missing(self, note_id: str) -> Tuple[bool, bool]: def _artifacts_missing(self, note_id: str) -> Tuple[bool, bool]:
"""Prüft, ob Chunks oder Kanten für eine Note fehlen (Integritätscheck)."""
from qdrant_client.http import models as rest from qdrant_client.http import models as rest
c_col = f"{self.prefix}_chunks" c_col = f"{self.prefix}_chunks"
e_col = f"{self.prefix}_edges" e_col = f"{self.prefix}_edges"
@ -308,6 +331,7 @@ class IngestionService:
except: return True, True except: return True, True
def _purge_artifacts(self, note_id: str): def _purge_artifacts(self, note_id: str):
"""Löscht alle Chunks und Edges einer Note (vor dem Neu-Schreiben)."""
from qdrant_client.http import models as rest from qdrant_client.http import models as rest
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))]) f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
selector = rest.FilterSelector(filter=f) selector = rest.FilterSelector(filter=f)
@ -317,6 +341,7 @@ class IngestionService:
except Exception: pass except Exception: pass
async def create_from_text(self, markdown_content: str, filename: str, vault_root: str, folder: str = "00_Inbox") -> Dict[str, Any]: async def create_from_text(self, markdown_content: str, filename: str, vault_root: str, folder: str = "00_Inbox") -> Dict[str, Any]:
"""Hilfsmethode zur Erstellung einer Note aus einem Textstream (Editor-Save)."""
target_dir = os.path.join(vault_root, folder) target_dir = os.path.join(vault_root, folder)
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
file_path = os.path.join(target_dir, filename) file_path = os.path.join(target_dir, filename)

View File

@ -1,84 +1,63 @@
""" """
FILE: app/core/retriever.py FILE: app/core/retriever.py
DESCRIPTION: Implementiert die Hybrid-Suche (Vektor + Graph-Expansion) und das Scoring-Modell (Explainability). DESCRIPTION: Haupt-Schnittstelle für die Suche. Orchestriert Vektorsuche und Graph-Expansion.
VERSION: 0.5.3 Nutzt retriever_scoring.py für die WP-22 Logik.
FIX: TypeError in embed_text (model_name) behoben.
FIX: Pydantic ValidationError (Target/Source) behoben.
VERSION: 0.6.15 (WP-22 Full & Stable)
STATUS: Active STATUS: Active
DEPENDENCIES: app.config, app.models.dto, app.core.qdrant*, app.services.embeddings_client, app.core.graph_adapter DEPENDENCIES: app.config, app.models.dto, app.core.qdrant*, app.core.graph_adapter, app.core.retriever_scoring
LAST_ANALYSIS: 2025-12-15
""" """
from __future__ import annotations from __future__ import annotations
import os import os
import time import time
from functools import lru_cache import logging
from typing import Any, Dict, List, Tuple, Iterable, Optional from typing import Any, Dict, List, Tuple, Iterable, Optional
from app.config import get_settings from app.config import get_settings
from app.models.dto import ( from app.models.dto import (
QueryRequest, QueryRequest, QueryResponse, QueryHit,
QueryResponse, Explanation, ScoreBreakdown, Reason, EdgeDTO
QueryHit,
Explanation,
ScoreBreakdown,
Reason,
EdgeDTO
) )
import app.core.qdrant as qdr import app.core.qdrant as qdr
import app.core.qdrant_points as qp import app.core.qdrant_points as qp
import app.services.embeddings_client as ec import app.services.embeddings_client as ec
import app.core.graph_adapter as ga import app.core.graph_adapter as ga
try: # Mathematische Engine importieren
import yaml # type: ignore[import] from app.core.retriever_scoring import get_weights, compute_wp22_score
except Exception: # pragma: no cover
yaml = None # type: ignore[assignment]
logger = logging.getLogger(__name__)
@lru_cache # ==============================================================================
def _get_scoring_weights() -> Tuple[float, float, float]: # 1. CORE HELPERS & CONFIG LOADERS
"""Liefert (semantic_weight, edge_weight, centrality_weight) für den Retriever.""" # ==============================================================================
settings = get_settings()
sem = float(getattr(settings, "RETRIEVER_W_SEM", 1.0))
edge = float(getattr(settings, "RETRIEVER_W_EDGE", 0.0))
cent = float(getattr(settings, "RETRIEVER_W_CENT", 0.0))
config_path = os.getenv("MINDNET_RETRIEVER_CONFIG", "config/retriever.yaml")
if yaml is None:
return sem, edge, cent
try:
if os.path.exists(config_path):
with open(config_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
scoring = data.get("scoring", {}) or {}
sem = float(scoring.get("semantic_weight", sem))
edge = float(scoring.get("edge_weight", edge))
cent = float(scoring.get("centrality_weight", cent))
except Exception:
return sem, edge, cent
return sem, edge, cent
def _get_client_and_prefix() -> Tuple[Any, str]: def _get_client_and_prefix() -> Tuple[Any, str]:
"""Liefert (QdrantClient, prefix).""" """Initialisiert Qdrant Client und lädt Collection-Prefix."""
cfg = qdr.QdrantConfig.from_env() cfg = qdr.QdrantConfig.from_env()
client = qdr.get_client(cfg) return qdr.get_client(cfg), cfg.prefix
return client, cfg.prefix
def _get_query_vector(req: QueryRequest) -> List[float]: def _get_query_vector(req: QueryRequest) -> List[float]:
"""Liefert den Query-Vektor aus dem Request.""" """
Vektorisiert die Anfrage.
FIX: Enthält try-except Block für unterschiedliche Signaturen von ec.embed_text.
"""
if req.query_vector: if req.query_vector:
return list(req.query_vector) return list(req.query_vector)
if not req.query: if not req.query:
raise ValueError("QueryRequest benötigt entweder query oder query_vector") raise ValueError("Kein Text oder Vektor für die Suche angegeben.")
settings = get_settings() settings = get_settings()
model_name = settings.MODEL_NAME
try: try:
return ec.embed_text(req.query, model_name=model_name) # Versuch mit modernem Interface (WP-03 kompatibel)
return ec.embed_text(req.query, model_name=settings.MODEL_NAME)
except TypeError: except TypeError:
# Fallback für Signaturen, die 'model_name' nicht als Keyword akzeptieren
logger.debug("ec.embed_text does not accept 'model_name' keyword. Falling back.")
return ec.embed_text(req.query) return ec.embed_text(req.query)
@ -87,138 +66,116 @@ def _semantic_hits(
prefix: str, prefix: str,
vector: List[float], vector: List[float],
top_k: int, top_k: int,
filters: Dict[str, Any] | None = None, filters: Optional[Dict] = None
) -> List[Tuple[str, float, Dict[str, Any]]]: ) -> List[Tuple[str, float, Dict[str, Any]]]:
"""Führt eine semantische Suche aus.""" """Führt die Vektorsuche durch und konvertiert Qdrant-Points in ein einheitliches Format."""
flt = filters or None raw_hits = qp.search_chunks_by_vector(client, prefix, vector, top=top_k, filters=filters)
raw_hits = qp.search_chunks_by_vector(client, prefix, vector, top=top_k, filters=flt) # Strikte Typkonvertierung für Stabilität
results: List[Tuple[str, float, Dict[str, Any]]] = [] return [(str(hit[0]), float(hit[1]), dict(hit[2] or {})) for hit in raw_hits]
for pid, score, payload in raw_hits:
results.append((str(pid), float(score), dict(payload or {})))
return results
# ==============================================================================
def _compute_total_score( # 2. EXPLANATION LAYER (DEBUG & VERIFIABILITY)
semantic_score: float, # ==============================================================================
payload: Dict[str, Any],
edge_bonus: float = 0.0,
cent_bonus: float = 0.0,
) -> Tuple[float, float, float]:
"""Berechnet total_score."""
raw_weight = payload.get("retriever_weight", 1.0)
try:
weight = float(raw_weight)
except (TypeError, ValueError):
weight = 1.0
if weight < 0.0:
weight = 0.0
sem_w, edge_w, cent_w = _get_scoring_weights()
total = (sem_w * float(semantic_score) * weight) + (edge_w * edge_bonus) + (cent_w * cent_bonus)
return float(total), float(edge_bonus), float(cent_bonus)
# --- WP-04b Explanation Logic ---
def _build_explanation( def _build_explanation(
semantic_score: float, semantic_score: float,
payload: Dict[str, Any], payload: Dict[str, Any],
edge_bonus: float, scoring_debug: Dict[str, Any],
cent_bonus: float,
subgraph: Optional[ga.Subgraph], subgraph: Optional[ga.Subgraph],
node_key: Optional[str] target_note_id: Optional[str],
applied_boosts: Optional[Dict[str, float]] = None
) -> Explanation: ) -> Explanation:
"""Erstellt ein Explanation-Objekt.""" """
sem_w, _edge_w, _cent_w = _get_scoring_weights() Transformiert mathematische Scores und Graph-Signale in eine menschenlesbare Erklärung.
# Scoring weights erneut laden für Reason-Details Behebt Pydantic ValidationErrors durch explizite String-Sicherung.
_, edge_w_cfg, cent_w_cfg = _get_scoring_weights() """
_, edge_w_cfg, _ = get_weights()
try: base_val = scoring_debug["base_val"]
type_weight = float(payload.get("retriever_weight", 1.0))
except (TypeError, ValueError):
type_weight = 1.0
note_type = payload.get("type", "unknown")
# 1. Detaillierter mathematischer Breakdown
breakdown = ScoreBreakdown( breakdown = ScoreBreakdown(
semantic_contribution=(sem_w * semantic_score * type_weight), semantic_contribution=base_val,
edge_contribution=(edge_w_cfg * edge_bonus), edge_contribution=base_val * scoring_debug["edge_impact_final"],
centrality_contribution=(cent_w_cfg * cent_bonus), centrality_contribution=base_val * scoring_debug["cent_impact_final"],
raw_semantic=semantic_score, raw_semantic=semantic_score,
raw_edge_bonus=edge_bonus, raw_edge_bonus=scoring_debug["edge_bonus"],
raw_centrality=cent_bonus, raw_centrality=scoring_debug["cent_bonus"],
node_weight=type_weight node_weight=float(payload.get("retriever_weight", 1.0)),
status_multiplier=scoring_debug["status_multiplier"],
graph_boost_factor=scoring_debug["graph_boost_factor"]
) )
reasons: List[Reason] = [] reasons: List[Reason] = []
edges_dto: List[EdgeDTO] = [] edges_dto: List[EdgeDTO] = []
# 2. Gründe für Semantik hinzufügen
if semantic_score > 0.85: if semantic_score > 0.85:
reasons.append(Reason(kind="semantic", message="Sehr hohe textuelle Übereinstimmung.", score_impact=breakdown.semantic_contribution)) reasons.append(Reason(kind="semantic", message="Sehr hohe textuelle Übereinstimmung.", score_impact=base_val))
elif semantic_score > 0.70: elif semantic_score > 0.70:
reasons.append(Reason(kind="semantic", message="Gute textuelle Übereinstimmung.", score_impact=breakdown.semantic_contribution)) reasons.append(Reason(kind="semantic", message="Inhaltliche Übereinstimmung.", score_impact=base_val))
# 3. Gründe für Typ und Lifecycle
type_weight = float(payload.get("retriever_weight", 1.0))
if type_weight != 1.0: if type_weight != 1.0:
msg = "Bevorzugt" if type_weight > 1.0 else "Leicht abgewertet" msg = "Bevorzugt" if type_weight > 1.0 else "De-priorisiert"
reasons.append(Reason(kind="type", message=f"{msg} aufgrund des Typs '{note_type}'.", score_impact=(sem_w * semantic_score * (type_weight - 1.0)))) reasons.append(Reason(kind="type", message=f"{msg} durch Typ-Profil.", score_impact=base_val * (type_weight - 1.0)))
if subgraph and node_key and edge_bonus > 0:
if hasattr(subgraph, "get_outgoing_edges"):
outgoing = subgraph.get_outgoing_edges(node_key)
for edge in outgoing:
target = edge.get("target", "Unknown")
kind = edge.get("kind", "edge")
weight = edge.get("weight", 0.0)
if weight > 0.05:
edges_dto.append(EdgeDTO(id=f"{node_key}->{target}:{kind}", kind=kind, source=node_key, target=target, weight=weight, direction="out"))
# 4. Kanten-Verarbeitung (Graph-Intelligence)
if subgraph and target_note_id and scoring_debug["edge_bonus"] > 0:
raw_edges = []
if hasattr(subgraph, "get_incoming_edges"): if hasattr(subgraph, "get_incoming_edges"):
incoming = subgraph.get_incoming_edges(node_key) raw_edges.extend(subgraph.get_incoming_edges(target_note_id) or [])
for edge in incoming: if hasattr(subgraph, "get_outgoing_edges"):
src = edge.get("source", "Unknown") raw_edges.extend(subgraph.get_outgoing_edges(target_note_id) or [])
kind = edge.get("kind", "edge")
weight = edge.get("weight", 0.0)
if weight > 0.05:
edges_dto.append(EdgeDTO(id=f"{src}->{node_key}:{kind}", kind=kind, source=src, target=node_key, weight=weight, direction="in"))
all_edges = sorted(edges_dto, key=lambda e: e.weight, reverse=True) for edge in raw_edges:
for top_edge in all_edges[:3]: # FIX: Zwingende String-Konvertierung für Pydantic-Stabilität
impact = edge_w_cfg * top_edge.weight src = str(edge.get("source") or "note_root")
dir_txt = "Verweist auf" if top_edge.direction == "out" else "Referenziert von" tgt = str(edge.get("target") or target_note_id or "unknown_target")
tgt_txt = top_edge.target if top_edge.direction == "out" else top_edge.source kind = str(edge.get("kind", "related_to"))
reasons.append(Reason(kind="edge", message=f"{dir_txt} '{tgt_txt}' via '{top_edge.kind}'", score_impact=impact, details={"kind": top_edge.kind})) prov = str(edge.get("provenance", "rule"))
conf = float(edge.get("confidence", 1.0))
if cent_bonus > 0.01: direction = "in" if tgt == target_note_id else "out"
reasons.append(Reason(kind="centrality", message="Knoten liegt zentral im Kontext.", score_impact=breakdown.centrality_contribution))
return Explanation(breakdown=breakdown, reasons=reasons, related_edges=edges_dto if edges_dto else None) edge_obj = EdgeDTO(
id=f"{src}->{tgt}:{kind}",
kind=kind,
source=src,
target=tgt,
weight=conf,
direction=direction,
provenance=prov,
confidence=conf
)
edges_dto.append(edge_obj)
# Die 3 wichtigsten Kanten als Begründung formulieren
top_edges = sorted(edges_dto, key=lambda e: e.confidence, reverse=True)
for e in top_edges[:3]:
peer = e.source if e.direction == "in" else e.target
prov_txt = "Bestätigte" if e.provenance == "explicit" else "KI-basierte"
boost_txt = f" [Boost x{applied_boosts.get(e.kind)}]" if applied_boosts and e.kind in applied_boosts else ""
def _extract_expand_options(req: QueryRequest) -> Tuple[int, List[str] | None]: reasons.append(Reason(
"""Extrahiert depth und edge_types.""" kind="edge",
expand = getattr(req, "expand", None) message=f"{prov_txt} Kante '{e.kind}'{boost_txt} von/zu '{peer}'.",
if not expand: score_impact=edge_w_cfg * e.confidence
return 0, None ))
depth = 1 if scoring_debug["cent_bonus"] > 0.01:
edge_types: List[str] | None = None reasons.append(Reason(kind="centrality", message="Die Notiz ist ein zentraler Informations-Hub.", score_impact=breakdown.centrality_contribution))
if hasattr(expand, "depth") or hasattr(expand, "edge_types"): return Explanation(
depth = int(getattr(expand, "depth", 1) or 1) breakdown=breakdown,
types_val = getattr(expand, "edge_types", None) reasons=reasons,
if types_val: related_edges=edges_dto if edges_dto else None,
edge_types = list(types_val) applied_boosts=applied_boosts
return depth, edge_types )
if isinstance(expand, dict):
if "depth" in expand:
depth = int(expand.get("depth") or 1)
if "edge_types" in expand and expand["edge_types"] is not None:
edge_types = list(expand["edge_types"])
return depth, edge_types
return 0, None
# ==============================================================================
# 3. CORE RETRIEVAL PIPELINE
# ==============================================================================
def _build_hits_from_semantic( def _build_hits_from_semantic(
hits: Iterable[Tuple[str, float, Dict[str, Any]]], hits: Iterable[Tuple[str, float, Dict[str, Any]]],
@ -226,113 +183,128 @@ def _build_hits_from_semantic(
used_mode: str, used_mode: str,
subgraph: ga.Subgraph | None = None, subgraph: ga.Subgraph | None = None,
explain: bool = False, explain: bool = False,
dynamic_edge_boosts: Dict[str, float] = None
) -> QueryResponse: ) -> QueryResponse:
"""Baut strukturierte QueryHits.""" """Wandelt semantische Roh-Treffer in hochgeladene, bewertete QueryHits um."""
t0 = time.time() t0 = time.time()
enriched: List[Tuple[str, float, Dict[str, Any], float, float, float]] = [] enriched = []
for pid, semantic_score, payload in hits: for pid, semantic_score, payload in hits:
edge_bonus = 0.0 edge_bonus, cent_bonus = 0.0, 0.0
cent_bonus = 0.0 target_id = payload.get("note_id")
node_key = payload.get("chunk_id") or payload.get("note_id")
if subgraph is not None and node_key: if subgraph and target_id:
try: try:
edge_bonus = float(subgraph.edge_bonus(node_key)) edge_bonus = float(subgraph.edge_bonus(target_id))
cent_bonus = float(subgraph.centrality_bonus(target_id))
except Exception: except Exception:
edge_bonus = 0.0 pass
try:
cent_bonus = float(subgraph.centrality_bonus(node_key))
except Exception:
cent_bonus = 0.0
total, edge_bonus, cent_bonus = _compute_total_score(semantic_score, payload, edge_bonus=edge_bonus, cent_bonus=cent_bonus) # Mathematisches Scoring via WP-22 Engine
enriched.append((pid, float(semantic_score), payload, total, edge_bonus, cent_bonus)) debug_data = compute_wp22_score(
semantic_score, payload, edge_bonus, cent_bonus, dynamic_edge_boosts
)
enriched.append((pid, semantic_score, payload, debug_data))
enriched_sorted = sorted(enriched, key=lambda h: h[3], reverse=True) # Sortierung nach finalem mathematischen Score
limited = enriched_sorted[: max(1, top_k)] enriched_sorted = sorted(enriched, key=lambda h: h[3]["total"], reverse=True)
limited_hits = enriched_sorted[: max(1, top_k)]
results: List[QueryHit] = [] results: List[QueryHit] = []
for pid, semantic_score, payload, total, edge_bonus, cent_bonus in limited: for pid, s_score, pl, dbg in limited_hits:
explanation_obj = None explanation_obj = None
if explain: if explain:
explanation_obj = _build_explanation( explanation_obj = _build_explanation(
semantic_score=float(semantic_score), semantic_score=float(s_score),
payload=payload, payload=pl,
edge_bonus=edge_bonus, scoring_debug=dbg,
cent_bonus=cent_bonus,
subgraph=subgraph, subgraph=subgraph,
node_key=payload.get("chunk_id") or payload.get("note_id") target_note_id=pl.get("note_id"),
applied_boosts=dynamic_edge_boosts
) )
text_content = payload.get("page_content") or payload.get("text") or payload.get("content") # Payload Text-Feld normalisieren
text_content = pl.get("page_content") or pl.get("text") or pl.get("content", "[Kein Text]")
results.append(QueryHit( results.append(QueryHit(
node_id=str(pid), node_id=str(pid),
note_id=payload.get("note_id"), note_id=str(pl.get("note_id", "unknown")),
semantic_score=float(semantic_score), semantic_score=float(s_score),
edge_bonus=edge_bonus, edge_bonus=dbg["edge_bonus"],
centrality_bonus=cent_bonus, centrality_bonus=dbg["cent_bonus"],
total_score=total, total_score=dbg["total"],
paths=None,
source={ source={
"path": payload.get("path"), "path": pl.get("path"),
"section": payload.get("section") or payload.get("section_title"), "section": pl.get("section") or pl.get("section_title"),
"text": text_content "text": text_content
}, },
# --- FIX: Wir füllen das payload-Feld explizit --- payload=pl,
payload=payload,
explanation=explanation_obj explanation=explanation_obj
)) ))
dt = int((time.time() - t0) * 1000) return QueryResponse(results=results, used_mode=used_mode, latency_ms=int((time.time() - t0) * 1000))
return QueryResponse(results=results, used_mode=used_mode, latency_ms=dt)
def semantic_retrieve(req: QueryRequest) -> QueryResponse:
"""Reiner semantischer Retriever."""
client, prefix = _get_client_and_prefix()
vector = _get_query_vector(req)
top_k = req.top_k or get_settings().RETRIEVER_TOP_K
hits = _semantic_hits(client, prefix, vector, top_k=top_k, filters=req.filters)
return _build_hits_from_semantic(hits, top_k=top_k, used_mode="semantic", subgraph=None, explain=req.explain)
def hybrid_retrieve(req: QueryRequest) -> QueryResponse: def hybrid_retrieve(req: QueryRequest) -> QueryResponse:
"""Hybrid-Retriever: semantische Suche + optionale Edge-Expansion.""" """
Die Haupt-Einstiegsfunktion für die hybride Suche.
Kombiniert Vektorsuche mit Graph-Expansion, Provenance-Weighting und Intent-Boosting.
"""
client, prefix = _get_client_and_prefix() client, prefix = _get_client_and_prefix()
if req.query_vector: vector = list(req.query_vector) if req.query_vector else _get_query_vector(req)
vector = list(req.query_vector) top_k = req.top_k or 10
else:
vector = _get_query_vector(req)
top_k = req.top_k or get_settings().RETRIEVER_TOP_K # 1. Semantische Seed-Suche
hits = _semantic_hits(client, prefix, vector, top_k=top_k, filters=req.filters) hits = _semantic_hits(client, prefix, vector, top_k=top_k, filters=req.filters)
depth, edge_types = _extract_expand_options(req) # 2. Graph Expansion Konfiguration
expand_cfg = req.expand if isinstance(req.expand, dict) else {}
depth = int(expand_cfg.get("depth", 1))
boost_edges = getattr(req, "boost_edges", {}) or {}
subgraph: ga.Subgraph | None = None subgraph: ga.Subgraph | None = None
if depth and depth > 0: if depth > 0 and hits:
seed_ids: List[str] = [] # Start-IDs für den Graph-Traversal sammeln
for _pid, _score, payload in hits: seed_ids = list({h[2].get("note_id") for h in hits if h[2].get("note_id")})
key = payload.get("chunk_id") or payload.get("note_id")
if key and key not in seed_ids:
seed_ids.append(key)
if seed_ids: if seed_ids:
try: try:
subgraph = ga.expand(client, prefix, seed_ids, depth=depth, edge_types=edge_types) # Subgraph aus RAM/DB laden
except Exception: subgraph = ga.expand(client, prefix, seed_ids, depth=depth, edge_types=expand_cfg.get("edge_types"))
# --- WP-22: Kanten-Gewichtung im RAM-Graphen vor Bonus-Berechnung ---
if subgraph and hasattr(subgraph, "graph"):
for _, _, data in subgraph.graph.edges(data=True):
# A. Provenance Weighting (WP-22 Bonus für Herkunft)
prov = data.get("provenance", "rule")
# Belohnung: Explizite Links (1.0) > Smart (0.9) > Rule (0.7)
prov_w = 1.0 if prov == "explicit" else (0.9 if prov == "smart" else 0.7)
# B. Intent Boost Multiplikator (Vom Router dynamisch injiziert)
kind = data.get("kind")
intent_multiplier = boost_edges.get(kind, 1.0)
# Finales Gewicht setzen (Basis * Provenance * Intent)
data["weight"] = data.get("weight", 1.0) * prov_w * intent_multiplier
except Exception as e:
logger.error(f"Graph Expansion failed: {e}")
subgraph = None subgraph = None
return _build_hits_from_semantic(hits, top_k=top_k, used_mode="hybrid", subgraph=subgraph, explain=req.explain) # 3. Scoring & Explanation Generierung
return _build_hits_from_semantic(hits, top_k, "hybrid", subgraph, req.explain, boost_edges)
def semantic_retrieve(req: QueryRequest) -> QueryResponse:
"""Standard Vektorsuche ohne Graph-Einfluss (WP-02 Fallback)."""
client, prefix = _get_client_and_prefix()
vector = _get_query_vector(req)
hits = _semantic_hits(client, prefix, vector, req.top_k or 10, req.filters)
return _build_hits_from_semantic(hits, req.top_k or 10, "semantic", explain=req.explain)
class Retriever: class Retriever:
""" """Schnittstelle für die asynchrone Suche."""
Wrapper-Klasse für WP-05 (Chat).
"""
def __init__(self):
pass
async def search(self, request: QueryRequest) -> QueryResponse: async def search(self, request: QueryRequest) -> QueryResponse:
"""Führt eine hybride Suche aus."""
return hybrid_retrieve(request) return hybrid_retrieve(request)

View File

@ -0,0 +1,120 @@
"""
FILE: app/core/retriever_scoring.py
DESCRIPTION: Mathematische Kern-Logik für das WP-22 Scoring.
Berechnet Relevanz-Scores basierend auf Semantik, Graph-Intelligence und Content Lifecycle.
VERSION: 1.0.1 (WP-22 Full Math Engine)
STATUS: Active
DEPENDENCIES: app.config, typing
"""
import os
import logging
from functools import lru_cache
from typing import Any, Dict, Tuple, Optional
try:
import yaml
except ImportError:
yaml = None
logger = logging.getLogger(__name__)
@lru_cache
def get_weights() -> Tuple[float, float, float]:
"""
Liefert die Basis-Gewichtung (semantic, edge, centrality) aus der Konfiguration.
Priorität:
1. config/retriever.yaml (Scoring-Sektion)
2. Umgebungsvariablen (RETRIEVER_W_*)
3. System-Defaults (1.0, 0.0, 0.0)
"""
from app.config import get_settings
settings = get_settings()
# Defaults aus Settings laden
sem = float(getattr(settings, "RETRIEVER_W_SEM", 1.0))
edge = float(getattr(settings, "RETRIEVER_W_EDGE", 0.0))
cent = float(getattr(settings, "RETRIEVER_W_CENT", 0.0))
# Optionaler Override via YAML
config_path = os.getenv("MINDNET_RETRIEVER_CONFIG", "config/retriever.yaml")
if yaml and os.path.exists(config_path):
try:
with open(config_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
scoring = data.get("scoring", {})
sem = float(scoring.get("semantic_weight", sem))
edge = float(scoring.get("edge_weight", edge))
cent = float(scoring.get("centrality_weight", cent))
except Exception as e:
logger.warning(f"Retriever Configuration could not be fully loaded from {config_path}: {e}")
return sem, edge, cent
def get_status_multiplier(payload: Dict[str, Any]) -> float:
"""
WP-22 A: Content Lifecycle Multiplier.
Steuert das Ranking basierend auf dem Reifegrad der Information.
- stable: 1.2 (Belohnung für verifiziertes Wissen)
- active: 1.0 (Standard-Gewichtung)
- draft: 0.5 (Bestrafung für unfertige Fragmente)
"""
status = str(payload.get("status", "active")).lower().strip()
if status == "stable":
return 1.2
if status == "draft":
return 0.5
return 1.0
def compute_wp22_score(
semantic_score: float,
payload: Dict[str, Any],
edge_bonus_raw: float = 0.0,
cent_bonus_raw: float = 0.0,
dynamic_edge_boosts: Optional[Dict[str, float]] = None
) -> Dict[str, Any]:
"""
Die zentrale mathematische Scoring-Formel der Mindnet Intelligence.
Implementiert das WP-22 Hybrid-Scoring (Semantic * Lifecycle * Graph).
FORMEL:
Score = (Similarity * StatusMult) * (1 + (TypeWeight - 1) + ((EdgeW * EB + CentW * CB) * IntentBoost))
Returns:
Dict mit dem finalen 'total' Score und allen mathematischen Zwischenwerten für den Explanation Layer.
"""
sem_w, edge_w_cfg, cent_w_cfg = get_weights()
status_mult = get_status_multiplier(payload)
# Retriever Weight (Type Boost aus types.yaml, z.B. 1.1 für Decisions)
node_weight = float(payload.get("retriever_weight", 1.0))
# 1. Berechnung des Base Scores (Semantik gewichtet durch Lifecycle-Status)
base_val = float(semantic_score) * status_mult
# 2. Graph Boost Factor (Teil C: Intent-spezifische Verstärkung)
# Erhöht das Gewicht des gesamten Graphen um 50%, wenn ein spezifischer Intent vorliegt.
graph_boost_factor = 1.5 if dynamic_edge_boosts and (edge_bonus_raw > 0 or cent_bonus_raw > 0) else 1.0
# 3. Einzelne Graph-Komponenten berechnen
edge_impact_final = (edge_w_cfg * edge_bonus_raw) * graph_boost_factor
cent_impact_final = (cent_w_cfg * cent_bonus_raw) * graph_boost_factor
# 4. Finales Zusammenführen (Merging)
# (node_weight - 1.0) sorgt dafür, dass ein Gewicht von 1.0 keinen Einfluss hat (neutral).
total = base_val * (1.0 + (node_weight - 1.0) + edge_impact_final + cent_impact_final)
# Sicherstellen, dass der Score niemals 0 oder negativ ist (Floor)
final_score = max(0.0001, float(total))
return {
"total": final_score,
"edge_bonus": float(edge_bonus_raw),
"cent_bonus": float(cent_bonus_raw),
"status_multiplier": status_mult,
"graph_boost_factor": graph_boost_factor,
"type_impact": node_weight - 1.0,
"base_val": base_val,
"edge_impact_final": edge_impact_final,
"cent_impact_final": cent_impact_final
}

View File

@ -1,10 +1,10 @@
""" """
FILE: app/models/dto.py FILE: app/models/dto.py
DESCRIPTION: Pydantic-Modelle (DTOs) für Request/Response Bodies. Definiert das API-Schema. DESCRIPTION: Pydantic-Modelle (DTOs) für Request/Response Bodies. Definiert das API-Schema.
VERSION: 0.6.2 VERSION: 0.6.6 (WP-22 Debug & Stability Update)
STATUS: Active STATUS: Active
DEPENDENCIES: pydantic, typing, uuid DEPENDENCIES: pydantic, typing, uuid
LAST_ANALYSIS: 2025-12-15 LAST_ANALYSIS: 2025-12-18
""" """
from __future__ import annotations from __future__ import annotations
@ -12,7 +12,8 @@ from pydantic import BaseModel, Field
from typing import List, Literal, Optional, Dict, Any from typing import List, Literal, Optional, Dict, Any
import uuid import uuid
EdgeKind = Literal["references", "references_at", "backlink", "next", "prev", "belongs_to", "depends_on", "related_to", "similar_to"] # Gültige Kanten-Typen gemäß Manual
EdgeKind = Literal["references", "references_at", "backlink", "next", "prev", "belongs_to", "depends_on", "related_to", "similar_to", "caused_by", "derived_from", "based_on", "solves", "blocks", "uses", "guides"]
# --- Basis-DTOs --- # --- Basis-DTOs ---
@ -40,6 +41,8 @@ class EdgeDTO(BaseModel):
target: str target: str
weight: float weight: float
direction: Literal["out", "in", "undirected"] = "out" direction: Literal["out", "in", "undirected"] = "out"
provenance: Optional[Literal["explicit", "rule", "smart", "structure"]] = "explicit"
confidence: float = 1.0
# --- Request Models --- # --- Request Models ---
@ -57,17 +60,17 @@ class QueryRequest(BaseModel):
ret: Dict = {"with_paths": True, "with_notes": True, "with_chunks": True} ret: Dict = {"with_paths": True, "with_notes": True, "with_chunks": True}
explain: bool = False explain: bool = False
# WP-22: Semantic Graph Routing
boost_edges: Optional[Dict[str, float]] = None
class FeedbackRequest(BaseModel): class FeedbackRequest(BaseModel):
""" """
User-Feedback zu einem spezifischen Treffer oder der Gesamtantwort. User-Feedback zu einem spezifischen Treffer oder der Gesamtantwort (WP-08 Basis).
""" """
query_id: str = Field(..., description="ID der ursprünglichen Suche") query_id: str = Field(..., description="ID der ursprünglichen Suche")
# node_id ist optional: Wenn leer oder "generated_answer", gilt es für die Antwort.
# Wenn eine echte Chunk-ID, gilt es für die Quelle.
node_id: str = Field(..., description="ID des bewerteten Treffers oder 'generated_answer'") node_id: str = Field(..., description="ID des bewerteten Treffers oder 'generated_answer'")
# Update: Range auf 1-5 erweitert für differenziertes Tuning score: int = Field(..., ge=1, le=5, description="1 (Irrelevant) bis 5 (Perfekt)")
score: int = Field(..., ge=1, le=5, description="1 (Irrelevant/Falsch) bis 5 (Perfekt)")
comment: Optional[str] = None comment: Optional[str] = None
@ -76,8 +79,7 @@ class ChatRequest(BaseModel):
WP-05: Request für /chat. WP-05: Request für /chat.
""" """
message: str = Field(..., description="Die Nachricht des Users") message: str = Field(..., description="Die Nachricht des Users")
conversation_id: Optional[str] = Field(None, description="Optional: ID für Chat-Verlauf (noch nicht implementiert)") conversation_id: Optional[str] = Field(None, description="ID für Chat-Verlauf")
# RAG Parameter (Override defaults)
top_k: int = 5 top_k: int = 5
explain: bool = False explain: bool = False
@ -85,7 +87,7 @@ class ChatRequest(BaseModel):
# --- WP-04b Explanation Models --- # --- WP-04b Explanation Models ---
class ScoreBreakdown(BaseModel): class ScoreBreakdown(BaseModel):
"""Aufschlüsselung der Score-Komponenten.""" """Aufschlüsselung der Score-Komponenten nach der WP-22 Formel."""
semantic_contribution: float semantic_contribution: float
edge_contribution: float edge_contribution: float
centrality_contribution: float centrality_contribution: float
@ -93,11 +95,14 @@ class ScoreBreakdown(BaseModel):
raw_edge_bonus: float raw_edge_bonus: float
raw_centrality: float raw_centrality: float
node_weight: float node_weight: float
# WP-22 Debug Fields für Messbarkeit
status_multiplier: float = 1.0
graph_boost_factor: float = 1.0
class Reason(BaseModel): class Reason(BaseModel):
"""Ein semantischer Grund für das Ranking.""" """Ein semantischer Grund für das Ranking."""
kind: Literal["semantic", "edge", "type", "centrality"] kind: Literal["semantic", "edge", "type", "centrality", "lifecycle"]
message: str message: str
score_impact: Optional[float] = None score_impact: Optional[float] = None
details: Optional[Dict[str, Any]] = None details: Optional[Dict[str, Any]] = None
@ -108,6 +113,9 @@ class Explanation(BaseModel):
breakdown: ScoreBreakdown breakdown: ScoreBreakdown
reasons: List[Reason] reasons: List[Reason]
related_edges: Optional[List[EdgeDTO]] = None related_edges: Optional[List[EdgeDTO]] = None
# WP-22 Debug: Verifizierung des Routings
applied_intent: Optional[str] = None
applied_boosts: Optional[Dict[str, float]] = None
# --- Response Models --- # --- Response Models ---
@ -115,14 +123,14 @@ class Explanation(BaseModel):
class QueryHit(BaseModel): class QueryHit(BaseModel):
"""Einzelnes Trefferobjekt für /query.""" """Einzelnes Trefferobjekt für /query."""
node_id: str node_id: str
note_id: Optional[str] note_id: str
semantic_score: float semantic_score: float
edge_bonus: float edge_bonus: float
centrality_bonus: float centrality_bonus: float
total_score: float total_score: float
paths: Optional[List[List[Dict]]] = None paths: Optional[List[List[Dict]]] = None
source: Optional[Dict] = None source: Optional[Dict] = None
payload: Optional[Dict] = None # Added for flexibility & WP-06 meta-data payload: Optional[Dict] = None
explanation: Optional[Explanation] = None explanation: Optional[Explanation] = None
@ -146,11 +154,9 @@ class ChatResponse(BaseModel):
""" """
WP-05/06: Antwortstruktur für /chat. WP-05/06: Antwortstruktur für /chat.
""" """
query_id: str = Field(..., description="Traceability ID (dieselbe wie für Search)") query_id: str = Field(..., description="Traceability ID")
answer: str = Field(..., description="Generierte Antwort vom LLM") answer: str = Field(..., description="Generierte Antwort vom LLM")
sources: List[QueryHit] = Field(..., description="Die für die Antwort genutzten Quellen") sources: List[QueryHit] = Field(..., description="Die genutzten Quellen")
latency_ms: int latency_ms: int
intent: Optional[str] = Field("FACT", description="WP-06: Erkannter Intent (FACT/DECISION)") intent: Optional[str] = Field("FACT", description="WP-06: Erkannter Intent")
intent_source: Optional[str] = Field("Unknown", description="WP-06: Quelle der Intent-Erkennung (Keyword vs. LLM)") intent_source: Optional[str] = Field("Unknown", description="Quelle der Intent-Erkennung")

View File

@ -1,11 +1,10 @@
""" """
FILE: app/routers/chat.py FILE: app/routers/chat.py
DESCRIPTION: Haupt-Chat-Interface (RAG & Interview). Enthält Intent-Router (Keywords/LLM) und Prompt-Construction. DESCRIPTION: Haupt-Chat-Interface (RAG & Interview). Enthält Intent-Router (Keywords/LLM) und Prompt-Construction.
VERSION: 2.5.0 VERSION: 2.7.0 (WP-22 Semantic Graph Routing)
STATUS: Active STATUS: Active
DEPENDENCIES: app.config, app.models.dto, app.services.llm_service, app.core.retriever, app.services.feedback_service DEPENDENCIES: app.config, app.models.dto, app.services.llm_service, app.core.retriever, app.services.feedback_service
EXTERNAL_CONFIG: config/decision_engine.yaml, config/types.yaml EXTERNAL_CONFIG: config/decision_engine.yaml, config/types.yaml
LAST_ANALYSIS: 2025-12-15
""" """
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
@ -188,9 +187,6 @@ async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]:
return intent_name, "Keyword (Strategy)" return intent_name, "Keyword (Strategy)"
# 2. FAST PATH B: Type Keywords (z.B. "Projekt", "Werte") -> INTERVIEW # 2. FAST PATH B: Type Keywords (z.B. "Projekt", "Werte") -> INTERVIEW
# FIX: Wir prüfen, ob es eine Frage ist. Fragen zu Typen sollen RAG (FACT/DECISION) sein,
# keine Interviews. Wir überlassen das dann dem LLM Router (Slow Path).
if not _is_question(query_lower): if not _is_question(query_lower):
types_cfg = get_types_config() types_cfg = get_types_config()
types_def = types_cfg.get("types", {}) types_def = types_cfg.get("types", {})
@ -203,7 +199,6 @@ async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]:
# 3. SLOW PATH: LLM Router # 3. SLOW PATH: LLM Router
if settings.get("llm_fallback_enabled", False): if settings.get("llm_fallback_enabled", False):
# Nutze Prompts aus prompts.yaml (via LLM Service)
router_prompt_template = llm.prompts.get("router_prompt", "") router_prompt_template = llm.prompts.get("router_prompt", "")
if router_prompt_template: if router_prompt_template:
@ -211,11 +206,9 @@ async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]:
logger.info("Keywords failed (or Question detected). Asking LLM for Intent...") logger.info("Keywords failed (or Question detected). Asking LLM for Intent...")
try: try:
# Nutze priority="realtime" für den Router, damit er nicht wartet
raw_response = await llm.generate_raw_response(prompt, priority="realtime") raw_response = await llm.generate_raw_response(prompt, priority="realtime")
llm_output_upper = raw_response.upper() llm_output_upper = raw_response.upper()
# Zuerst INTERVIEW prüfen
if "INTERVIEW" in llm_output_upper or "CREATE" in llm_output_upper: if "INTERVIEW" in llm_output_upper or "CREATE" in llm_output_upper:
return "INTERVIEW", "LLM Router" return "INTERVIEW", "LLM Router"
@ -282,11 +275,19 @@ async def chat_endpoint(
inject_types = strategy.get("inject_types", []) inject_types = strategy.get("inject_types", [])
prepend_instr = strategy.get("prepend_instruction", "") prepend_instr = strategy.get("prepend_instruction", "")
# --- WP-22: Semantic Graph Routing (Teil C) ---
# Wir laden die konfigurierten Edge-Boosts für diesen Intent
edge_boosts = strategy.get("edge_boosts", {})
if edge_boosts:
logger.info(f"[{query_id}] Applying Edge Boosts: {edge_boosts}")
query_req = QueryRequest( query_req = QueryRequest(
query=request.message, query=request.message,
mode="hybrid", mode="hybrid",
top_k=request.top_k, top_k=request.top_k,
explain=request.explain explain=request.explain,
# WP-22: Boosts an den Retriever weitergeben
boost_edges=edge_boosts
) )
retrieve_result = await retriever.search(query_req) retrieve_result = await retriever.search(query_req)
hits = retrieve_result.results hits = retrieve_result.results
@ -297,7 +298,9 @@ async def chat_endpoint(
mode="hybrid", mode="hybrid",
top_k=3, top_k=3,
filters={"type": inject_types}, filters={"type": inject_types},
explain=False explain=False,
# WP-22: Boosts auch hier anwenden (Konsistenz)
boost_edges=edge_boosts
) )
strategy_result = await retriever.search(strategy_req) strategy_result = await retriever.search(strategy_req)
existing_ids = {h.node_id for h in hits} existing_ids = {h.node_id for h in hits}

View File

@ -0,0 +1,111 @@
"""
FILE: app/services/edge_registry.py
DESCRIPTION: Single Source of Truth für Kanten-Typen.
FIX: Regex angepasst auf Format **`canonical`** (Bold + Backticks).
VERSION: 0.6.10 (Regex Precision Update)
"""
import re
import os
import json
import logging
from typing import Dict, Optional, Set
print(">>> MODULE_LOAD: edge_registry.py initialized <<<", flush=True)
from app.config import get_settings
logger = logging.getLogger(__name__)
class EdgeRegistry:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(EdgeRegistry, cls).__new__(cls)
cls._instance.initialized = False
return cls._instance
def __init__(self, vault_root: Optional[str] = None):
if self.initialized:
return
settings = get_settings()
env_vocab_path = os.getenv("MINDNET_VOCAB_PATH")
env_vault_root = os.getenv("MINDNET_VAULT_ROOT") or getattr(settings, "MINDNET_VAULT_ROOT", "./vault")
if env_vocab_path:
self.full_vocab_path = os.path.abspath(env_vocab_path)
else:
self.full_vocab_path = os.path.abspath(
os.path.join(env_vault_root, "01_User_Manual", "01_edge_vocabulary.md")
)
self.unknown_log_path = "data/logs/unknown_edges.jsonl"
self.canonical_map: Dict[str, str] = {}
self.valid_types: Set[str] = set()
self._load_vocabulary()
self.initialized = True
def _load_vocabulary(self):
"""Parst die Markdown-Tabelle im Vault."""
print(f">>> CHECK: Loading Vocabulary from {self.full_vocab_path}", flush=True)
if not os.path.exists(self.full_vocab_path):
print(f"!!! [DICT-ERROR] File not found: {self.full_vocab_path} !!!", flush=True)
return
# WP-22 Precision Regex:
# Sucht nach | **`typ`** | oder | **typ** |
# Die Backticks `? sind jetzt optional enthalten.
pattern = re.compile(r"\|\s*\*\*`?([a-zA-Z0-9_-]+)`?\*\*\s*\|\s*([^|]+)\|")
try:
with open(self.full_vocab_path, "r", encoding="utf-8") as f:
c_types, c_aliases = 0, 0
for line in f:
match = pattern.search(line)
if match:
canonical = match.group(1).strip().lower()
aliases_str = match.group(2).strip()
self.valid_types.add(canonical)
self.canonical_map[canonical] = canonical
c_types += 1
if aliases_str and "Kein Alias" not in aliases_str:
# Aliase säubern (entfernt Backticks auch hier)
aliases = [a.strip() for a in aliases_str.split(",") if a.strip()]
for alias in aliases:
clean_alias = alias.replace("`", "").lower().strip().replace(" ", "_")
self.canonical_map[clean_alias] = canonical
c_aliases += 1
if c_types == 0:
print("!!! [DICT-WARN] Pattern mismatch! Ensure types are **`canonical`** or **canonical**. !!!", flush=True)
else:
print(f"=== [DICT-SUCCESS] Registered {c_types} Canonical Types and {c_aliases} Aliases ===", flush=True)
except Exception as e:
print(f"!!! [DICT-FATAL] Error reading file: {e} !!!", flush=True)
def resolve(self, edge_type: str) -> str:
"""Normalisiert Kanten-Typen via Registry oder loggt Unbekannte."""
if not edge_type: return "related_to"
clean_type = edge_type.lower().strip().replace(" ", "_").replace("-", "_")
if clean_type in self.canonical_map:
return self.canonical_map[clean_type]
self._log_unknown(clean_type)
return clean_type
def _log_unknown(self, edge_type: str):
try:
os.makedirs(os.path.dirname(self.unknown_log_path), exist_ok=True)
entry = {"unknown_type": edge_type, "status": "new"}
with open(self.unknown_log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry) + "\n")
except Exception: pass
registry = EdgeRegistry()

View File

@ -1,8 +1,8 @@
# config/decision_engine.yaml # config/decision_engine.yaml
# Steuerung der Decision Engine (Intent Recognition) # Steuerung der Decision Engine (Intent Recognition & Graph Routing)
# Version: 2.4.0 (Clean Architecture: Generic Intents only) # Version: 2.5.0 (WP-22: Semantic Graph Routing)
version: 1.4 version: 2.5
settings: settings:
llm_fallback_enabled: true llm_fallback_enabled: true
@ -37,6 +37,12 @@ strategies:
description: "Reine Wissensabfrage." description: "Reine Wissensabfrage."
trigger_keywords: [] trigger_keywords: []
inject_types: [] inject_types: []
# WP-22: Definitionen & Hierarchien bevorzugen
edge_boosts:
part_of: 2.0
composed_of: 2.0
similar_to: 1.5
caused_by: 0.5
prompt_template: "rag_template" prompt_template: "rag_template"
prepend_instruction: null prepend_instruction: null
@ -53,6 +59,12 @@ strategies:
- "abwägung" - "abwägung"
- "vergleich" - "vergleich"
inject_types: ["value", "principle", "goal", "risk"] inject_types: ["value", "principle", "goal", "risk"]
# WP-22: Risiken und Konsequenzen hervorheben
edge_boosts:
blocks: 2.5
solves: 2.0
depends_on: 1.5
risk_of: 2.5
prompt_template: "decision_template" prompt_template: "decision_template"
prepend_instruction: | prepend_instruction: |
!!! ENTSCHEIDUNGS-MODUS !!! !!! ENTSCHEIDUNGS-MODUS !!!
@ -71,6 +83,12 @@ strategies:
- "überfordert" - "überfordert"
- "müde" - "müde"
inject_types: ["experience", "belief", "profile"] inject_types: ["experience", "belief", "profile"]
# WP-22: Weiche Assoziationen & Erfahrungen stärken
edge_boosts:
based_on: 2.0
related_to: 2.0
experienced_in: 2.5
blocks: 0.1
prompt_template: "empathy_template" prompt_template: "empathy_template"
prepend_instruction: null prepend_instruction: null
@ -88,6 +106,11 @@ strategies:
- "yaml" - "yaml"
- "bash" - "bash"
inject_types: ["snippet", "reference", "source"] inject_types: ["snippet", "reference", "source"]
# WP-22: Technische Abhängigkeiten
edge_boosts:
uses: 2.5
depends_on: 2.0
implemented_in: 3.0
prompt_template: "technical_template" prompt_template: "technical_template"
prepend_instruction: null prepend_instruction: null
@ -108,9 +131,9 @@ strategies:
- "idee speichern" - "idee speichern"
- "draft" - "draft"
inject_types: [] inject_types: []
edge_boosts: {}
prompt_template: "interview_template" prompt_template: "interview_template"
prepend_instruction: null prepend_instruction: null
# Schemas: Hier nur der Fallback. # Schemas: Hier nur der Fallback.
# Spezifische Schemas (Project, Experience) kommen jetzt aus types.yaml! # Spezifische Schemas (Project, Experience) kommen jetzt aus types.yaml!
schemas: schemas:

View File

@ -2,43 +2,40 @@
doc_type: glossary doc_type: glossary
audience: all audience: all
status: active status: active
version: 2.6.0 version: 2.7.0
context: "Definitionen zentraler Begriffe und Entitäten im Mindnet-System." context: "Zentrales Glossar für Mindnet v2.7. Definitionen von Entitäten, WP-22 Scoring-Konzepten und der Edge Registry."
--- ---
# Mindnet Glossar # Mindnet Glossar
**Quellen:** `appendix.md`, `Overview.md` **Quellen:** `01_edge_vocabulary.md`, `retriever_scoring.py`, `edge_registry.py`
## Kern-Entitäten ## Kern-Entitäten
* **Note:** Repräsentiert eine Markdown-Datei. Die fachliche Haupteinheit. * **Note:** Repräsentiert eine Markdown-Datei. Die fachliche Haupteinheit. Verfügt über einen **Status** (stable, draft, system), der das Scoring beeinflusst.
* **Chunk:** Ein Textabschnitt einer Note. Die technische Sucheinheit (Vektor). Durch neue Strategien kann dies ein Fließtext-Abschnitt oder ein logisches Kapitel (Heading) sein. * **Chunk:** Ein Textabschnitt einer Note. Die technische Sucheinheit (Vektor).
* **Edge:** Eine gerichtete Verbindung zwischen zwei Knoten (Chunks oder Notes). * **Edge:** Eine gerichtete Verbindung zwischen zwei Knoten. Wird in WP-22 durch die Registry validiert.
* **Vault:** Der lokale Ordner mit den Markdown-Dateien (Source of Truth). * **Vault:** Der lokale Ordner mit den Markdown-Dateien (Source of Truth).
* **Frontmatter:** Der YAML-Header am Anfang einer Notiz (enthält `id`, `type`, `title`). * **Frontmatter:** Der YAML-Header am Anfang einer Notiz (enthält `id`, `type`, `title`, `status`).
## Komponenten ## Komponenten
* **Importer:** Das Python-Skript (`import_markdown.py`), das Markdown liest und in Qdrant schreibt. * **Edge Registry:** Der zentrale Dienst (SSOT), der Kanten-Typen validiert und Aliase in kanonische Typen auflöst. Nutzt `01_edge_vocabulary.md` als Basis.
* **Retriever:** Die Komponente, die sucht. Nutzt hybrides Scoring (Semantik + Graph). * **Retriever:** Besteht in v2.7 aus der Orchestrierung (`retriever.py`) und der mathematischen Scoring-Engine (`retriever_scoring.py`).
* **Decision Engine:** Teil des Routers, der entscheidet, wie auf eine Anfrage reagiert wird (z.B. Strategie wählen). * **Decision Engine:** Teil des Routers, der Intents erkennt und entsprechende **Boost-Faktoren** für das Retrieval injiziert.
* **Hybrid Router v5:** Die Logik, die erkennt, ob der User eine Frage stellt (`RAG`) oder einen Befehl gibt (`INTERVIEW`). * **Traffic Control:** Verwaltet Prioritäten und drosselt Hintergrund-Tasks (z.B. Smart Edges) mittels Semaphoren.
* **Draft Editor:** Die Web-UI-Komponente, in der generierte Notizen bearbeitet werden. * **Unknown Edges Log:** Die Datei `unknown_edges.jsonl`, in der das System Kanten-Typen protokolliert, die nicht im Dictionary gefunden wurden.
* **Traffic Control (WP15):** Ein Mechanismus im `LLMService`, der Prioritäten verwaltet (`realtime` für Chat vs. `background` für Import) und Hintergrund-Tasks mittels Semaphoren drosselt.
## Konzepte & Features ## Konzepte & Features
* **Active Intelligence:** Feature im Web-Editor, das während des Schreibens automatisch Links vorschlägt. * **Canonical Type:** Der standardisierte System-Name einer Kante (z.B. `based_on`), der in der Datenbank gespeichert wird.
* **Smart Edge Allocation (WP15):** Ein KI-Verfahren, das prüft, ob ein Link in einer Notiz für einen spezifischen Textabschnitt relevant ist, statt ihn blind allen Chunks zuzuordnen. * **Alias (Edge):** Ein nutzerfreundliches Synonym (z.B. `basiert_auf`), das während der Ingestion automatisch zum Canonical Type aufgelöst wird.
* **Strict Heading Split:** Chunking-Strategie, bei der Überschriften (z.B. H2) als harte Grenzen dienen. Verhindert das Vermischen von Themen (z.B. zwei unterschiedliche Rollen in einem Chunk). Besitzt ein "Safety Net" für zu lange Abschnitte. * **Lifecycle Scoring (WP-22):** Ein Mechanismus, der die Relevanz einer Notiz basierend auf ihrem Status gewichtet (z.B. Bonus für `stable`, Malus für `draft`).
* **Soft Heading Split:** Chunking-Strategie, die Überschriften respektiert, aber kleine Abschnitte zusammenfasst, um Vektor-Kontext zu füllen ("Fuller Chunks"). * **Intent Boosting:** Dynamische Erhöhung der Kanten-Gewichte basierend auf der Nutzerfrage (z.B. Fokus auf `caused_by` bei "Warum"-Fragen).
* **Healing Parser:** UI-Funktion, die fehlerhaften Output des LLMs (z.B. defektes YAML) automatisch repariert. * **Provenance Weighting:** Gewichtung einer Kante nach ihrer Herkunft:
* **Explanation Layer:** Die Schicht, die dem Nutzer erklärt, *warum* ein Suchergebnis gefunden wurde (z.B. "Weil Projekt X davon abhängt"). * `explicit`: Vom Mensch gesetzt (Prio 1).
* **Provenance:** Die Herkunft einer Kante. * `smart`: Von der KI validiert (Prio 2).
* `explicit`: Vom Mensch geschrieben. * `rule`: Durch System-Regeln/Matrix erzeugt (Prio 3).
* `smart`: Vom LLM validiert. * **Smart Edge Allocation:** KI-Verfahren zur Relevanzprüfung von Links für spezifische Textabschnitte.
* `rule`: Durch Config-Regel erzeugt. * **Strict Heading Split:** Chunking-Strategie mit harten Grenzen an Überschriften und integriertem "Safety Net" gegen zu große Chunks.
* **Matrix Logic:** Regelwerk, das den Typ einer Kante basierend auf Quell- und Ziel-Typ bestimmt (z.B. Erfahrung -> Wert = `based_on`). * **Matrix Logic:** Bestimmung des Kanten-Typs basierend auf Quell- und Ziel-Entität (z.B. Erfahrung -> Wert = `based_on`).
* **Idempotenz:** Die Eigenschaft des Importers, bei mehrfacher Ausführung dasselbe Ergebnis zu liefern ohne Duplikate.
* **Resurrection Pattern:** UI-Technik, um User-Eingaben beim Tab-Wechsel zu erhalten.

View File

@ -1,89 +0,0 @@
---
doc_type: reference
audience: author
status: active
context: "Zentrales Wörterbuch für Kanten-Bezeichner (Edges). Referenz für Autoren zur Wahl des richtigen Verbindungstyps."
---
# Edge Vocabulary & Semantik
**Standort:** `01_User_Manual/01_edge_vocabulary.md`
**Zweck:** Damit Mindnet "verstehen" kann, was eine Verbindung bedeutet (z.B. für "Warum"-Fragen), nutzen wir konsistente Begriffe.
**Die Goldene Regel:**
Nutze bevorzugt den **System-Typ** (linke Spalte). Wenn dieser sprachlich nicht passt, nutze einen der **erlaubten Aliasse** (und bleibe dabei konsistent).
---
## 1. Kausalität & Logik ("Warum?")
*Verbindungen, die Gründe, Ursachen oder Herleitungen beschreiben.*
| System-Typ (Canonical) | Erlaubte Aliasse (User) | Wann nutzen? |
| :--- | :--- | :--- |
| **`caused_by`** | `ausgelöst_durch`, `wegen`, `ursache_ist` | Wenn A der direkte Auslöser für B ist. (Technisch/Faktisch) |
| **`derived_from`** | `abgeleitet_von`, `quelle`, `inspiriert_durch` | Wenn eine Idee/Prinzip aus einer Quelle stammt (z.B. Buch, Zitat). |
| **`based_on`** | `basiert_auf`, `fundament`, `grundlage` | Wenn B nicht existieren könnte ohne das fundamentale Konzept A (z.B. Werte). |
| **`solves`** | `löst`, `beantwortet`, `fix_für` | Wenn A eine Lösung für das Problem B ist. |
## 2. Struktur & Hierarchie ("Wo gehört es hin?")
*Verbindungen, die Ordnung schaffen.*
| System-Typ (Canonical) | Erlaubte Aliasse (User) | Wann nutzen? |
| :--- | :--- | :--- |
| **`part_of`** | `teil_von`, `gehört_zu`, `cluster` | Harte Hierarchie. Projekt A ist Teil von Programm B. |
| **`belongs_to`** | *(System-Intern)* | Automatisch generiert (Chunk -> Note). Nicht manuell nutzen. |
## 3. Abhängigkeit & Einfluss ("Was brauche ich?")
*Verbindungen, die Blockaden oder Voraussetzungen definieren.*
| System-Typ (Canonical) | Erlaubte Aliasse (User) | Wann nutzen? |
| :--- | :--- | :--- |
| **`depends_on`** | `hängt_ab_von`, `braucht`, `requires`, `enforced_by` | Harte Abhängigkeit. Ohne A kann B nicht starten/existieren. |
| **`blocks`** | `blockiert`, `verhindert`, `risiko_für` | Wenn A ein aktives Hindernis für B ist. |
| **`uses`** | `nutzt`, `verwendet`, `tool` | Wenn A ein Werkzeug/Methode B einsetzt (z.B. Projekt nutzt Python). |
| **`guides`** | `steuert`, `leitet`, `orientierung` | (Alias für `depends_on` oder `related_to`) Weiche Abhängigkeit, z.B. Prinzipien steuern Verhalten. |
## 4. Zeit & Prozess ("Was kommt dann?")
*Verbindungen für Abläufe zwischen verschiedenen Notizen.*
| System-Typ (Canonical) | Erlaubte Aliasse (User) | Wann nutzen? |
| :--- | :--- | :--- |
| **`next`** | `danach`, `folgt`, `nachfolger`, `followed_by` | Manuell: Wenn Notiz A logisch vor Notiz B kommt (Prozesskette: A -> B). |
| **`prev`** | `davor`, `vorgänger`, `preceded_by` | Manuell: Der vorherige Prozessschritt (B -> A). |
## 5. Assoziation ("Was ist ähnlich?")
*Lose Verbindungen für Entdeckungen.*
| System-Typ (Canonical) | Erlaubte Aliasse (User) | Wann nutzen? |
| :--- | :--- | :--- |
| **`related_to`** | `siehe_auch`, `kontext`, `thematisch` | Wenn zwei Dinge etwas miteinander zu tun haben, ohne strikte Logik. |
| **`similar_to`** | `ähnlich_wie`, `vergleichbar` | Wenn A und B fast das Gleiche sind (z.B. Synonyme, Alternativen). |
| **`references`** | *(Kein Alias)* | Standard für "erwähnt einfach nur". (Niedrigste Prio). |
---
## Best Practices für Autoren
### A. Der "Callout"-Standard (Footer)
Sammle Verbindungen, die für die **ganze Notiz** gelten, am Ende der Datei in einem Bereich `## Kontext & Verbindungen`.
```markdown
## Kontext & Verbindungen
> [!edge] derived_from
> [[Buch der Weisen]]
> [!edge] part_of
> [[Leitbild Identity Core]]
```
### B. Der "Inline"-Standard (Präzision)
Nutze Inline-Kanten nur, wenn du im Fließtext eine **spezifische Aussage** triffst, die im Graph sichtbar sein soll.
* *Schlecht:* "Das [[rel:related_to Projekt]] ist wichtig." (Wenig Aussagekraft)
* *Gut:* "Dieser Fehler wird [[rel:caused_by Systemabsturz]] verursacht." (Starke Kausalität)
### C. Konsistenz-Check
Bevor du ein neues Wort (z.B. `ermöglicht`) nutzt:
1. Prüfe diese Liste: Passt `solves` oder `depends_on`?
2. Falls nein: Schreibe `ermöglicht` in die Spalte "Erlaubte Aliasse" oben und ordne es einem System-Typ zu.

View File

@ -3,13 +3,13 @@ doc_type: concept
audience: architect, product_owner audience: architect, product_owner
scope: graph, logic, provenance scope: graph, logic, provenance
status: active status: active
version: 2.6 version: 2.7.0
context: "Fachliche Beschreibung des Wissensgraphen: Knoten, Kanten, Provenance und Matrix-Logik." context: "Fachliche Beschreibung des Wissensgraphen: Knoten, Kanten, Provenance, Matrix-Logik und WP-22 Scoring-Prinzipien."
--- ---
# Konzept: Die Graph-Logik # Konzept: Die Graph-Logik
**Quellen:** `mindnet_functional_architecture.md`, `chunking_strategy.md` **Quellen:** `mindnet_functional_architecture.md`, `chunking_strategy.md`, `01_edge_vocabulary.md`, `retriever_scoring.py`, `03_tech_retrieval_scoring.md`
Mindnet ist keine reine Dokumentablage, sondern ein **semantischer Graph**. Dieses Dokument beschreibt, wie aus statischen Textdateien ein vernetztes Wissensobjekt wird. Mindnet ist keine reine Dokumentablage, sondern ein **semantischer Graph**. Dieses Dokument beschreibt, wie aus statischen Textdateien ein vernetztes Wissensobjekt wird.
@ -19,7 +19,7 @@ Der Graph besteht aus zwei Ebenen von Knoten.
### 1.1 Die Note (Das Fachobjekt) ### 1.1 Die Note (Das Fachobjekt)
Eine Note repräsentiert ein atomares Konzept (z.B. "Projekt Alpha"). Eine Note repräsentiert ein atomares Konzept (z.B. "Projekt Alpha").
* **Eigenschaften:** Titel, Typ, Tags, Erstellungsdatum. * **Eigenschaften:** Titel, Typ, Tags, Erstellungsdatum sowie der **Status** (Lifecycle).
* **Rolle:** Sie ist der Container für Metadaten und steuert via `type`, wie der Inhalt verarbeitet wird (siehe `types.yaml`). * **Rolle:** Sie ist der Container für Metadaten und steuert via `type`, wie der Inhalt verarbeitet wird (siehe `types.yaml`).
* **Identität:** Definiert durch eine deterministische UUIDv5. * **Identität:** Definiert durch eine deterministische UUIDv5.
@ -29,71 +29,97 @@ Da LLMs (Large Language Models) nicht unendlich viel Text auf einmal lesen könn
* **Rolle:** Dies sind die eigentlichen Treffer bei einer Suche. * **Rolle:** Dies sind die eigentlichen Treffer bei einer Suche.
* **Hierarchie:** Jeder Chunk gehört streng zu einer Note (`belongs_to`). * **Hierarchie:** Jeder Chunk gehört streng zu einer Note (`belongs_to`).
### 1.3 Content Lifecycle & Status (WP-22)
Seit v2.7 beeinflusst der `status` einer Note direkt deren Gewichtung im Retrieval (Lifecycle Scoring).
* **Stable:** Notizen mit `status: stable` erhalten einen positiven Modifier (Standard: 1.0), da sie geprüftes Wissen repräsentieren.
* **Draft:** Notizen mit `status: draft` werden durch einen Malus (Standard: 0.5 - 0.8) herabgestuft, um "Rauschen" durch unfertige Gedanken zu reduzieren.
* **System/Template:** Diese Status-Typen führen zu einem vollständigen Ausschluss aus dem Index.
--- ---
## 2. Die Kanten (Edges) ## 2. Die Kanten (Edges)
Kanten machen aus isolierten Informationen Wissen. Mindnet nutzt gerichtete Kanten mit semantischer Bedeutung. Kanten machen aus isolierten Informationen Wissen. Mindnet nutzt gerichtete Kanten mit semantischer Bedeutung, die durch die **Edge Registry** validiert werden.
### 2.1 Semantische Typen ### 2.1 Kanonische Typen & Aliase
Um eine konsistente mathematische Gewichtung zu garantieren, werden alle Kanten auf **kanonische Typen** normalisiert.
| Kanten-Typ | Frage, die er beantwortet | Beispiel | | Kanonischer Typ | Aliase (Beispiele) | Bedeutung |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| `references` | Worüber wird gesprochen? | Text erwähnt "Python". | | `caused_by` | `ausgelöst_durch`, `wegen` | Kausalität: A löst B aus. |
| `depends_on` | Was ist Voraussetzung? | Projekt braucht "Budget". | | `derived_from` | `abgeleitet_von`, `quelle` | Herkunft: A stammt von B. |
| `caused_by` | Warum ist das passiert? | Bug durch "Commit X". | | `based_on` | `basiert_auf`, `fundament` | Fundament: B baut auf A auf. |
| `blocks` | Was steht im Weg? | Risiko blockiert "Release". | | `solves` | `löst`, `fix_für` | Lösung: A ist Lösung für Problem B. |
| `based_on` | Worauf fußt das? | Erfahrung basiert auf "Wert Y". | | `part_of` | `teil_von`, `cluster` | Hierarchie: Kind -> Eltern. |
| `derived_from` | Woher kommt das? | Prinzip stammt aus "Buch Z". | | `depends_on` | `braucht`, `requires` | Abhängigkeit: A braucht B. |
| `related_to` | Was ist ähnlich? | "Hund" ist verwandt mit "Wolf". | | `blocks` | `blockiert`, `risiko_für` | Blocker: A verhindert B. |
| `solves` | Was ist die Lösung? | "Qdrant" löst "Vektorsuche". | | `related_to` | `siehe_auch`, `kontext` | Lose Assoziation. |
### 2.2 Provenance (Herkunft & Vertrauen) ### 2.2 Provenance (Herkunft & Vertrauen)
Nicht alle Kanten sind gleich viel wert. Mindnet unterscheidet in v2.6 drei Qualitätsstufen (**Provenance**), um Konflikte aufzulösen. Nicht alle Kanten sind gleich viel wert. Mindnet unterscheidet drei Qualitätsstufen (**Provenance**), um bei der Berechnung des Edge-Bonus Prioritäten zu setzen.
**1. Explicit (Der Mensch hat es gesagt)** **1. Explicit (Der Mensch hat es gesagt)**
* *Quelle:* Inline-Links (`[[rel:...]]`) oder Wikilinks im Text. * *Quelle:* Inline-Links (`[[rel:...]]`) oder Wikilinks im Text.
* *Vertrauen:* **Hoch (1.0)**. * *Vertrauen:* **Hoch (1.0)**.
* *Bedeutung:* Dies ist hartes Faktenwissen. "Ich habe diesen Link bewusst gesetzt." * *Bedeutung:* Hartes Faktenwissen. Die explizite menschliche Intention wird im Scoring am stärksten gewichtet.
**2. Smart (Die KI hat es bestätigt)** **2. Smart (Die KI hat es bestätigt)**
* *Quelle:* Smart Edge Allocation (WP15). * *Quelle:* Smart Edge Allocation (WP15).
* *Vertrauen:* **Mittel (0.9)**. * *Vertrauen:* **Mittel (0.9)**.
* *Bedeutung:* Ein LLM hat geprüft, ob der Link im Kontext dieses Textabschnitts wirklich relevant ist. Dies filtert "Rauschen" heraus (z.B. Links im Footer, die nichts mit dem Absatz zu tun haben). * *Bedeutung:* Ein LLM hat die Relevanz des Links im Kontext geprüft. Dies filtert irrelevante Links (z.B. aus Navigationsleisten) heraus.
**3. Rule (Die Regel hat es vermutet)** **3. Rule (Die Regel hat es vermutet)**
* *Quelle:* `types.yaml` Defaults. * *Quelle:* `types.yaml` Defaults oder Matrix-Logik.
* *Vertrauen:* **Niedrig (0.7)**. * *Vertrauen:* **Niedrig (0.7)**.
* *Bedeutung:* Eine Heuristik. "Weil es ein Projekt ist, nehmen wir an, dass es von allen erwähnten Technologien abhängt." * *Bedeutung:* Systemseitige Heuristik. Diese Verbindungen dienen der Entdeckung neuer Pfade, haben aber weniger Gewicht als explizite Links.
--- ---
## 3. Matrix-Logik (Kontext-Sensitivität) ## 3. Matrix-Logik (Kontext-Sensitivität)
Mit WP11 wurde eine Intelligenz eingeführt, die Kanten-Typen nicht nur anhand des Quell-Typs, sondern auch anhand des Ziel-Typs bestimmt ("Matrix"). Die Matrix-Logik bestimmt Kanten-Typen dynamisch anhand der Quell- und Ziel-Typen der verknüpften Notizen.
**Logik-Beispiele:** **Logik-Beispiele:**
* **Quelle `experience` → Ziel `value`**: Wird zu `based_on` (Erfahrungen fußen auf Werten).
* **Quelle `experience` → Ziel `value`** * **Quelle `principle` → Ziel `source`**: Wird zu `derived_from` (Prinzipien stammen aus Quellen).
* *Standard:* `references`
* *Matrix:* `based_on` (Erfahrungen basieren auf Werten).
* **Quelle `principle` → Ziel `source` (Buch)**
* *Standard:* `references`
* *Matrix:* `derived_from` (Prinzipien stammen aus Quellen).
* **Quelle `project` → Ziel `tool`**
* *Standard:* `references`
* *Matrix:* `uses` (Projekte nutzen Tools).
*Nutzen:* Dies erlaubt im Chat präzise Fragen wie: *"Auf welchen Werten basiert diese Entscheidung?"* (Suche nach eingehenden `based_on` Kanten).
--- ---
## 4. Idempotenz & Konsistenz ## 4. Semantisch bezogene Booster & Scoring-Logik
Das Scoring in Mindnet v2.7 folgt einer hybriden Logik, bei der semantische Ähnlichkeit durch kontextuelle Faktoren modifiziert wird.
### 4.1 Semantische Modifikatoren (Type & Status)
Bevor der Graph-Bonus berechnet wird, durchläuft die semantische Ähnlichkeit (Cosine Similarity) zwei Filter:
1. **Type-Weight ($W_{type}$):** Bestimmt die "Wichtigkeit" einer Notizklasse. Ein Wert von 1.0 (z. B. für `decision`) lässt die volle semantische Stärke durch. Ein Wert von 0.4 (z. B. für `glossary`) dämpft den Treffer massiv ab, es sei denn, die Frage passt exakt auf den Text.
2. **Status-Modifier:** Agiert als Qualitätsfilter. `draft` markierte Inhalte werden herabgestuft, damit `stable` Inhalte bei gleicher semantischer Passung immer oben stehen.
### 4.2 Graph-Boosting (Additive Boni)
Der Graph-Bonus wird auf den modifizierten semantischen Score addiert. Dies ermöglicht es Inhalten, im Ranking nach oben zu steigen, wenn sie stark mit anderen relevanten Treffern vernetzt sind.
| Parameter-Gruppe | Wertebereich | Logik |
| :--- | :--- | :--- |
| **Notiz-Gewichte** | 0.1 - 1.0 | Multiplikativ. Dient als Relevanz-Filter für Informationstypen. |
| **Kanten-Boosts** | 0.1 - 3.0+ | Additiv. Hebt vernetztes Wissen über isolierte Texttreffer. |
| **Status-Malus** | 0.5 - 0.9 | Multiplikativ. Bestraft unfertiges Wissen (Drafts). |
---
## 5. Dynamic Intent Boosting (WP-22)
In v2.7 ist die Graph-Gewichtung nicht mehr statisch, sondern hängt vom **Intent** der Nutzerfrage ab (Query-Time Boosting).
Der Intent-Router injiziert spezifische Multiplikatoren für kanonische Typen:
* **Intent `EMPATHY`**: Boostet `based_on` (Fokus auf Werte).
* **Intent `WHY`**: Boostet `caused_by` (Fokus auf Ursachenforschung).
---
## 6. Idempotenz & Konsistenz
Das System garantiert fachliche Konsistenz auch bei mehrfachen Importen. Das System garantiert fachliche Konsistenz auch bei mehrfachen Importen.
* **Stabile IDs:** Importiert man dieselbe Datei zweimal, ändern sich die IDs der Knoten nicht. * **Stabile IDs:** Deterministische IDs verhindern Duplikate bei Re-Imports.
* **Keine Duplikate:** Kanten werden dedupliziert. Die "stärkere" Quelle (Explicit > Smart > Rule) gewinnt. * **Deduplizierung:** Kanten werden anhand ihrer Identität erkannt. Die "stärkere" Provenance gewinnt.
* **Lösch-Garantie:** Wenn eine Notiz gelöscht wird, verschwinden auch alle ihre Chunks und ausgehenden Kanten (via `--sync-deletes`).

View File

@ -1,19 +1,19 @@
--- ---
doc_type: technical_reference doc_type: technical_reference
audience: developer, admin audience: developer, admin
scope: configuration, env scope: configuration, env, registry, scoring
status: active status: active
version: 2.7.0 version: 2.7.2
context: "Referenztabellen für Umgebungsvariablen und YAML-Konfigurationen." context: "Umfassende Referenztabellen für Umgebungsvariablen, YAML-Konfigurationen und die Edge Registry Struktur."
--- ---
# Konfigurations-Referenz # Konfigurations-Referenz
Dieses Dokument beschreibt die Steuerungsdateien von Mindnet. Dieses Dokument beschreibt alle Steuerungsdateien von Mindnet. In der Version 2.7 wurde die Konfiguration professionalisiert, um die Edge Registry und dynamische Scoring-Parameter (Lifecycle & Intent) zu unterstützen.
## 1. Environment Variablen (`.env`) ## 1. Environment Variablen (`.env`)
Diese Variablen steuern die Infrastruktur, Timeouts und Feature-Flags. Diese Variablen steuern die Infrastruktur, Pfade und globale Timeouts.
| Variable | Default | Beschreibung | | Variable | Default | Beschreibung |
| :--- | :--- | :--- | | :--- | :--- | :--- |
@ -21,6 +21,8 @@ Diese Variablen steuern die Infrastruktur, Timeouts und Feature-Flags.
| `QDRANT_API_KEY` | *(leer)* | Optionaler Key für Absicherung. | | `QDRANT_API_KEY` | *(leer)* | Optionaler Key für Absicherung. |
| `COLLECTION_PREFIX` | `mindnet` | Namensraum für Collections (erzeugt `{prefix}_notes` etc). | | `COLLECTION_PREFIX` | `mindnet` | Namensraum für Collections (erzeugt `{prefix}_notes` etc). |
| `VECTOR_DIM` | `768` | **Muss 768 sein** (für Nomic Embeddings). | | `VECTOR_DIM` | `768` | **Muss 768 sein** (für Nomic Embeddings). |
| `MINDNET_VOCAB_PATH` | *(Pfad)* | **Neu (WP-22):** Absoluter Pfad zur `01_edge_vocabulary.md`. Definiert den Ort des Dictionarys. |
| `MINDNET_VAULT_ROOT` | `./vault` | Basis-Pfad für Datei-Operationen. Dient als Fallback-Basis, falls `MINDNET_VOCAB_PATH` nicht gesetzt ist. |
| `MINDNET_TYPES_FILE` | `config/types.yaml` | Pfad zur Typ-Registry. | | `MINDNET_TYPES_FILE` | `config/types.yaml` | Pfad zur Typ-Registry. |
| `MINDNET_RETRIEVER_CONFIG`| `config/retriever.yaml`| Pfad zur Scoring-Konfiguration. | | `MINDNET_RETRIEVER_CONFIG`| `config/retriever.yaml`| Pfad zur Scoring-Konfiguration. |
| `MINDNET_DECISION_CONFIG` | `config/decision_engine.yaml` | Pfad zur Router & Intent Config. | | `MINDNET_DECISION_CONFIG` | `config/decision_engine.yaml` | Pfad zur Router & Intent Config. |
@ -30,9 +32,8 @@ Diese Variablen steuern die Infrastruktur, Timeouts und Feature-Flags.
| `MINDNET_OLLAMA_URL` | `http://127.0.0.1:11434`| URL zum LLM-Server. | | `MINDNET_OLLAMA_URL` | `http://127.0.0.1:11434`| URL zum LLM-Server. |
| `MINDNET_LLM_TIMEOUT` | `300.0` | Timeout in Sekunden (Erhöht für CPU Cold-Starts). | | `MINDNET_LLM_TIMEOUT` | `300.0` | Timeout in Sekunden (Erhöht für CPU Cold-Starts). |
| `MINDNET_API_TIMEOUT` | `300.0` | Frontend Timeout (Erhöht für Smart Edge Wartezeiten). | | `MINDNET_API_TIMEOUT` | `300.0` | Frontend Timeout (Erhöht für Smart Edge Wartezeiten). |
| `MINDNET_LLM_BACKGROUND_LIMIT`| `2` | **Traffic Control (Neu):** Max. parallele Import-Tasks (Semaphore). | | `MINDNET_LLM_BACKGROUND_LIMIT`| `2` | **Traffic Control:** Max. parallele Hintergrund-Tasks (Semaphore). |
| `MINDNET_VAULT_ROOT` | `./vault` | Pfad für Write-Back Operationen (Drafts). | | `MINDNET_CHANGE_DETECTION_MODE` | `full` | `full` (Text + Meta) oder `body` (nur Text). |
| `MINDNET_CHANGE_DETECTION_MODE` | `full` | **Change Detection (Neu):** `full` (Text + Meta) oder `body` (nur Text). |
--- ---
@ -42,102 +43,119 @@ Steuert das Import-Verhalten, Chunking und die Kanten-Logik pro Typ.
### 2.1 Konfigurations-Hierarchie (Override-Logik) ### 2.1 Konfigurations-Hierarchie (Override-Logik)
Seit Version 2.7.0 gilt für `chunking_profile` und `retriever_weight` folgende Priorität: Seit Version 2.7.0 gilt für `chunking_profile` und `retriever_weight` folgende Priorität:
1. **Frontmatter (Höchste Prio):** Ein Wert direkt in der Markdown-Datei überschreibt alles. 1. **Frontmatter (Höchste Prio):** Ein Wert direkt in der Markdown-Datei überschreibt alles.
* *Beispiel:* `chunking_profile: structured_smart_edges_strict` im Header einer Notiz erzwingt diesen Splitter, egal welcher Typ eingestellt ist. 2. **Type Config:** Der Standardwert für den `type` aus `types.yaml`.
2. **Type Config:** Der Standardwert für den `type` (z.B. `concept`) aus `types.yaml`.
3. **Global Default:** Fallback aus `defaults` in `types.yaml`. 3. **Global Default:** Fallback aus `defaults` in `types.yaml`.
### 2.2 Typ-Referenztabelle ### 2.2 Typ-Referenztabelle (Vollständig)
| Typ (`type`) | Chunk Profile (Standard) | Retriever Weight | Smart Edges? | Beschreibung | | Typ (`type`) | Chunk Profile (Standard) | Retriever Weight ($W_{type}$) | Smart Edges? | Beschreibung |
| :--- | :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- | :--- |
| **concept** | `sliding_smart_edges` | 0.60 | Ja | Abstrakte Begriffe. | | **decision** | `structured_strict` | 1.00 | Ja | Entscheidungen. Atomar. |
| **project** | `sliding_smart_edges` | 0.97 | Ja | Aktive Vorhaben. | | **value** | `structured_strict` | 1.00 | Ja | Werte/Prinzipien. Atomar. |
| **decision** | `structured_smart_edges_strict` | 1.00 | Ja | Entscheidungen (ADRs). Atomar. | | **project** | `sliding_smart` | 0.97 | Ja | Aktive Vorhaben. |
| **experience** | `sliding_smart_edges` | 0.90 | Ja | Persönliche Learnings. | | **experience** | `sliding_smart` | 0.90 | Ja | Persönliche Learnings. |
| **journal** | `sliding_standard` | 0.80 | Nein | Logs / Dailies. | | **concept** | `sliding_smart` | 0.60 | Ja | Abstrakte Begriffe. |
| **value** | `structured_smart_edges_strict` | 1.00 | Ja | Werte/Prinzipien. Atomar. | | **principle** | `structured_L3` | 0.95 | Nein | Prinzipien (Tiefer Split). |
| **belief** | `sliding_short` | 0.90 | Nein | Glaubenssätze. |
| **risk** | `sliding_short` | 0.90 | Nein | Risiken. | | **risk** | `sliding_short` | 0.90 | Nein | Risiken. |
| **journal** | `sliding_standard` | 0.80 | Nein | Logs / Dailies. |
| **person** | `sliding_standard` | 0.50 | Nein | Profile. | | **person** | `sliding_standard` | 0.50 | Nein | Profile. |
| **source** | `sliding_standard` | 0.50 | Nein | Externe Quellen. | | **source** | `sliding_standard` | 0.50 | Nein | Externe Quellen. |
| **event** | `sliding_standard` | 0.60 | Nein | Meetings. |
| **goal** | `sliding_smart_edges` | 0.95 | Nein | Strategische Ziele. |
| **belief** | `sliding_short` | 0.90 | Nein | Glaubenssätze. |
| **profile** | `structured_smart_edges_strict` | 0.70 | Nein | Rollenprofile. Strict Split. |
| **principle** | `structured_smart_edges_strict_L3`| 0.95 | Nein | Prinzipien. Tiefer Split (H3) für Mikro-Prinzipien. |
| **task** | `sliding_short` | 0.80 | Nein | Aufgaben. |
| **glossary** | `sliding_short` | 0.40 | Nein | Begriffsdefinitionen. | | **glossary** | `sliding_short` | 0.40 | Nein | Begriffsdefinitionen. |
| **default** | `sliding_standard` | 1.00 | Nein | Fallback. | | **default** | `sliding_standard` | 1.00 | Nein | Fallback für alle anderen. |
*Hinweis: `Smart Edges?` entspricht dem YAML-Key `enable_smart_edge_allocation: true`.* *Richtwert für $W_{type}$: 0.1 (minimale Relevanz/Filter) bis 1.0 (maximale Relevanz).*
--- ---
## 3. Retriever Config (`retriever.yaml`) ## 3. Retriever Config (`retriever.yaml`)
Steuert die Gewichtung der Scoring-Formel (WP04a). Steuert die Gewichtung der Scoring-Formel und die neuen Lifecycle-Modifier.
**Beispielkonfiguration:**
```yaml ```yaml
scoring: scoring:
semantic_weight: 1.0 # Basis-Relevanz (Cosine Similarity) semantic_weight: 1.0 # Basis-Relevanz (Cosine Similarity)
edge_weight: 0.7 # Einfluss des Graphen (Bonus) edge_weight: 0.7 # Einfluss des Graphen (Bonus)
centrality_weight: 0.5 # Einfluss von Hubs centrality_weight: 0.5 # Einfluss von Hubs (Zentralität)
# WP-22 Lifecycle Modifier (Multiplikativ auf Semantik)
lifecycle_weights:
stable: 1.2 # Bonus: Geprüftes Wissen wird 20% höher gewichtet
draft: 0.5 # Malus: Entwürfe werden auf 50% gedämpft
system: 0.0 # Ausschluss aus dem Index
# Kanten-spezifische Basis-Gewichtung (Ohne Intent-Boost)
edge_weights: edge_weights:
# Multiplikatoren für den Edge-Bonus
depends_on: 1.5 # Harte Abhängigkeiten stark gewichten depends_on: 1.5 # Harte Abhängigkeiten stark gewichten
blocks: 1.5 # Risiken stark gewichten blocks: 1.5 # Blocker/Risiken stark gewichten
caused_by: 1.2 # Kausalitäten stärken caused_by: 1.2 # Kausalitäten moderat stärken
related_to: 0.5 # Weiche Themen schwächer gewichten based_on: 1.3 # Werte-Bezug stärken
related_to: 0.5 # Weiche Assoziation schwächen
references: 0.8 # Standard-Referenzen references: 0.8 # Standard-Referenzen
based_on: 1.3 # Werte-Bezug
``` ```
--- ---
## 4. Edge Typen Referenz ## 4. Edge Typen & Registry Referenz
Definiert die Semantik der `kind`-Felder in der Edge-Collection. Die `EdgeRegistry` ist die **Single Source of Truth** für das Vokabular.
| Kind | Symmetrisch? | Herkunft (Primär) | Bedeutung | ### 4.1 Dateistruktur & Speicherort
Die Registry erwartet eine Markdown-Datei an folgendem Ort:
* **Standard-Pfad:** `<MINDNET_VAULT_ROOT>/01_User_Manual/01_edge_vocabulary.md`.
* **Custom-Pfad:** Kann via `.env` Variable `MINDNET_VOCAB_PATH` überschrieben werden.
### 4.2 Aufbau des Dictionaries (Markdown-Schema)
Die Datei muss eine Markdown-Tabelle enthalten, die vom Regex-Parser gelesen wird.
**Erwartetes Format:**
```markdown
| System-Typ (Canonical) | Erlaubte Aliasse (User) | Beschreibung |
| :--- | :--- | :--- |
| **`based_on`** | `basiert_auf`, `fundament` | Fundament: B baut auf A auf. |
| **`caused_by`** | `ausgelöst_durch`, `wegen` | Kausalität: A löst B aus. |
```
**Regeln für die Spalten:**
1. **Canonical:** Muss fett gedruckt sein (`**type**` oder `**`type`**`). Dies ist der Wert, der in der DB landet.
2. **Aliasse:** Kommagetrennte Liste von Synonymen. Diese werden beim Import automatisch zum Canonical aufgelöst.
3. **Beschreibung:** Rein informativ für den Nutzer.
### 4.3 Verfügbare Kanten-Typen (System-Standard)
| Kind (Canonical) | Symmetrisch? | Herkunft | Bedeutung |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
| `references` | Nein | Wikilink | Standard-Verweis. "Erwähnt X". | | `references` | Nein | Wikilink | Standard-Verweis ("Erwähnt X"). |
| `belongs_to` | Nein | Struktur | Chunk gehört zu Note. | | `belongs_to` | Nein | Struktur | Chunk gehört zu Note. |
| `next` / `prev` | Ja | Struktur | Lesereihenfolge. | | `caused_by` | Nein | Inline | Kausalität: A löst B aus. |
| `depends_on` | Nein | Inline / Default | Harte Abhängigkeit. | | `based_on` | Nein | Matrix | Fundament: A fußt auf B. |
| `related_to` | Ja | Callout / Default | Thematische Nähe. | | `blocks` | Nein | Inline | Blocker: A verhindert B. |
| `similar_to` | Ja | Inline | Inhaltliche Ähnlichkeit. | | `solves` | Nein | Inline | Lösung: A ist Lösung für Problem B. |
| `caused_by` | Nein | Inline | Kausalität. | | `next` / `prev` | Ja | Struktur | Sequenzielle Lesereihenfolge. |
| `solves` | Nein | Inline | Lösung für ein Problem. |
| `blocks` | Nein | Inline | Blockade / Risiko. |
| `derived_from` | Nein | Matrix | Herkunft (z.B. Prinzip -> Buch). |
| `based_on` | Nein | Matrix | Fundament (z.B. Erfahrung -> Wert). |
| `uses` | Nein | Matrix | Nutzung (z.B. Projekt -> Tool). |
--- ---
## 5. Decision Engine (`decision_engine.yaml`) ## 5. Decision Engine (`decision_engine.yaml`)
Steuert den Hybrid Router (WP06). Definiert, welche Keywords welche Strategie auslösen. Steuert den Hybrid Router und das dynamische Intent-Boosting (WP-22).
**Beispielauszug:** **Beispielauszug für Intent-Boosting:**
```yaml ```yaml
# Strategie-Definitionen intents:
strategies:
DECISION:
description: "Hilfe bei Entscheidungen"
inject_types: ["value", "principle", "goal", "risk"] # Strategic Retrieval
llm_fallback_enabled: true
INTERVIEW:
description: "Wissenserfassung"
trigger_keywords: ["erstellen", "neu", "anlegen", "festhalten"]
EMPATHY: EMPATHY:
description: "Emotionaler Support" description: "Emotionaler Support / Werte-Fokus"
boost_edges:
based_on: 2.0 # Verdoppelt den Einfluss von Werte-Kanten
related_to: 1.5 # Stärkt assoziative Bezüge
inject_types: ["experience", "belief"] inject_types: ["experience", "belief"]
WHY:
description: "Ursachenanalyse (Kausalität)"
boost_edges:
caused_by: 2.5 # Massiver Boost für Kausalitätsketten
derived_from: 1.8 # Fokus auf die Herkunft
``` ```
*Richtwert für Kanten-Boosts: 0.1 (Abwertung) bis 3.0+ (Dominanz gegenüber Text-Match).*

View File

@ -1,19 +1,19 @@
--- ---
doc_type: technical_reference doc_type: technical_reference
audience: developer, devops audience: developer, devops
scope: backend, ingestion, smart_edges scope: backend, ingestion, smart_edges, edge_registry
status: active status: active
version: 2.7.0 version: 2.7.1
context: "Detaillierte technische Beschreibung der Import-Pipeline, Chunking-Strategien und CLI-Befehle." context: "Detaillierte technische Beschreibung der Import-Pipeline, Chunking-Strategien und CLI-Befehle."
--- ---
# Ingestion Pipeline & Smart Processing # Ingestion Pipeline & Smart Processing
**Quellen:** `pipeline_playbook.md`, `Handbuch.md` **Quellen:** `pipeline_playbook.md`, `Handbuch.md`, `edge_registry.py`, `01_edge_vocabulary.md`, `06_active_roadmap.md`
Die Ingestion transformiert Markdown in den Graphen. Entrypoint: `scripts/import_markdown.py` (CLI) oder `routers/ingest.py` (API). Die Ingestion transformiert Markdown in den Graphen. Entrypoint: `scripts/import_markdown.py` (CLI) oder `routers/ingest.py` (API). Seit v2.7 integriert dieser Prozess die **Edge Registry** zur Normalisierung des Vokabulars und beachtet den **Content Lifecycle**.
## 1. Der Import-Prozess (14-Schritte-Workflow) ## 1. Der Import-Prozess (15-Schritte-Workflow)
Der Prozess ist **asynchron** und **idempotent**. Der Prozess ist **asynchron** und **idempotent**.
@ -21,27 +21,39 @@ Der Prozess ist **asynchron** und **idempotent**.
* **API (`/save`):** Nimmt Request entgegen, validiert und startet Background-Task ("Fire & Forget"). Antwortet sofort mit `202/Queued`. * **API (`/save`):** Nimmt Request entgegen, validiert und startet Background-Task ("Fire & Forget"). Antwortet sofort mit `202/Queued`.
* **CLI:** Iteriert über Dateien und nutzt `asyncio.Semaphore` zur Drosselung. * **CLI:** Iteriert über Dateien und nutzt `asyncio.Semaphore` zur Drosselung.
2. **Markdown lesen:** Rekursives Scannen des Vaults. 2. **Markdown lesen:** Rekursives Scannen des Vaults.
3. **Frontmatter extrahieren:** Validierung von Pflichtfeldern (`id`, `type`, `title`). 3. **Frontmatter Check & Hard Skip (WP-22):**
4. **Config Resolution:** * Extraktion von `status` und `type`.
* **Hard Skip Rule:** Wenn `status` in `['system', 'template']` ist, wird die Datei **sofort übersprungen**. Sie wird weder vektorisiert noch in den Graphen aufgenommen.
* Validierung der Pflichtfelder (`id`, `title`) für alle anderen Dateien.
4. **Edge Registry Initialisierung (WP-22):**
* Laden der Singleton-Instanz der `EdgeRegistry`.
* Validierung der Vokabular-Datei unter `MINDNET_VOCAB_PATH`.
5. **Config Resolution:**
* Bestimmung von `chunking_profile` und `retriever_weight`. * Bestimmung von `chunking_profile` und `retriever_weight`.
* **Priorität:** 1. Frontmatter (Override) -> 2. `types.yaml` (Type) -> 3. Default. * **Priorität:** 1. Frontmatter (Override) -> 2. `types.yaml` (Type) -> 3. Global Default.
5. **Note-Payload generieren:** 6. **Note-Payload generieren:**
* Erstellen des JSON-Objekts für `mindnet_notes`. * Erstellen des JSON-Objekts inklusive `status` (für Scoring).
* **Multi-Hash Calculation:** Berechnet Hashtabellen für `body` (nur Text) und `full` (Text + Metadaten). * **Multi-Hash Calculation:** Berechnet Hashtabellen für `body` (nur Text) und `full` (Text + Metadaten).
6. **Change Detection:** 7. **Change Detection:**
* Vergleich des Hashes mit Qdrant. * Vergleich des Hashes mit Qdrant.
* Strategie wählbar via ENV `MINDNET_CHANGE_DETECTION_MODE` (`full` oder `body`). * Strategie wählbar via ENV `MINDNET_CHANGE_DETECTION_MODE` (`full` oder `body`).
7. **Chunking anwenden:** Zerlegung des Textes basierend auf dem ermittelten Profil (siehe Kap. 3). 8. **Chunking anwenden:** Zerlegung des Textes basierend auf dem ermittelten Profil (siehe Kap. 3).
8. **Smart Edge Allocation (WP15):** 9. **Smart Edge Allocation (WP15):**
* Wenn `enable_smart_edge_allocation: true`: Der `SemanticAnalyzer` sendet Chunks an das LLM. * Wenn `enable_smart_edge_allocation: true`: Der `SemanticAnalyzer` sendet Chunks an das LLM.
* **Traffic Control:** Request nutzt `priority="background"`. Semaphore (Limit via `.env`) drosselt die Last. * **Traffic Control:** Request nutzt `priority="background"`. Semaphore (Limit via `.env`) drosselt die Last.
* **Resilienz:** Bei Timeout (Ollama) greift ein Fallback (Broadcasting an alle Chunks). * **Resilienz:** Bei Timeout (Ollama) greift ein Fallback (Broadcasting an alle Chunks).
9. **Inline-Kanten finden:** Parsing von `[[rel:...]]`. 10. **Inline-Kanten finden:** Parsing von `[[rel:...]]`.
10. **Callout-Kanten finden:** Parsing von `> [!edge]`. 11. **Alias-Auflösung & Kanonisierung (WP-22):**
11. **Default-Edges erzeugen:** Anwendung der `edge_defaults` aus Registry. * Jede Kante wird via `edge_registry.resolve()` normalisiert.
12. **Strukturkanten erzeugen:** `belongs_to`, `next`, `prev`. * Aliase (z.B. `basiert_auf`) werden zu kanonischen Typen (z.B. `based_on`) aufgelöst.
13. **Embedding (Async):** Generierung via `nomic-embed-text` (768 Dim). * Unbekannte Typen werden in `unknown_edges.jsonl` protokolliert.
14. **Diagnose:** Integritäts-Check nach dem Lauf. 12. **Callout-Kanten finden:** Parsing von `> [!edge]`.
13. **Default- & Matrix-Edges erzeugen:** Anwendung der `edge_defaults` aus Registry und Matrix-Logik.
14. **Strukturkanten erzeugen:** `belongs_to`, `next`, `prev`.
15. **Embedding (Async):** Generierung via `nomic-embed-text` (768 Dim).
16. **Diagnose:** Integritäts-Check nach dem Lauf.
--- ---
@ -69,7 +81,7 @@ export MINDNET_CHANGE_DETECTION_MODE="full"
> Das Flag `--purge-before-upsert` ist kritisch. Es löscht vor dem Schreiben einer Note ihre alten Chunks/Edges. Ohne dieses Flag entstehen **"Geister-Chunks"** (alte Textabschnitte, die im Markdown gelöscht wurden, aber im Index verbleiben). > Das Flag `--purge-before-upsert` ist kritisch. Es löscht vor dem Schreiben einer Note ihre alten Chunks/Edges. Ohne dieses Flag entstehen **"Geister-Chunks"** (alte Textabschnitte, die im Markdown gelöscht wurden, aber im Index verbleiben).
### 2.2 Full Rebuild (Clean Slate) ### 2.2 Full Rebuild (Clean Slate)
Notwendig bei Änderungen an `types.yaml` (z.B. neue Chunking-Profile) oder Modell-Wechsel. Notwendig bei Änderungen an `types.yaml` (z.B. neue Chunking-Profile), der Registry oder Modell-Wechsel.
```bash ```bash
# 0. Modell sicherstellen # 0. Modell sicherstellen
@ -89,16 +101,16 @@ python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --
Das Chunking ist profilbasiert und in `types.yaml` konfiguriert. Das Chunking ist profilbasiert und in `types.yaml` konfiguriert.
### 3.1 Profile und Strategien ### 3.1 Profile und Strategien (Vollständige Referenz)
| Profil | Strategie | Parameter | Einsatzgebiet | | Profil | Strategie | Parameter | Einsatzgebiet |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
| `sliding_short` | `sliding_window` | Max: 350, Target: 200 | Kurze Logs, Chats, Risiken. | | `sliding_short` | `sliding_window` | Max: 350, Target: 200 | Kurze Logs, Chats, Risiken. |
| `sliding_standard` | `sliding_window` | Max: 650, Target: 450 | Massendaten (Journal, Quellen). | | `sliding_standard` | `sliding_window` | Max: 650, Target: 450 | Massendaten (Journal, Quellen). |
| `sliding_smart_edges`| `sliding_window` | Max: 600, Target: 400 | Fließtexte mit hohem Wert (Projekte, Erfahrungen). | | `sliding_smart_edges`| `sliding_window` | Max: 600, Target: 400 | Fließtexte mit hohem Wert (Projekte). |
| `structured_smart_edges` | `by_heading` | `strict: false` (Soft) | Strukturierte Texte, wo kleine Abschnitte gemergt werden dürfen. | | `structured_smart_edges` | `by_heading` | `strict: false` (Soft) | Strukturierte Texte, Merging erlaubt. |
| `structured_smart_edges_strict` | `by_heading` | `strict: true` (Hard) | **Atomare Einheiten**: Entscheidungen, Werte, Profile. | | `structured_smart_edges_strict` | `by_heading` | `strict: true` (Hard) | **Atomare Einheiten**: Entscheidungen, Werte. |
| `structured_smart_edges_strict_L3`| `by_heading` | `strict: true`, `level: 3` | Tief geschachtelte Prinzipien (Tier 2/MP1 Logik). | | `structured_smart_edges_strict_L3`| `by_heading` | `strict: true`, `level: 3` | Tief geschachtelte Prinzipien (Tier 2/MP1). |
### 3.2 Die `by_heading` Logik (v2.9 Hybrid) ### 3.2 Die `by_heading` Logik (v2.9 Hybrid)
@ -110,44 +122,46 @@ Die Strategie `by_heading` zerlegt Texte anhand ihrer Struktur (Überschriften).
* *Merge-Check:* Wenn der vorherige Chunk leer war (nur Überschriften), wird gemergt (verhindert verwaiste Überschriften). * *Merge-Check:* Wenn der vorherige Chunk leer war (nur Überschriften), wird gemergt (verhindert verwaiste Überschriften).
* *Safety Net:* Wird ein Abschnitt zu lang (> `max` Token), wird auch ohne Überschrift getrennt. * *Safety Net:* Wird ein Abschnitt zu lang (> `max` Token), wird auch ohne Überschrift getrennt.
* **Modus "Soft" (`strict_heading_split: false`):** * **Modus "Soft" (`strict_heading_split: false`):**
* **Hierarchie-Check:** Überschriften *oberhalb* des Split-Levels (z.B. H1 bei Level 2) erzwingen **immer** einen Split. * **Hierarchie-Check:** Überschriften *oberhalb* des Split-Levels erzwingen **immer** einen Split.
* **Füll-Logik:** Überschriften *auf* dem Split-Level (z.B. H2) lösen nur dann einen neuen Chunk aus, wenn der aktuelle Chunk die `target`-Größe erreicht hat. * **Füll-Logik:** Überschriften *auf* dem Split-Level (z.B. H2) lösen nur dann einen neuen Chunk aus, wenn der aktuelle Chunk die `target`-Größe erreicht hat.
* *Safety Net:* Auch hier greift das `max` Token Limit. * *Safety Net:* Auch hier greift das `max` Token Limit.
### 3.3 Payload-Felder (Qdrant) ### 3.3 Payload-Felder (Qdrant)
* `text`: Der reine Inhalt (Anzeige im UI). Überschriften bleiben erhalten. * `text`: Der reine Inhalt (Anzeige im UI).
* `window`: Inhalt plus Overlap (für Embedding). Bei `by_heading` wird der Kontext (Eltern-Überschrift) oft vorangestellt. * `window`: Inhalt plus Overlap (für Embedding).
* `chunk_profile`: Das effektiv genutzte Profil (zur Nachverfolgung). * `chunk_profile`: Das effektiv genutzte Profil (zur Nachverfolgung).
--- ---
## 4. Edge-Erzeugung & Prioritäten ## 4. Edge-Erzeugung & Prioritäten (Provenance)
Kanten werden nach Vertrauenswürdigkeit (`provenance`) priorisiert. Die höhere Prio gewinnt. Kanten werden nach Vertrauenswürdigkeit (`provenance`) priorisiert. Die höhere Prio gewinnt.
| Prio | Quelle | Rule ID | Confidence | | Prio | Quelle | Rule ID | Confidence | Erläuterung |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- | :--- |
| **1** | Inline | `inline:rel` | 0.95 | | **1** | Wikilink | `explicit:wikilink` | **1.00** | Harte menschliche Setzung. |
| **2** | Callout | `callout:edge` | 0.90 | | **2** | Inline | `inline:rel` | **0.95** | Typisierte menschliche Kante. |
| **3** | Wikilink | `explicit:wikilink` | 1.00 | | **3** | Callout | `callout:edge` | **0.90** | Explizite Meta-Information. |
| **4** | Smart Edge | `smart:llm_filter` | 0.90 | | **4** | Smart Edge | `smart:llm_filter` | **0.90** | KI-validierte Verbindung. |
| **5** | Type Default | `edge_defaults` | 0.70 | | **5** | Type Default | `edge_defaults` | **0.70** | Heuristik aus der Registry. |
| **6** | Struktur | `structure` | 1.00 | | **6** | Struktur | `structure` | **1.00** | System-interne Verkettung. |
--- ---
## 5. Quality Gates & Tests ## 5. Quality Gates & Monitoring
Diese Tests garantieren die Stabilität der Pipeline. In v2.7 wurden Tools zur Überwachung der Datenqualität integriert:
**1. Payload Dryrun (Schema-Check):** **1. Registry Review:** Prüfung der `data/logs/unknown_edges.jsonl`. Administratoren sollten hier gelistete Begriffe als Aliase in die `01_edge_vocabulary.md` aufnehmen.
**2. Payload Dryrun (Schema-Check):**
Simuliert Import, prüft JSON-Schema Konformität. Simuliert Import, prüft JSON-Schema Konformität.
```bash ```bash
python3 -m scripts.payload_dryrun --vault ./test_vault python3 -m scripts.payload_dryrun --vault ./test_vault
``` ```
**2. Full Edge Check (Graph-Integrität):** **3. Full Edge Check (Graph-Integrität):**
Prüft Invarianten (z.B. `next` muss reziprok zu `prev` sein). Prüft Invarianten (z.B. `next` muss reziprok zu `prev` sein).
```bash ```bash
python3 -m scripts.edges_full_check python3 -m scripts.edges_full_check

View File

@ -1,25 +1,25 @@
--- ---
doc_type: technical_reference doc_type: technical_reference
audience: developer, data_scientist audience: developer, data_scientist
scope: backend, retrieval, scoring scope: backend, retrieval, scoring, modularization
status: active status: active
version: 2.6 version: 2.7.1
context: "Formeln und Algorithmen des Hybrid Retrievers und Explanation Layer." context: "Detaillierte Dokumentation der Scoring-Algorithmen, inklusive WP-22 Lifecycle-Modifier, Intent-Boosting und Modularisierung."
--- ---
# Retrieval & Scoring Algorithmen # Retrieval & Scoring Algorithmen
Der Retriever (`app/core/retriever.py`) unterstützt **Semantic Search** und **Hybrid Search**. Seit v2.4 (WP04a) nutzt Mindnet ein gewichtetes Scoring-Modell, das Semantik, Graphentheorie und Metadaten kombiniert. Der Retriever unterstützt **Semantic Search** und **Hybrid Search**. Seit v2.4 nutzt Mindnet ein gewichtetes Scoring-Modell, das Semantik, Graphentheorie und Metadaten kombiniert. Mit Version 2.7 (WP-22) wurde dieses Modell um **Lifecycle-Faktoren** und **Intent-Boosting** erweitert sowie die Architektur modularisiert.
## 1. Scoring Formel (WP04a) ## 1. Scoring Formel (v2.7.0)
Der Gesamtscore eines Treffers berechnet sich als gewichtete Summe. Alle Gewichte ($W$) sind in `retriever.yaml` konfigurierbar. Der Gesamtscore eines Treffers berechnet sich als gewichtete Summe. Alle Gewichte ($W$) und Modifier ($M$) sind in `retriever.yaml` und `decision_engine.yaml` konfigurierbar.
$$ $$
TotalScore = (W_{sem} \cdot S_{sem} \cdot \max(W_{type}, 0)) + (W_{edge} \cdot B_{edge}) + (W_{cent} \cdot B_{cent}) TotalScore = (W_{sem} \cdot S_{sem} \cdot W_{type} \cdot M_{status}) + (W_{edge} \cdot B_{edge}) + (W_{cent} \cdot B_{cent}) + B_{intent}
$$ $$
### Die Komponenten ### Die Komponenten (Klassisch v2.4)
**1. Semantic Score ($S_{sem}$):** **1. Semantic Score ($S_{sem}$):**
* **Basis:** Cosine Similarity des Vektors. * **Basis:** Cosine Similarity des Vektors.
@ -27,87 +27,103 @@ $$
* **Wertebereich:** 0.0 bis 1.0. * **Wertebereich:** 0.0 bis 1.0.
**2. Type Weight ($W_{type}$):** **2. Type Weight ($W_{type}$):**
* **Herkunft:** Feld `retriever_weight` aus der Note/Chunk Payload. * **Herkunft:** Feld `retriever_weight` aus der Note/Chunk Payload oder `types.yaml`.
* **Zweck:** Priorisierung bestimmter Dokumententypen unabhängig vom Inhalt. * **Zweck:** Priorisierung bestimmter Dokumententypen unabhängig vom Inhalt.
* **Beispiel:** Eine `decision` (Gewicht 1.0) schlägt ein `concept` (Gewicht 0.6) bei gleicher textueller Ähnlichkeit. * **Beispiel:** Eine `decision` (1.0) schlägt ein `concept` (0.6) bei gleicher Ähnlichkeit.
**3. Edge Bonus ($B_{edge}$):** **3. Edge Bonus ($B_{edge}$):**
* **Kontext:** Berechnet im lokalen Subgraphen (Expansion Tiefe 1). * **Kontext:** Berechnet im lokalen Subgraphen (Expansion Tiefe 1).
* **Logik:** Summe der `confidence` aller eingehenden Kanten, die von Knoten stammen, die *ebenfalls* im aktuellen Result-Set (oder dem übergebenen Kontext) enthalten sind. * **Logik:** Summe der `confidence` aller eingehenden Kanten von Knoten im aktuellen Result-Set.
* **Zweck:** Belohnt Chunks, die "im Thema" vernetzt sind. * **Zweck:** Belohnt Chunks, die "im Thema" vernetzt sind.
**4. Centrality Bonus ($B_{cent}$):** **4. Centrality Bonus ($B_{cent}$):**
* **Kontext:** Berechnet im lokalen Subgraphen. * **Kontext:** Berechnet im lokalen Subgraphen.
* **Logik:** Eine vereinfachte PageRank-Metrik (Degree Centrality) im temporären Graphen. * **Logik:** Vereinfachte PageRank-Metrik (Degree Centrality).
* **Zweck:** Belohnt "Hubs", die viele Verbindungen zu anderen Treffern haben. * **Zweck:** Belohnt "Hubs" mit vielen Verbindungen zu anderen Treffern.
### Die WP-22 Erweiterungen (v2.7.0)
**5. Status Modifier ($M_{status}$):**
* **Herkunft:** Feld `status` aus dem Frontmatter.
* **Zweck:** Bestraft unfertiges Wissen (Drafts) oder bevorzugt stabiles Wissen.
* **Werte (Auftrag WP-22):** * `stable`: **1.2** (Bonus für Qualität).
* `draft`: **0.5** (Malus für Entwürfe).
* `system`: Exkludiert (siehe Ingestion).
**6. Intent Boost ($B_{intent}$):**
* **Herkunft:** Dynamische Injektion durch die Decision Engine basierend auf der Nutzerfrage.
* **Zweck:** Erhöhung des Scores für Knoten, die über spezifische Kanten-Typen (z.B. `caused_by` bei "Warum"-Fragen) verbunden sind.
--- ---
## 2. Hybrid Retrieval Flow ## 2. Hybrid Retrieval Flow & Modularisierung
Der Hybrid-Modus ist der Standard für den Chat (`/chat`). Er läuft in drei Phasen ab: In v2.7 wurde die Engine in einen Orchestrator (`retriever.py`) und eine Scoring-Engine (`retriever_scoring.py`) aufgeteilt.
**Phase 1: Vector Search (Seed Generation)** **Phase 1: Vector Search (Seed Generation)**
* Suche die Top-K (z.B. 20) Kandidaten via Embeddings in Qdrant. * Der Orchestrator sucht Top-K (Standard: 20) Kandidaten via Embeddings in Qdrant.
* Dies sind die "Seeds" für den Graphen. * Diese bilden die "Seeds" für den Graphen.
**Phase 2: Graph Expansion** **Phase 2: Graph Expansion**
* Nutze `graph_adapter.expand(seeds, depth=1)`. * Nutze `graph_adapter.expand(seeds, depth=1)`.
* Lade alle direkten Nachbarn (Incoming & Outgoing) der Seeds aus der `_edges` Collection. * Lade direkte Nachbarn aus der `_edges` Collection.
* Konstruiere einen temporären `NetworkX`-Graphen im Speicher (`Subgraph`). * Konstruiere einen `NetworkX`-Graphen im Speicher.
**Phase 3: Re-Ranking** **Phase 3: Re-Ranking (Modular)**
* Berechne für jeden Knoten im Subgraphen die Boni ($B_{edge}$, $B_{cent}$). * Der Orchestrator übergibt den Graphen und die Seeds an die `ScoringEngine`.
* Wende die Scoring-Formel an. * Berechne Boni ($B_{edge}$, $B_{cent}$) sowie die neuen Lifecycle- und Intent-Modifier.
* Sortiere die Liste neu absteigend nach `TotalScore`. * Sortierung absteigend nach `TotalScore` und Limitierung auf Top-Resultate (z.B. 5).
* Schneide die Liste beim finalen Limit (z.B. 5) ab.
--- ---
## 3. Explanation Layer (WP04b) ## 3. Explanation Layer (WP-22 Update)
Wenn der Parameter `explain=True` an die API übergeben wird, generiert der Retriever ein `Explanation` Objekt für jeden Treffer. Bei `explain=True` generiert das System eine detaillierte Begründung.
**JSON-Struktur der Erklärung:** **Erweiterte JSON-Struktur:**
```json ```json
{ {
"score_breakdown": { "score_breakdown": {
"semantic": 0.85, "semantic": 0.85,
"type_boost": 1.0, "type_boost": 1.0,
"lifecycle_modifier": 0.5,
"edge_bonus": 0.4, "edge_bonus": 0.4,
"intent_boost": 0.5,
"centrality": 0.1 "centrality": 0.1
}, },
"reasons": [ "reasons": [
"Hohe textuelle Übereinstimmung (>0.85).", "Hohe textuelle Übereinstimmung (>0.85).",
"Bevorzugt, da Typ 'decision' (Gewicht 1.0).", "Status 'draft' reduziert Relevanz (Modifier 0.5).",
"Wird referenziert von 'Projekt Alpha' via 'depends_on'." "Wird referenziert via 'caused_by' (Intent-Bonus 0.5).",
"Bevorzugt, da Typ 'decision' (Gewicht 1.0)."
] ]
} }
``` ```
**Logik der Text-Generierung:**
* **Semantik:** Wenn $S_{sem} > 0.8$: "Hohe Übereinstimmung".
* **Typ:** Wenn $W_{type} > 0.8$: "Bevorzugt, da Typ X".
* **Graph:** Listet bis zu 3 eingehende Kanten mit hoher Konfidenz auf ("Wird referenziert von...").
--- ---
## 4. Konfiguration (`retriever.yaml`) ## 4. Konfiguration (`retriever.yaml`)
Diese Datei steuert die Balance der Formel. Steuert die Gewichtung der mathematischen Komponenten.
```yaml ```yaml
scoring: scoring:
semantic_weight: 1.0 # Basis-Relevanz (sollte immer ca. 1.0 sein) semantic_weight: 1.0 # Basis-Relevanz
edge_weight: 0.7 # Einfluss des Graphen (0.5 - 1.0 empfohlen) edge_weight: 0.7 # Graphen-Einfluss
centrality_weight: 0.5 # Einfluss der Zentralität (Hub-Bonus) centrality_weight: 0.5 # Hub-Einfluss
# Kanten-spezifische Gewichtung für den Edge-Bonus # WP-22 Lifecycle Konfiguration (Abgleich mit Auftrag)
lifecycle_weights:
stable: 1.2 # Bonus für Qualität
draft: 0.5 # Malus für Entwürfe
# Kanten-Gewichtung für den Edge-Bonus (Basis)
edge_weights: edge_weights:
depends_on: 1.5 # Harte Abhängigkeiten sehr stark gewichten depends_on: 1.5 # Harte Abhängigkeiten
blocks: 1.5 # Risiken/Blocker stark gewichten blocks: 1.5 # Risiken/Blocker
caused_by: 1.2 # Kausalitäten moderat stärken caused_by: 1.2 # Kausalitäten
related_to: 0.5 # Weiche Themen schwächer gewichten based_on: 1.3 # Werte-Bezug
related_to: 0.5 # Weiche Themen
references: 0.8 # Standard-Referenzen references: 0.8 # Standard-Referenzen
``` ```

View File

@ -1,10 +1,10 @@
--- ---
doc_type: operations_manual doc_type: operations_manual
audience: admin, devops audience: admin, devops
scope: deployment, maintenance, backup scope: deployment, maintenance, backup, edge_registry
status: active status: active
version: 2.6 version: 2.7.0
context: "Installationsanleitung, Systemd-Units und Wartungsprozesse." context: "Installationsanleitung, Systemd-Units und Wartungsprozesse für Mindnet v2.7."
--- ---
# Admin Operations Guide # Admin Operations Guide
@ -58,6 +58,7 @@ User=llmadmin
Group=llmadmin Group=llmadmin
WorkingDirectory=/home/llmadmin/mindnet WorkingDirectory=/home/llmadmin/mindnet
# Startet Uvicorn (Async Server) # Startet Uvicorn (Async Server)
# Hinweis: Die .env muss MINDNET_VOCAB_PATH für die Edge Registry enthalten.
ExecStart=/home/llmadmin/mindnet/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8001 --env-file .env ExecStart=/home/llmadmin/mindnet/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8001 --env-file .env
Restart=always Restart=always
RestartSec=5 RestartSec=5
@ -96,7 +97,7 @@ WantedBy=multi-user.target
--- ---
## 3. Wartung & Cronjobs ## 3. Wartung & Monitoring
### 3.1 Regelmäßiger Import (Cron) ### 3.1 Regelmäßiger Import (Cron)
Führt den Sync stündlich durch. Nutzt `--purge-before-upsert` für Sauberkeit. Führt den Sync stündlich durch. Nutzt `--purge-before-upsert` für Sauberkeit.
@ -106,7 +107,16 @@ Führt den Sync stündlich durch. Nutzt `--purge-before-upsert` für Sauberkeit.
0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1 0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1
``` ```
### 3.2 Troubleshooting Guide ### 3.2 Monitoring der Edge Registry (WP-22)
Administratoren sollten regelmäßig das Log für unbekannte Kanten-Typen prüfen.
* **Pfad:** `data/logs/unknown_edges.jsonl`.
* **Aktion:** Wenn neue Typen häufig auftreten, sollten diese als Alias in die `01_edge_vocabulary.md` aufgenommen werden.
### 3.3 Troubleshooting Guide
**Fehler: "Registry Initialization Failure" (Neu in v2.7)**
* **Symptom:** API startet, aber Kanten werden nicht gewichtet oder Fehlermeldung im Log.
* **Lösung:** Prüfen Sie `MINDNET_VOCAB_PATH` in der `.env`. Der Pfad muss absolut sein und auf eine existierende Markdown-Tabelle zeigen.
**Fehler: "ModuleNotFoundError: No module named 'st_cytoscape'"** **Fehler: "ModuleNotFoundError: No module named 'st_cytoscape'"**
* Ursache: Alte Dependencies oder falsches Paket installiert. * Ursache: Alte Dependencies oder falsches Paket installiert.
@ -115,8 +125,6 @@ Führt den Sync stündlich durch. Nutzt `--purge-before-upsert` für Sauberkeit.
source .venv/bin/activate source .venv/bin/activate
pip uninstall streamlit-cytoscapejs pip uninstall streamlit-cytoscapejs
pip install st-cytoscape pip install st-cytoscape
# Oder sicherheitshalber:
pip install -r requirements.txt
``` ```
**Fehler: "Vector dimension error: expected 768, got 384"** **Fehler: "Vector dimension error: expected 768, got 384"**

View File

@ -2,6 +2,119 @@
doc_type: roadmap doc_type: roadmap
audience: product_owner, developer audience: product_owner, developer
status: active status: active
version: 2.7.0
context: "Aktuelle Planung für kommende Features (ab WP16), Release-Strategie und Historie der abgeschlossenen WPs."
---
# Mindnet Active Roadmap
**Aktueller Stand:** v2.7.0 (Post-WP-22)
**Fokus:** Professionalisierung, Content Lifecycle & Graph Intelligence.
## 1. Programmstatus
Wir haben mit der Implementierung des Graph Explorers (WP19) und der Smart Edge Allocation (WP15) die Basis für ein intelligentes, robustes System gelegt. Der Abschluss von WP-22 (Content Lifecycle) professionalisiert nun die Datenhaltung und das Vokabular-Management.
| Phase | Fokus | Status |
| :--- | :--- | :--- |
| **Phase A** | Fundament & Import | ✅ Fertig |
| **Phase B** | Semantik & Graph | ✅ Fertig |
| **Phase C** | Persönlichkeit | ✅ Fertig |
| **Phase D** | Interaktion & Tools | ✅ Fertig |
| **Phase E** | Maintenance & Professionalisierung | 🚀 Aktiv (v2.7.0) |
---
## 2. Historie: Abgeschlossene Workpackages
Eine Übersicht der implementierten Features zum schnellen Auffinden von Funktionen. Details siehe `99_legacy_workpackages.md`.
| WP | Titel | Ergebnis / Kern-Feature |
| :--- | :--- | :--- |
| **WP-01** | Knowledge Design | Definition von `types.yaml` und Note-Typen (Project, Concept, etc.). |
| **WP-02** | Chunking Strategy | Implementierung von Sliding-Window und Hash-basierter Änderungserkennung. |
| **WP-03** | Import-Pipeline | Asynchroner Importer, der Markdown in Qdrant (Notes/Edges) schreibt. |
| **WP-04a**| Retriever Scoring | Hybride Suche: `Score = Semantik + GraphBonus + TypGewicht`. |
| **WP-04b**| Explanation Layer | Transparenz: API liefert "Reasons" (Warum wurde das gefunden?). |
| **WP-04c**| Feedback Loop | Logging von User-Feedback (JSONL) als Basis für Learning. |
| **WP-05** | RAG-Chat | Integration von Ollama (`phi3`) und Context-Enrichment im Prompt. |
| **WP-06** | Decision Engine | Hybrid Router unterscheidet Frage (`RAG`) vs. Handlung (`Interview`). |
| **WP-07** | Interview-Assistent | One-Shot Extraction: Erzeugt Markdown-Drafts aus User-Input. |
| **WP-10** | Web UI | Streamlit-Frontend als Ersatz für das Terminal. |
| **WP-10a**| Draft Editor | GUI-Komponente zum Bearbeiten und Speichern generierter Notizen. |
| **WP-11** | Backend Intelligence | `nomic-embed-text` (768d) und Matrix-Logik für Kanten-Typisierung. |
| **WP-15** | Smart Edge Allocation | LLM-Filter für Kanten in Chunks + Traffic Control (Semaphore) + Strict Chunking. |
| **WP-19** | Graph Visualisierung | **Frontend Modularisierung:** Umbau auf `ui_*.py`.<br>**Graph Engines:** Parallelbetrieb von Cytoscape und Agraph.<br>**Tools:** SSOT Editor, Persistenz via URL. |
| **WP-20** | Cloud Hybrid Mode | Nutzung von Public LLM für schnellere Verarbeitung und bestimmte Aufgaben. |
| **WP-21** | Semantic Graph Routing | Transformation des Graphen von statischen Verbindungen zu dynamischen, kontextsensitiven Pfaden. |
| **WP-22** | **Content Lifecycle & Registry** | **Ergebnis:** SSOT via `01_edge_vocabulary.md`, Alias-Mapping, Status-Scoring (`stable`/`draft`) und Modularisierung der Scoring-Engine. |
### 2.1 WP-22 Lessons Learned
* **Architektur:** Die Trennung von `retriever.py` und `retriever_scoring.py` war notwendig, um LLM-Context-Limits zu wahren und die Testbarkeit der mathematischen Formeln zu erhöhen.
* **Kanten-Validierung:** Die Edge Registry muss beim Start explizit initialisiert werden (Singleton), um "Lazy Loading" Probleme in der API zu vermeiden.
---
## 3. Offene Workpackages (Planung)
### WP-19a Graph Intelligence & Discovery (Sprint-Fokus)
**Status:** 🚀 Startklar
**Ziel:** Vom "Anschauen" zum "Verstehen". Deep-Dive Werkzeuge für den Graphen.
* **Discovery Screen:** Neuer Tab für semantische Suche ("Finde Notizen über Vaterschaft") und Wildcard-Filter.
* **Filter-Logik:** "Zeige nur Wege, die zu `type:decision` führen".
* **Chunk Inspection:** Umschaltbare Granularität (Notiz vs. Chunk) zur Validierung des Smart Chunkers.
### WP-14 Review / Refactoring / Dokumentation
**Status:** 🟡 Laufend (Phase E)
**Ziel:** Technische Schulden abbauen, die durch schnelle Feature-Entwicklung entstanden sind.
* **Refactoring `chunker.py`:** Die Datei ist monolithisch geworden (Parsing, Strategien, LLM-Orchestrierung).
* *Lösung:* Aufteilung in ein Package `app/core/chunking/` mit Modulen (`strategies.py`, `orchestration.py`, `utils.py`).
* **Dokumentation:** Kontinuierliche Synchronisation von Code und Docs (v2.7.0 Stand).
### WP-16 Auto-Discovery & Intelligent Ingestion
**Status:** 🟡 Geplant
**Ziel:** Das System soll "dumme" Textdateien beim Import automatisch analysieren, strukturieren und anreichern, bevor sie gespeichert werden.
**Kern-Features:**
1. **Smart Link Enricher:** Automatisches Erkennen von fehlenden Kanten in Texten ohne explizite Wikilinks. Ein "Enricher" scannt Text vor dem Import, findet Keywords (z.B. "Mindnet") und schlägt Links vor (`[[Mindnet]]`).
2. **Structure Analyzer (Auto-Strategy):**
* *Problem:* Manuelle Zuweisung von `chunking_profile` in `types.yaml` ist starr.
* *Lösung:* Vorschalten einer Analysestufe im Importer (`chunker.py`), die die Text-Topologie prüft und die Strategie wählt.
* *Metrik 1 (Heading Density):* Verhältnis `Anzahl Überschriften / Wortanzahl`. Hohe Dichte (> 1/200) -> Indikator für `structured_smart_edges`. Niedrige Dichte -> `sliding_smart_edges`.
* *Metrik 2 (Variance):* Regelmäßigkeit der Abstände zwischen Headings.
3. **Context-Aware Hierarchy Merging:**
* *Problem:* Leere Zwischenüberschriften (z.B. "Tier 2") gingen früher als bedeutungslose Chunks verloren oder wurden isoliert.
* *Lösung:* Generalisierung der Logik aus WP-15, die leere Eltern-Elemente automatisch mit dem ersten Kind-Element verschmilzt ("Tier 2 + MP1"), um den Kontext für das Embedding zu wahren.
### WP-17 Conversational Memory (Gedächtnis)
**Status:** 🟡 Geplant
**Ziel:** Echte Dialoge statt Request-Response.
* **Tech:** Erweiterung des `ChatRequest` DTO um `history`.
* **Logik:** Token-Management (Context Window Balancing zwischen RAG-Doks und Chat-Verlauf).
### WP-18 Graph Health & Maintenance
**Status:** 🟡 Geplant (Prio 2)
**Ziel:** Konsistenzprüfung ("Garbage Collection").
* **Feature:** Cronjob `check_graph_integrity.py`.
* **Funktion:** Findet "Dangling Edges" (Links auf gelöschte Notizen) und repariert/löscht sie.
### WP-13 MCP-Integration & Agenten-Layer
**Status:** 🟡 Geplant
**Ziel:** mindnet als MCP-Server bereitstellen, damit Agenten (Claude Desktop, OpenAI) standardisierte Tools nutzen können.
* **Umfang:** MCP-Server mit Tools (`mindnet_query`, `mindnet_explain`, etc.).
### WP-20 Cloud Hybrid Mode (Optional)
**Status:** ⚪ Optional
**Ziel:** "Turbo-Modus" für Massen-Imports.
* **Konzept:** Switch in `.env`, um statt Ollama (Lokal) auf Google Gemini (Cloud) umzuschalten.
### WP-21 Semantic Graph Routing & Canonical Edges
**Status:** 🟡 Geplant
**Ziel:** Transformation des Graphen von statischen Verbindungen zu dynamischen, kontextsensitiven Pfaden.
**Problem:**
1. **Statische Gewichte:** Aktuell ist `caused_by` immer gleich wichtig, egal ob der User nach Ursachen oder Definitionen fragt.---
doc_type: roadmap
audience: product_owner, developer
status: active
version: 2.7 version: 2.7
context: "Aktuelle Planung für kommende Features (ab WP16), Release-Strategie und Historie der abgeschlossenen WPs." context: "Aktuelle Planung für kommende Features (ab WP16), Release-Strategie und Historie der abgeschlossenen WPs."
--- ---
@ -50,6 +163,60 @@ Eine Übersicht der implementierten Features zum schnellen Auffinden von Funktio
| **WP-20** | Cloud Hybrid Mode | Nutzung von Public LLM für schnellere Verarbeitung und bestimmte Aufgaben | | **WP-20** | Cloud Hybrid Mode | Nutzung von Public LLM für schnellere Verarbeitung und bestimmte Aufgaben |
| **WP-21** | Semantic Graph Routing & Canonical Edges | Transformation des Graphen von statischen Verbindungen zu dynamischen, kontextsensitiven Pfaden. Das System soll verstehen, *welche* Art von Verbindung für die aktuelle Frage relevant ist ("Warum?" vs. "Was kommt danach?"). | | **WP-21** | Semantic Graph Routing & Canonical Edges | Transformation des Graphen von statischen Verbindungen zu dynamischen, kontextsensitiven Pfaden. Das System soll verstehen, *welche* Art von Verbindung für die aktuelle Frage relevant ist ("Warum?" vs. "Was kommt danach?"). |
| **WP-22** | **Content Lifecycle & Registry** | **Ergebnis:** SSOT via `01_edge_vocabulary.md`, Alias-Mapping, Status-Scoring (`stable`/`draft`) und Modularisierung der Scoring-Engine. | | **WP-22** | **Content Lifecycle & Registry** | **Ergebnis:** SSOT via `01_edge_vocabulary.md`, Alias-Mapping, Status-Scoring (`stable`/`draft`) und Modularisierung der Scoring-Engine. |
---
doc_type: roadmap
audience: product_owner, developer
status: active
version: 2.7
context: "Aktuelle Planung für kommende Features (ab WP16), Release-Strategie und Historie der abgeschlossenen WPs."
---
# Mindnet Active Roadmap
**Aktueller Stand:** v2.6.0 (Post-WP15/WP19)
**Fokus:** Visualisierung, Exploration & Intelligent Ingestion.
## 1. Programmstatus
Wir haben mit der Implementierung des Graph Explorers (WP19) und der Smart Edge Allocation (WP15) die Basis für ein intelligentes, robustes System gelegt. Der nächste Schritt (WP19a) vertieft die Analyse, während WP16 die "Eingangs-Intelligenz" erhöht.
| Phase | Fokus | Status |
| :--- | :--- | :--- |
| **Phase A** | Fundament & Import | ✅ Fertig |
| **Phase B** | Semantik & Graph | ✅ Fertig |
| **Phase C** | Persönlichkeit | ✅ Fertig |
| **Phase D** | Interaktion & Tools | ✅ Fertig |
| **Phase E** | Maintenance & Visualisierung | 🚀 Aktiv |
---
## 2. Historie: Abgeschlossene Workpackages
Eine Übersicht der implementierten Features zum schnellen Auffinden von Funktionen. Details siehe `99_legacy_workpackages.md`.
| WP | Titel | Ergebnis / Kern-Feature |
| :--- | :--- | :--- |
| **WP-01** | Knowledge Design | Definition von `types.yaml` und Note-Typen (Project, Concept, etc.). |
| **WP-02** | Chunking Strategy | Implementierung von Sliding-Window und Hash-basierter Änderungserkennung. |
| **WP-03** | Import-Pipeline | Asynchroner Importer, der Markdown in Qdrant (Notes/Edges) schreibt. |
| **WP-04a**| Retriever Scoring | Hybride Suche: `Score = Semantik + GraphBonus + TypGewicht`. |
| **WP-04b**| Explanation Layer | Transparenz: API liefert "Reasons" (Warum wurde das gefunden?). |
| **WP-04c**| Feedback Loop | Logging von User-Feedback (JSONL) als Basis für Learning. |
| **WP-05** | RAG-Chat | Integration von Ollama (`phi3`) und Context-Enrichment im Prompt. |
| **WP-06** | Decision Engine | Hybrid Router unterscheidet Frage (`RAG`) vs. Handlung (`Interview`). |
| **WP-07** | Interview-Assistent | One-Shot Extraction: Erzeugt Markdown-Drafts aus User-Input. |
| **WP-08** | Interview-Assistent | One-Shot Extraction: Erzeugt Markdown-Drafts aus User-Input. |
| **WP-09** | Interview-Assistent | One-Shot Extraction: Erzeugt Markdown-Drafts aus User-Input. |
| **WP-08** | Interview-Assistent | One-Shot Extraction: Erzeugt Markdown-Drafts aus User-Input. |
| **WP-09** | Interview-Assistent | One-Shot Extraction: Erzeugt Markdown-Drafts aus User-Input. |
| **WP-10** | Web UI | Streamlit-Frontend als Ersatz für das Terminal. |
| **WP-10a**| Draft Editor | GUI-Komponente zum Bearbeiten und Speichern generierter Notizen. |
| **WP-11** | Backend Intelligence | `nomic-embed-text` (768d) und Matrix-Logik für Kanten-Typisierung. |
| **WP-15** | Smart Edge Allocation | LLM-Filter für Kanten in Chunks + Traffic Control (Semaphore) + Strict Chunking. |
| **WP-19** | Graph Visualisierung | **Frontend Modularisierung:** Umbau auf `ui_*.py`.<br>**Graph Engines:** Parallelbetrieb von Cytoscape (COSE) und Agraph.<br>**Tools:** "Single Source of Truth" Editor, Persistenz via URL. |
| **WP-20** | Cloud Hybrid Mode | Nutzung von Public LLM für schnellere Verarbeitung und bestimmte Aufgaben |
| **WP-21** | Semantic Graph Routing & Canonical Edges | Transformation des Graphen von statischen Verbindungen zu dynamischen, kontextsensitiven Pfaden. Das System soll verstehen, *welche* Art von Verbindung für die aktuelle Frage relevant ist ("Warum?" vs. "Was kommt danach?"). |
| **WP-22** | **Content Lifecycle & Registry** | **Ergebnis:** SSOT via `01_edge_vocabulary.md`, Alias-Mapping, Status-Scoring (`stable`/`draft`) und Modularisierung der Scoring-Engine. |
### 2.1 WP-22 Lessons Learned ### 2.1 WP-22 Lessons Learned
* **Architektur:** Die Trennung von `retriever.py` und `retriever_scoring.py` war notwendig, um LLM-Context-Limits zu wahren und die Testbarkeit der mathematischen Formeln zu erhöhen. * **Architektur:** Die Trennung von `retriever.py` und `retriever_scoring.py` war notwendig, um LLM-Context-Limits zu wahren und die Testbarkeit der mathematischen Formeln zu erhöhen.
@ -60,6 +227,7 @@ Eine Übersicht der implementierten Features zum schnellen Auffinden von Funktio
## 3. Offene Workpackages (Planung) ## 3. Offene Workpackages (Planung)
### WP-08 Self-Tuning v1/v2 (geplant)
### WP-08 Self-Tuning v1/v2 (geplant) ### WP-08 Self-Tuning v1/v2 (geplant)
Diese Features stehen als nächstes an oder befinden sich in der Umsetzung. Diese Features stehen als nächstes an oder befinden sich in der Umsetzung.
**Phase:** B/C **Phase:** B/C
@ -86,6 +254,33 @@ Diese Features stehen als nächstes an oder befinden sich in der Umsetzung.
- Tools zur Analyse des Vault-Status. - Tools zur Analyse des Vault-Status.
- Empfehlungen für minimale Anpassungen. - Empfehlungen für minimale Anpassungen.
**Aufwand / Komplexität:**
- Aufwand: Mittel
- Komplexität: Niedrig/Mittel
**Phase:** B/C
**Status:** 🟡 geplant (Nächster Fokus)
**Ziel:** Aufbau eines Self-Tuning-Mechanismus, der auf Basis von Feedback-Daten (WP-04c) Vorschläge für Retriever- und Policy-Anpassungen macht.
**Umfang:**
- Auswertung der JSONL-Feedback-Daten.
- Regel-basierte Anpassungs-Vorschläge für `retriever.yaml` und Typ-Prioritäten.
**Aufwand / Komplexität:**
- Aufwand: Hoch
- Komplexität: Hoch
### WP-09 Vault-Onboarding & Migration (geplant)
**Phase:** B
**Status:** 🟡 geplant
**Ziel:** Sicherstellen, dass bestehende und neue Obsidian-Vaults schrittweise in mindnet integriert werden können ohne Massenumbau.
**Umfang:**
- Tools zur Analyse des Vault-Status.
- Empfehlungen für minimale Anpassungen.
**Aufwand / Komplexität:** **Aufwand / Komplexität:**
- Aufwand: Mittel - Aufwand: Mittel
- Komplexität: Niedrig/Mittel - Komplexität: Niedrig/Mittel

View File

@ -254,3 +254,65 @@ Bitte bestätige die Übernahme, erstelle die `edge_mappings.yaml` Struktur und
* Es ist der logisch perfekte Ort, um zu sagen: "Wenn der User im Analyse-Modus ist, sind Fakten-Kanten wichtiger als Gefühls-Kanten." * Es ist der logisch perfekte Ort, um zu sagen: "Wenn der User im Analyse-Modus ist, sind Fakten-Kanten wichtiger als Gefühls-Kanten."
Damit hast du das perfekte Paket für den nächsten Entwicklungsschritt geschnürt! Damit hast du das perfekte Paket für den nächsten Entwicklungsschritt geschnürt!
## WP-22: Content Lifecycle & Meta-Config
**Status:** 🚀 Startklar
**Fokus:** Ingestion-Filter, Scoring-Logik, Registry-Architektur.
```text
Du bist der Lead Architect für "Mindnet" (v2.7).
Wir starten ein umfassendes Architektur-Paket: **WP-22: Graph Intelligence & Lifecycle**.
**Kontext:**
Wir professionalisieren die Datenhaltung ("Docs as Code") und machen die Suche kontextsensitiv.
Wir haben eine Markdown-Datei (`01_edge_vocabulary.md`), die als Single-Source-of-Truth für Kanten-Typen dient.
**Dein Auftrag:**
Implementiere (A) den Content-Lifecycle, (B) die Edge-Registry und (C) das Semantic Routing.
---
### Teil A: Content Lifecycle (Ingestion Logic)
Steuerung über Frontmatter `status`:
1. **System-Dateien (No-Index):**
* Wenn `status` in `['system', 'template']`: **Hard Skip**. Datei wird nicht vektorisiert.
2. **Wissens-Status (Scoring):**
* Wenn `status` in `['draft', 'active', 'stable']`: Status wird im Payload gespeichert.
* **ToDo:** Erweitere `scoring.py`, damit `stable` Notizen einen Bonus erhalten (`x 1.2`), `drafts` einen Malus (`x 0.5`).
---
### Teil B: Central Edge Registry & Validation
1. **Registry Klasse:**
* Erstelle `EdgeRegistry` (Singleton).
* Liest beim Start `01_User_Manual/01_edge_vocabulary.md` (Regex parsing der Tabelle).
* Stellt `canonical_types` und `aliases` bereit.
2. **Rückwärtslogik (Learning):**
* Prüfe beim Import jede Kante gegen die Registry.
* Unbekannte Typen werden **nicht** verworfen, sondern in `data/logs/unknown_edges.jsonl` geloggt (für späteres Review).
---
### Teil C: Semantic Graph Routing (Dynamic Boosting)
**Ziel:** Die Bedeutung einer Kante soll sich je nach Frage-Typ ändern ("Warum" vs. "Wie").
**Architektur-Vorgabe (WICHTIG):**
Die Gewichtung findet **Pre-Retrieval** (im Scoring-Algorithmus) statt, **nicht** im LLM-Prompt.
1. **Decision Engine (`decision_engine.yaml`):**
* Füge `boost_edges` zu Strategien hinzu.
* *Beispiel:* `EXPLANATION` (Warum-Fragen) -> Boost `caused_by: 2.5`, `derived_from: 2.0`.
* *Beispiel:* `ACTION` (Wie-Fragen) -> Boost `next: 3.0`, `depends_on: 2.0`.
2. **Scoring-Logik (`scoring.py`):**
* Der `Retriever` erhält vom Router die `boost_edges` Map.
* Berechne Score: `BaseScore * (1 + ConfigWeight + DynamicBoost)`.
---
**Deine Aufgaben:**
1. Zeige die `EdgeRegistry` Klasse (Parsing Logik).
2. Zeige die Integration in `ingestion.py` (Status-Filter & Edge-Validierung).
3. Zeige die Erweiterung in `scoring.py` (Status-Gewicht & Dynamic Edge Boosting).
Bitte bestätige die Übernahme dieses Architektur-Pakets.

View File

@ -0,0 +1,145 @@
"""
FILE: tests/test_WP22_intelligence.py
DESCRIPTION: Integrationstest für WP-22.
FIX: Erzwingt Pfad-Synchronisation für Registry & Router. Behebt Pydantic Validation Errors.
"""
import unittest
import os
import shutil
import yaml
import asyncio
from unittest.mock import MagicMock, patch, AsyncMock
# --- Modul-Caching Fix: Wir müssen Caches leeren ---
import app.routers.chat
from app.models.dto import ChatRequest, QueryHit, QueryRequest
from app.services.edge_registry import EdgeRegistry
from app.core.retriever import _compute_total_score, _get_status_multiplier
from app.routers.chat import _classify_intent, get_decision_strategy, chat_endpoint
class TestWP22Integration(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
"""Bereitet eine isolierte Test-Umgebung vor."""
# Wir simulieren hier 'vault_master' (oder venv_master) als Verzeichnis
self.test_root = os.path.abspath("tests/temp_wp22")
self.test_vault = os.path.join(self.test_root, "vault_master")
self.test_config_dir = os.path.join(self.test_root, "config")
# 1. Pfade erstellen
os.makedirs(os.path.join(self.test_vault, "01_User_Manual"), exist_ok=True)
os.makedirs(self.test_config_dir, exist_ok=True)
os.makedirs(os.path.join(self.test_root, "data/logs"), exist_ok=True)
# 2. Config Files schreiben (MOCK CONFIG)
self.decision_path = os.path.join(self.test_config_dir, "decision_engine.yaml")
self.decision_config = {
"strategies": {
"FACT": {
"trigger_keywords": ["was ist"],
"edge_boosts": {"part_of": 2.0}
},
"CAUSAL": {
"trigger_keywords": ["warum"],
"edge_boosts": {"caused_by": 3.0}
}
}
}
with open(self.decision_path, "w", encoding="utf-8") as f:
yaml.dump(self.decision_config, f)
# 3. Vocabulary File am RICHTIGEN Ort relativ zum test_vault
self.vocab_path = os.path.join(self.test_vault, "01_User_Manual/01_edge_vocabulary.md")
with open(self.vocab_path, "w", encoding="utf-8") as f:
f.write("| System-Typ | Aliases |\n| :--- | :--- |\n| **caused_by** | ursache_ist |\n| **part_of** | teil_von |")
# 4. MOCKING / RESETTING GLOBAL STATE
# Zwinge get_settings, unsere Test-Pfade zurückzugeben
self.mock_settings = MagicMock()
self.mock_settings.DECISION_CONFIG_PATH = self.decision_path
self.mock_settings.MINDNET_VAULT_ROOT = self.test_vault
self.mock_settings.RETRIEVER_TOP_K = 5
self.mock_settings.MODEL_NAME = "test-model"
# Patching get_settings in allen relevanten Modulen
self.patch_settings_chat = patch('app.routers.chat.get_settings', return_value=self.mock_settings)
self.patch_settings_registry = patch('app.services.edge_registry.get_settings', return_value=self.mock_settings)
self.patch_settings_chat.start()
self.patch_settings_registry.start()
# Caches zwingend leeren
app.routers.chat._DECISION_CONFIG_CACHE = None
# Registry Singleton Reset & Force Init mit Test-Pfad
EdgeRegistry._instance = None
self.registry = EdgeRegistry(vault_root=self.test_vault)
self.registry.unknown_log_path = os.path.join(self.test_root, "data/logs/unknown.jsonl")
async def asyncTearDown(self):
self.patch_settings_chat.stop()
self.patch_settings_registry.stop()
if os.path.exists(self.test_root):
shutil.rmtree(self.test_root)
EdgeRegistry._instance = None
app.routers.chat._DECISION_CONFIG_CACHE = None
def test_registry_resolution(self):
print("\n🔵 TEST 1: Registry Pfad & Alias Resolution")
# Prüfen ob die Datei gefunden wurde
self.assertTrue(len(self.registry.valid_types) > 0, f"Registry leer! Root: {self.registry.vault_root}")
self.assertEqual(self.registry.resolve("ursache_ist"), "caused_by")
print("✅ Registry OK.")
def test_scoring_math(self):
print("\n🔵 TEST 2: Scoring Math (Lifecycle)")
with patch("app.core.retriever._get_scoring_weights", return_value=(1.0, 1.0, 0.0)):
# Stable (1.2)
self.assertEqual(_get_status_multiplier({"status": "stable"}), 1.2)
# Draft (0.5)
self.assertEqual(_get_status_multiplier({"status": "draft"}), 0.5)
# Scoring Formel Test: BaseScore * (1 + ConfigWeight + DynamicBoost)
# BaseScore = 0.5 (sem) * 1.2 (stable) = 0.6
# ConfigWeight = 1.0 (neutral) - 1.0 = 0.0
# DynamicBoost = (1.0 * 0.5) = 0.5
# Total = 0.6 * (1 + 0 + 0.5) = 0.9
total, _, _ = _compute_total_score(0.5, {"status": "stable", "retriever_weight": 1.0}, edge_bonus_raw=0.5)
self.assertAlmostEqual(total, 0.9)
print("✅ Scoring OK.")
async def test_router_intent(self):
print("\n🔵 TEST 3: Intent Classification")
mock_llm = MagicMock()
intent, _ = await _classify_intent("Warum ist das so?", mock_llm)
self.assertEqual(intent, "CAUSAL")
print("✅ Routing OK.")
async def test_full_flow(self):
print("\n🔵 TEST 4: End-to-End Pipeline & Dynamic Boosting")
mock_llm = AsyncMock()
mock_llm.prompts = {}
mock_llm.generate_raw_response.return_value = "Test Antwort"
mock_retriever = AsyncMock()
# Fix note_id für Pydantic Validation
mock_hit = QueryHit(
node_id="c1", note_id="test_note_n1", semantic_score=0.8, edge_bonus=0.0,
centrality_bonus=0.0, total_score=0.8, source={"text": "t"},
payload={"status": "active", "type": "concept"}
)
mock_retriever.search.return_value.results = [mock_hit]
req = ChatRequest(message="Warum ist das passiert?", top_k=1)
resp = await chat_endpoint(req, llm=mock_llm, retriever=mock_retriever)
# Verify Intent
self.assertEqual(resp.intent, "CAUSAL")
# Verify Boosts Reached Retriever
called_req = mock_retriever.search.call_args[0][0]
self.assertEqual(called_req.boost_edges.get("caused_by"), 3.0)
print("✅ Full Flow & Boosting OK.")
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,31 @@
---
id: edge_vocabulary
title: Edge Vocabulary & Semantik
type: reference
status: system
system_role: config
context: "Zentrales Wörterbuch für Kanten-Bezeichner. Dient als Single Source of Truth für Obsidian-Skripte und Mindnet-Validierung."
---
# Edge Vocabulary & Semantik
**Pfad:** `01_User_Manual/01_edge_vocabulary.md`
**Zweck:** Definition aller erlaubten Kanten-Typen und ihrer Aliase.
| System-Typ (Canonical) | Erlaubte Aliasse (User) | Beschreibung |
| :--- | :--- | :--- |
| **`caused_by`** | `ausgelöst_durch`, `wegen`, `ursache_ist` | Kausalität: A löst B aus. |
| **`derived_from`** | `abgeleitet_von`, `quelle`, `inspiriert_durch` | Herkunft: A stammt von B. |
| **`based_on`** | `basiert_auf`, `fundament`, `grundlage` | Fundament: B baut auf A auf. |
| **`solves`** | `löst`, `beantwortet`, `fix_für` | Lösung: A ist Lösung für Problem B. |
| **`part_of`** | `teil_von`, `gehört_zu`, `cluster` | Hierarchie: Kind -> Eltern. |
| **`depends_on`** | `hängt_ab_von`, `braucht`, `requires`, `enforced_by` | Abhängigkeit: A braucht B. |
| **`blocks`** | `blockiert`, `verhindert`, `risiko_für` | Blocker: A verhindert B. |
| **`uses`** | `nutzt`, `verwendet`, `tool` | Werkzeug: A nutzt B. |
| **`guides`** | `steuert`, `leitet`, `orientierung` | Soft-Dependency: A gibt Richtung für B. |
| **`next`** | `danach`, `folgt`, `nachfolger`, `followed_by` | Prozess: A -> B. |
| **`prev`** | `davor`, `vorgänger`, `preceded_by` | Prozess: B <- A. |
| **`related_to`** | `siehe_auch`, `kontext`, `thematisch` | Lose Assoziation. |
| **`similar_to`** | `ähnlich_wie`, `vergleichbar` | Synonym / Ähnlichkeit. |
| **`experienced_in`** | `erfahren_in`, `spezialisiert_in` | Synonym / Ähnlichkeit. |
| **`references`** | *(Kein Alias)* | Standard-Verweis (Fallback). |