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
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.
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
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
"""
import os
@ -39,11 +41,13 @@ from app.core.qdrant_points import (
)
from app.services.embeddings_client import EmbeddingsClient
from app.services.edge_registry import registry as edge_registry
logger = logging.getLogger(__name__)
# --- Helper ---
def load_type_registry(custom_path: Optional[str] = None) -> dict:
"""Lädt die types.yaml zur Steuerung der typ-spezifischen Ingestion."""
import yaml
path = custom_path or os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
if not os.path.exists(path): return {}
@ -52,14 +56,15 @@ def load_type_registry(custom_path: Optional[str] = None) -> dict:
except Exception: return {}
def resolve_note_type(requested: Optional[str], reg: dict) -> str:
"""Bestimmt den finalen Notiz-Typ (Fallback auf 'concept')."""
types = reg.get("types", {})
if requested and requested in types: return requested
return "concept"
def effective_chunk_profile_name(fm: dict, note_type: str, reg: dict) -> str:
"""
Ermittelt den Namen des Chunk-Profils.
Prio: 1. Frontmatter -> 2. Type-Config -> 3. Default
Ermittelt den Namen des zu nutzenden Chunk-Profils.
Priorität: 1. Frontmatter Override -> 2. Type Config -> 3. Global Default
"""
# 1. Frontmatter Override
override = fm.get("chunking_profile") or fm.get("chunk_profile")
@ -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:
"""
Ermittelt das Retriever Weight.
Prio: 1. Frontmatter -> 2. Type-Config -> 3. Default
Ermittelt das effektive retriever_weight für das Scoring.
Priorität: 1. Frontmatter Override -> 2. Type Config -> 3. Global Default
"""
# 1. Frontmatter Override
override = fm.get("retriever_weight")
@ -107,7 +112,7 @@ class IngestionService:
self.registry = load_type_registry()
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")
try:
@ -117,20 +122,13 @@ class IngestionService:
logger.warning(f"DB init warning: {e}")
def _get_chunk_config_by_profile(self, profile_name: str, note_type: str) -> Dict[str, Any]:
"""
Lädt die konkrete Config (target, max, overlap) für einen Profilnamen.
"""
# Suche direkt in den definierten Profilen der Registry
"""Holt die Chunker-Parameter (max, target, overlap) für ein spezifisches Profil."""
profiles = self.registry.get("chunking_profiles", {})
if profile_name in profiles:
cfg = profiles[profile_name].copy()
# Tuple-Fix für Overlap (wie in chunker.py)
if "overlap" in cfg and isinstance(cfg["overlap"], list):
cfg["overlap"] = tuple(cfg["overlap"])
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)
async def process_file(
@ -144,7 +142,10 @@ class IngestionService:
hash_source: str = "parsed",
hash_normalize: str = "canonical"
) -> Dict[str, Any]:
"""
Verarbeitet eine Markdown-Datei und schreibt sie in den Graphen.
Folgt dem 14-Schritte-Workflow.
"""
result = {"path": file_path, "status": "skipped", "changed": False, "error": None}
# 1. Parse & Frontmatter Validation
@ -157,21 +158,25 @@ class IngestionService:
logger.error(f"Validation failed for {file_path}: {e}")
return {**result, "error": f"Validation failed: {str(e)}"}
# 2. Type & Config Resolution (FIXED)
# Wir ermitteln erst den Typ
# --- WP-22: Content Lifecycle Gate (Teil A) ---
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)
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_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["retriever_weight"] = effective_weight
# 3. Build Note Payload
# 3. Build Note Payload (Inkl. Multi-Hash für WP-22)
try:
note_pl = make_note_payload(
parsed,
@ -183,9 +188,11 @@ class IngestionService:
# Text Body Fallback
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["chunk_profile"] = effective_profile
# WP-22: Status speichern
note_pl["status"] = status
note_id = note_pl["note_id"]
except Exception as e:
@ -198,6 +205,7 @@ class IngestionService:
old_payload = self._fetch_note_payload(note_id)
has_old = old_payload is not None
# Prüfung gegen den aktuell konfigurierten Hash-Modus (body oder full)
check_key = f"{self.active_hash_mode}:{hash_source}:{hash_normalize}"
old_hashes = (old_payload or {}).get("hashes")
@ -217,17 +225,16 @@ class IngestionService:
if not apply:
return {**result, "status": "dry-run", "changed": True, "note_id": note_id}
# 5. Processing
# 5. Processing (Chunking, Embedding, Edge Generation)
try:
body_text = getattr(parsed, "body", "") or ""
# FIX: Wir laden jetzt die Config für das SPEZIFISCHE Profil
# (z.B. wenn User "sliding_short" wollte, laden wir dessen Params)
# Konfiguration für das spezifische Profil laden
chunk_config = self._get_chunk_config_by_profile(effective_profile, note_type)
chunks = await assemble_chunks(fm["id"], body_text, fm["type"], config=chunk_config)
# 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)
vecs = []
@ -244,32 +251,47 @@ class IngestionService:
logger.error(f"Embedding failed: {e}")
raise RuntimeError(f"Embedding failed: {e}")
# Kanten generieren
try:
edges = build_edges_for_note(
raw_edges = build_edges_for_note(
note_id,
chunk_pls,
note_level_references=note_pl.get("references", []),
include_note_scope_refs=note_scope_refs
)
except TypeError:
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:
logger.error(f"Processing failed: {e}", exc_info=True)
return {**result, "error": f"Processing failed: {str(e)}"}
# 6. Upsert
# 6. Upsert in Qdrant
try:
# Alte Fragmente löschen, um "Geister-Chunks" zu vermeiden
if purge_before and has_old:
self._purge_artifacts(note_id)
# Note Metadaten
n_name, n_pts = points_for_note(self.prefix, note_pl, None, self.dim)
upsert_batch(self.client, n_name, n_pts)
# Chunks (Vektoren)
if chunk_pls and vecs:
c_name, c_pts = points_for_chunks(self.prefix, chunk_pls, vecs)
upsert_batch(self.client, c_name, c_pts)
# Kanten
if edges:
e_name, e_pts = points_for_edges(self.prefix, edges)
upsert_batch(self.client, e_name, e_pts)
@ -286,8 +308,8 @@ class IngestionService:
logger.error(f"Upsert failed: {e}", exc_info=True)
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]:
"""Holt das aktuelle Payload einer Note aus Qdrant."""
from qdrant_client.http import models as rest
col = f"{self.prefix}_notes"
try:
@ -297,6 +319,7 @@ class IngestionService:
except: return None
def _artifacts_missing(self, note_id: str) -> Tuple[bool, bool]:
"""Prüft, ob Chunks oder Kanten für eine Note fehlen (Integritätscheck)."""
from qdrant_client.http import models as rest
c_col = f"{self.prefix}_chunks"
e_col = f"{self.prefix}_edges"
@ -308,6 +331,7 @@ class IngestionService:
except: return True, True
def _purge_artifacts(self, note_id: str):
"""Löscht alle Chunks und Edges einer Note (vor dem Neu-Schreiben)."""
from qdrant_client.http import models as rest
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
selector = rest.FilterSelector(filter=f)
@ -317,6 +341,7 @@ class IngestionService:
except Exception: pass
async def create_from_text(self, markdown_content: str, filename: str, vault_root: str, folder: str = "00_Inbox") -> Dict[str, Any]:
"""Hilfsmethode zur Erstellung einer Note aus einem Textstream (Editor-Save)."""
target_dir = os.path.join(vault_root, folder)
os.makedirs(target_dir, exist_ok=True)
file_path = os.path.join(target_dir, filename)

View File

@ -1,224 +1,181 @@
"""
FILE: app/core/retriever.py
DESCRIPTION: Implementiert die Hybrid-Suche (Vektor + Graph-Expansion) und das Scoring-Modell (Explainability).
VERSION: 0.5.3
DESCRIPTION: Haupt-Schnittstelle für die Suche. Orchestriert Vektorsuche und Graph-Expansion.
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
DEPENDENCIES: app.config, app.models.dto, app.core.qdrant*, app.services.embeddings_client, app.core.graph_adapter
LAST_ANALYSIS: 2025-12-15
DEPENDENCIES: app.config, app.models.dto, app.core.qdrant*, app.core.graph_adapter, app.core.retriever_scoring
"""
from __future__ import annotations
import os
import time
from functools import lru_cache
import logging
from typing import Any, Dict, List, Tuple, Iterable, Optional
from app.config import get_settings
from app.models.dto import (
QueryRequest,
QueryResponse,
QueryHit,
Explanation,
ScoreBreakdown,
Reason,
EdgeDTO
QueryRequest, QueryResponse, QueryHit,
Explanation, ScoreBreakdown, Reason, EdgeDTO
)
import app.core.qdrant as qdr
import app.core.qdrant_points as qp
import app.services.embeddings_client as ec
import app.core.graph_adapter as ga
try:
import yaml # type: ignore[import]
except Exception: # pragma: no cover
yaml = None # type: ignore[assignment]
# Mathematische Engine importieren
from app.core.retriever_scoring import get_weights, compute_wp22_score
logger = logging.getLogger(__name__)
@lru_cache
def _get_scoring_weights() -> Tuple[float, float, float]:
"""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
# ==============================================================================
# 1. CORE HELPERS & CONFIG LOADERS
# ==============================================================================
def _get_client_and_prefix() -> Tuple[Any, str]:
"""Liefert (QdrantClient, prefix)."""
"""Initialisiert Qdrant Client und lädt Collection-Prefix."""
cfg = qdr.QdrantConfig.from_env()
client = qdr.get_client(cfg)
return client, cfg.prefix
return qdr.get_client(cfg), cfg.prefix
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:
return list(req.query_vector)
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()
model_name = settings.MODEL_NAME
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:
# 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)
def _semantic_hits(
client: Any,
prefix: str,
vector: List[float],
top_k: int,
filters: Dict[str, Any] | None = None,
client: Any,
prefix: str,
vector: List[float],
top_k: int,
filters: Optional[Dict] = None
) -> List[Tuple[str, float, Dict[str, Any]]]:
"""Führt eine semantische Suche aus."""
flt = filters or None
raw_hits = qp.search_chunks_by_vector(client, prefix, vector, top=top_k, filters=flt)
results: List[Tuple[str, float, Dict[str, Any]]] = []
for pid, score, payload in raw_hits:
results.append((str(pid), float(score), dict(payload or {})))
return results
"""Führt die Vektorsuche durch und konvertiert Qdrant-Points in ein einheitliches Format."""
raw_hits = qp.search_chunks_by_vector(client, prefix, vector, top=top_k, filters=filters)
# Strikte Typkonvertierung für Stabilität
return [(str(hit[0]), float(hit[1]), dict(hit[2] or {})) for hit in raw_hits]
def _compute_total_score(
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 ---
# ==============================================================================
# 2. EXPLANATION LAYER (DEBUG & VERIFIABILITY)
# ==============================================================================
def _build_explanation(
semantic_score: float,
payload: Dict[str, Any],
edge_bonus: float,
cent_bonus: float,
scoring_debug: Dict[str, Any],
subgraph: Optional[ga.Subgraph],
node_key: Optional[str]
target_note_id: Optional[str],
applied_boosts: Optional[Dict[str, float]] = None
) -> Explanation:
"""Erstellt ein Explanation-Objekt."""
sem_w, _edge_w, _cent_w = _get_scoring_weights()
# Scoring weights erneut laden für Reason-Details
_, edge_w_cfg, cent_w_cfg = _get_scoring_weights()
try:
type_weight = float(payload.get("retriever_weight", 1.0))
except (TypeError, ValueError):
type_weight = 1.0
note_type = payload.get("type", "unknown")
"""
Transformiert mathematische Scores und Graph-Signale in eine menschenlesbare Erklärung.
Behebt Pydantic ValidationErrors durch explizite String-Sicherung.
"""
_, edge_w_cfg, _ = get_weights()
base_val = scoring_debug["base_val"]
# 1. Detaillierter mathematischer Breakdown
breakdown = ScoreBreakdown(
semantic_contribution=(sem_w * semantic_score * type_weight),
edge_contribution=(edge_w_cfg * edge_bonus),
centrality_contribution=(cent_w_cfg * cent_bonus),
semantic_contribution=base_val,
edge_contribution=base_val * scoring_debug["edge_impact_final"],
centrality_contribution=base_val * scoring_debug["cent_impact_final"],
raw_semantic=semantic_score,
raw_edge_bonus=edge_bonus,
raw_centrality=cent_bonus,
node_weight=type_weight
raw_edge_bonus=scoring_debug["edge_bonus"],
raw_centrality=scoring_debug["cent_bonus"],
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] = []
edges_dto: List[EdgeDTO] = []
# 2. Gründe für Semantik hinzufügen
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:
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:
msg = "Bevorzugt" if type_weight > 1.0 else "Leicht abgewertet"
reasons.append(Reason(kind="type", message=f"{msg} aufgrund des Typs '{note_type}'.", score_impact=(sem_w * semantic_score * (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"))
msg = "Bevorzugt" if type_weight > 1.0 else "De-priorisiert"
reasons.append(Reason(kind="type", message=f"{msg} durch Typ-Profil.", score_impact=base_val * (type_weight - 1.0)))
# 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"):
incoming = subgraph.get_incoming_edges(node_key)
for edge in incoming:
src = edge.get("source", "Unknown")
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"))
raw_edges.extend(subgraph.get_incoming_edges(target_note_id) or [])
if hasattr(subgraph, "get_outgoing_edges"):
raw_edges.extend(subgraph.get_outgoing_edges(target_note_id) or [])
for edge in raw_edges:
# FIX: Zwingende String-Konvertierung für Pydantic-Stabilität
src = str(edge.get("source") or "note_root")
tgt = str(edge.get("target") or target_note_id or "unknown_target")
kind = str(edge.get("kind", "related_to"))
prov = str(edge.get("provenance", "rule"))
conf = float(edge.get("confidence", 1.0))
direction = "in" if tgt == target_note_id else "out"
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)
all_edges = sorted(edges_dto, key=lambda e: e.weight, reverse=True)
for top_edge in all_edges[:3]:
impact = edge_w_cfg * top_edge.weight
dir_txt = "Verweist auf" if top_edge.direction == "out" else "Referenziert von"
tgt_txt = top_edge.target if top_edge.direction == "out" else top_edge.source
reasons.append(Reason(kind="edge", message=f"{dir_txt} '{tgt_txt}' via '{top_edge.kind}'", score_impact=impact, details={"kind": top_edge.kind}))
# 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 ""
reasons.append(Reason(
kind="edge",
message=f"{prov_txt} Kante '{e.kind}'{boost_txt} von/zu '{peer}'.",
score_impact=edge_w_cfg * e.confidence
))
if cent_bonus > 0.01:
reasons.append(Reason(kind="centrality", message="Knoten liegt zentral im Kontext.", score_impact=breakdown.centrality_contribution))
if scoring_debug["cent_bonus"] > 0.01:
reasons.append(Reason(kind="centrality", message="Die Notiz ist ein zentraler Informations-Hub.", score_impact=breakdown.centrality_contribution))
return Explanation(breakdown=breakdown, reasons=reasons, related_edges=edges_dto if edges_dto else None)
def _extract_expand_options(req: QueryRequest) -> Tuple[int, List[str] | None]:
"""Extrahiert depth und edge_types."""
expand = getattr(req, "expand", None)
if not expand:
return 0, None
depth = 1
edge_types: List[str] | None = None
if hasattr(expand, "depth") or hasattr(expand, "edge_types"):
depth = int(getattr(expand, "depth", 1) or 1)
types_val = getattr(expand, "edge_types", None)
if types_val:
edge_types = list(types_val)
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
return Explanation(
breakdown=breakdown,
reasons=reasons,
related_edges=edges_dto if edges_dto else None,
applied_boosts=applied_boosts
)
# ==============================================================================
# 3. CORE RETRIEVAL PIPELINE
# ==============================================================================
def _build_hits_from_semantic(
hits: Iterable[Tuple[str, float, Dict[str, Any]]],
@ -226,113 +183,128 @@ def _build_hits_from_semantic(
used_mode: str,
subgraph: ga.Subgraph | None = None,
explain: bool = False,
dynamic_edge_boosts: Dict[str, float] = None
) -> QueryResponse:
"""Baut strukturierte QueryHits."""
"""Wandelt semantische Roh-Treffer in hochgeladene, bewertete QueryHits um."""
t0 = time.time()
enriched: List[Tuple[str, float, Dict[str, Any], float, float, float]] = []
enriched = []
for pid, semantic_score, payload in hits:
edge_bonus = 0.0
cent_bonus = 0.0
node_key = payload.get("chunk_id") or payload.get("note_id")
edge_bonus, cent_bonus = 0.0, 0.0
target_id = payload.get("note_id")
if subgraph is not None and node_key:
if subgraph and target_id:
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:
edge_bonus = 0.0
try:
cent_bonus = float(subgraph.centrality_bonus(node_key))
except Exception:
cent_bonus = 0.0
pass
total, edge_bonus, cent_bonus = _compute_total_score(semantic_score, payload, edge_bonus=edge_bonus, cent_bonus=cent_bonus)
enriched.append((pid, float(semantic_score), payload, total, edge_bonus, cent_bonus))
# Mathematisches Scoring via WP-22 Engine
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)
limited = enriched_sorted[: max(1, top_k)]
# Sortierung nach finalem mathematischen Score
enriched_sorted = sorted(enriched, key=lambda h: h[3]["total"], reverse=True)
limited_hits = enriched_sorted[: max(1, top_k)]
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
if explain:
explanation_obj = _build_explanation(
semantic_score=float(semantic_score),
payload=payload,
edge_bonus=edge_bonus,
cent_bonus=cent_bonus,
semantic_score=float(s_score),
payload=pl,
scoring_debug=dbg,
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(
node_id=str(pid),
note_id=payload.get("note_id"),
semantic_score=float(semantic_score),
edge_bonus=edge_bonus,
centrality_bonus=cent_bonus,
total_score=total,
paths=None,
note_id=str(pl.get("note_id", "unknown")),
semantic_score=float(s_score),
edge_bonus=dbg["edge_bonus"],
centrality_bonus=dbg["cent_bonus"],
total_score=dbg["total"],
source={
"path": payload.get("path"),
"section": payload.get("section") or payload.get("section_title"),
"path": pl.get("path"),
"section": pl.get("section") or pl.get("section_title"),
"text": text_content
},
# --- FIX: Wir füllen das payload-Feld explizit ---
payload=payload,
payload=pl,
explanation=explanation_obj
))
dt = 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)
return QueryResponse(results=results, used_mode=used_mode, latency_ms=int((time.time() - t0) * 1000))
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()
if req.query_vector:
vector = list(req.query_vector)
else:
vector = _get_query_vector(req)
top_k = req.top_k or get_settings().RETRIEVER_TOP_K
vector = list(req.query_vector) if req.query_vector else _get_query_vector(req)
top_k = req.top_k or 10
# 1. Semantische Seed-Suche
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
if depth and depth > 0:
seed_ids: List[str] = []
for _pid, _score, payload in hits:
key = payload.get("chunk_id") or payload.get("note_id")
if key and key not in seed_ids:
seed_ids.append(key)
if depth > 0 and hits:
# Start-IDs für den Graph-Traversal sammeln
seed_ids = list({h[2].get("note_id") for h in hits if h[2].get("note_id")})
if seed_ids:
try:
subgraph = ga.expand(client, prefix, seed_ids, depth=depth, edge_types=edge_types)
except Exception:
# Subgraph aus RAM/DB laden
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
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:
"""
Wrapper-Klasse für WP-05 (Chat).
"""
def __init__(self):
pass
"""Schnittstelle für die asynchrone Suche."""
async def search(self, request: QueryRequest) -> QueryResponse:
"""Führt eine hybride Suche aus."""
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
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
DEPENDENCIES: pydantic, typing, uuid
LAST_ANALYSIS: 2025-12-15
LAST_ANALYSIS: 2025-12-18
"""
from __future__ import annotations
@ -12,7 +12,8 @@ from pydantic import BaseModel, Field
from typing import List, Literal, Optional, Dict, Any
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 ---
@ -40,6 +41,8 @@ class EdgeDTO(BaseModel):
target: str
weight: float
direction: Literal["out", "in", "undirected"] = "out"
provenance: Optional[Literal["explicit", "rule", "smart", "structure"]] = "explicit"
confidence: float = 1.0
# --- Request Models ---
@ -56,18 +59,18 @@ class QueryRequest(BaseModel):
filters: Optional[Dict] = None
ret: Dict = {"with_paths": True, "with_notes": True, "with_chunks": True}
explain: bool = False
# WP-22: Semantic Graph Routing
boost_edges: Optional[Dict[str, float]] = None
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")
# 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'")
# Update: Range auf 1-5 erweitert für differenziertes Tuning
score: int = Field(..., ge=1, le=5, description="1 (Irrelevant/Falsch) bis 5 (Perfekt)")
score: int = Field(..., ge=1, le=5, description="1 (Irrelevant) bis 5 (Perfekt)")
comment: Optional[str] = None
@ -76,8 +79,7 @@ class ChatRequest(BaseModel):
WP-05: Request für /chat.
"""
message: str = Field(..., description="Die Nachricht des Users")
conversation_id: Optional[str] = Field(None, description="Optional: ID für Chat-Verlauf (noch nicht implementiert)")
# RAG Parameter (Override defaults)
conversation_id: Optional[str] = Field(None, description="ID für Chat-Verlauf")
top_k: int = 5
explain: bool = False
@ -85,7 +87,7 @@ class ChatRequest(BaseModel):
# --- WP-04b Explanation Models ---
class ScoreBreakdown(BaseModel):
"""Aufschlüsselung der Score-Komponenten."""
"""Aufschlüsselung der Score-Komponenten nach der WP-22 Formel."""
semantic_contribution: float
edge_contribution: float
centrality_contribution: float
@ -93,11 +95,14 @@ class ScoreBreakdown(BaseModel):
raw_edge_bonus: float
raw_centrality: 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):
"""Ein semantischer Grund für das Ranking."""
kind: Literal["semantic", "edge", "type", "centrality"]
kind: Literal["semantic", "edge", "type", "centrality", "lifecycle"]
message: str
score_impact: Optional[float] = None
details: Optional[Dict[str, Any]] = None
@ -108,6 +113,9 @@ class Explanation(BaseModel):
breakdown: ScoreBreakdown
reasons: List[Reason]
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 ---
@ -115,14 +123,14 @@ class Explanation(BaseModel):
class QueryHit(BaseModel):
"""Einzelnes Trefferobjekt für /query."""
node_id: str
note_id: Optional[str]
note_id: str
semantic_score: float
edge_bonus: float
centrality_bonus: float
total_score: float
paths: Optional[List[List[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
@ -146,11 +154,9 @@ class ChatResponse(BaseModel):
"""
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")
sources: List[QueryHit] = Field(..., description="Die für die Antwort genutzten Quellen")
sources: List[QueryHit] = Field(..., description="Die genutzten Quellen")
latency_ms: int
intent: Optional[str] = Field("FACT", description="WP-06: Erkannter Intent (FACT/DECISION)")
intent_source: Optional[str] = Field("Unknown", description="WP-06: Quelle der Intent-Erkennung (Keyword vs. LLM)")
intent: Optional[str] = Field("FACT", description="WP-06: Erkannter Intent")
intent_source: Optional[str] = Field("Unknown", description="Quelle der Intent-Erkennung")

View File

@ -1,11 +1,10 @@
"""
FILE: app/routers/chat.py
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
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
LAST_ANALYSIS: 2025-12-15
"""
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)"
# 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):
types_cfg = get_types_config()
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
if settings.get("llm_fallback_enabled", False):
# Nutze Prompts aus prompts.yaml (via LLM Service)
router_prompt_template = llm.prompts.get("router_prompt", "")
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...")
try:
# Nutze priority="realtime" für den Router, damit er nicht wartet
raw_response = await llm.generate_raw_response(prompt, priority="realtime")
llm_output_upper = raw_response.upper()
# Zuerst INTERVIEW prüfen
if "INTERVIEW" in llm_output_upper or "CREATE" in llm_output_upper:
return "INTERVIEW", "LLM Router"
@ -281,12 +274,20 @@ async def chat_endpoint(
# --- RAG MODE ---
inject_types = strategy.get("inject_types", [])
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=request.message,
mode="hybrid",
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)
hits = retrieve_result.results
@ -297,7 +298,9 @@ async def chat_endpoint(
mode="hybrid",
top_k=3,
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)
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
# Steuerung der Decision Engine (Intent Recognition)
# Version: 2.4.0 (Clean Architecture: Generic Intents only)
# Steuerung der Decision Engine (Intent Recognition & Graph Routing)
# Version: 2.5.0 (WP-22: Semantic Graph Routing)
version: 1.4
version: 2.5
settings:
llm_fallback_enabled: true
@ -37,6 +37,12 @@ strategies:
description: "Reine Wissensabfrage."
trigger_keywords: []
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"
prepend_instruction: null
@ -53,6 +59,12 @@ strategies:
- "abwägung"
- "vergleich"
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"
prepend_instruction: |
!!! ENTSCHEIDUNGS-MODUS !!!
@ -71,6 +83,12 @@ strategies:
- "überfordert"
- "müde"
inject_types: ["experience", "belief", "profile"]
# WP-22: Weiche Assoziationen & Erfahrungen stärken
edge_boosts:
based_on: 2.0
related_to: 2.0
experienced_in: 2.5
blocks: 0.1
prompt_template: "empathy_template"
prepend_instruction: null
@ -88,11 +106,16 @@ strategies:
- "yaml"
- "bash"
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"
prepend_instruction: null
# 5. Interview / Datenerfassung
# HINWEIS: Spezifische Typen (Projekt, Ziel etc.) werden automatisch
# HINWEIS: Spezifische Typen (Projekt, Ziel etc.) werden automatisch
# über die types.yaml erkannt. Hier stehen nur generische Trigger.
INTERVIEW:
description: "Der User möchte Wissen erfassen."
@ -108,9 +131,9 @@ strategies:
- "idee speichern"
- "draft"
inject_types: []
edge_boosts: {}
prompt_template: "interview_template"
prepend_instruction: null
# Schemas: Hier nur der Fallback.
# Spezifische Schemas (Project, Experience) kommen jetzt aus types.yaml!
schemas:

View File

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

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
scope: graph, logic, provenance
status: active
version: 2.6
context: "Fachliche Beschreibung des Wissensgraphen: Knoten, Kanten, Provenance und Matrix-Logik."
version: 2.7.0
context: "Fachliche Beschreibung des Wissensgraphen: Knoten, Kanten, Provenance, Matrix-Logik und WP-22 Scoring-Prinzipien."
---
# 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.
@ -19,7 +19,7 @@ Der Graph besteht aus zwei Ebenen von Knoten.
### 1.1 Die Note (Das Fachobjekt)
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`).
* **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.
* **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)
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". |
| `depends_on` | Was ist Voraussetzung? | Projekt braucht "Budget". |
| `caused_by` | Warum ist das passiert? | Bug durch "Commit X". |
| `blocks` | Was steht im Weg? | Risiko blockiert "Release". |
| `based_on` | Worauf fußt das? | Erfahrung basiert auf "Wert Y". |
| `derived_from` | Woher kommt das? | Prinzip stammt aus "Buch Z". |
| `related_to` | Was ist ähnlich? | "Hund" ist verwandt mit "Wolf". |
| `solves` | Was ist die Lösung? | "Qdrant" löst "Vektorsuche". |
| `caused_by` | `ausgelöst_durch`, `wegen` | Kausalität: A löst B aus. |
| `derived_from` | `abgeleitet_von`, `quelle` | Herkunft: A stammt von B. |
| `based_on` | `basiert_auf`, `fundament` | Fundament: B baut auf A auf. |
| `solves` | `löst`, `fix_für` | Lösung: A ist Lösung für Problem B. |
| `part_of` | `teil_von`, `cluster` | Hierarchie: Kind -> Eltern. |
| `depends_on` | `braucht`, `requires` | Abhängigkeit: A braucht B. |
| `blocks` | `blockiert`, `risiko_für` | Blocker: A verhindert B. |
| `related_to` | `siehe_auch`, `kontext` | Lose Assoziation. |
### 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)**
* *Quelle:* Inline-Links (`[[rel:...]]`) oder Wikilinks im Text.
* *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)**
* *Quelle:* Smart Edge Allocation (WP15).
* *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)**
* *Quelle:* `types.yaml` Defaults.
* *Quelle:* `types.yaml` Defaults oder Matrix-Logik.
* *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)
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:**
* **Quelle `experience` → Ziel `value`**
* *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).
* **Quelle `experience` → Ziel `value`**: Wird zu `based_on` (Erfahrungen fußen auf Werten).
* **Quelle `principle` → Ziel `source`**: Wird zu `derived_from` (Prinzipien stammen aus Quellen).
---
## 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.
* **Stabile IDs:** Importiert man dieselbe Datei zweimal, ändern sich die IDs der Knoten nicht.
* **Keine Duplikate:** Kanten werden dedupliziert. Die "stärkere" Quelle (Explicit > Smart > Rule) gewinnt.
* **Lösch-Garantie:** Wenn eine Notiz gelöscht wird, verschwinden auch alle ihre Chunks und ausgehenden Kanten (via `--sync-deletes`).
* **Stabile IDs:** Deterministische IDs verhindern Duplikate bei Re-Imports.
* **Deduplizierung:** Kanten werden anhand ihrer Identität erkannt. Die "stärkere" Provenance gewinnt.

View File

@ -1,19 +1,19 @@
---
doc_type: technical_reference
audience: developer, admin
scope: configuration, env
scope: configuration, env, registry, scoring
status: active
version: 2.7.0
context: "Referenztabellen für Umgebungsvariablen und YAML-Konfigurationen."
version: 2.7.2
context: "Umfassende Referenztabellen für Umgebungsvariablen, YAML-Konfigurationen und die Edge Registry Struktur."
---
# 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`)
Diese Variablen steuern die Infrastruktur, Timeouts und Feature-Flags.
Diese Variablen steuern die Infrastruktur, Pfade und globale Timeouts.
| 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. |
| `COLLECTION_PREFIX` | `mindnet` | Namensraum für Collections (erzeugt `{prefix}_notes` etc). |
| `VECTOR_DIM` | `768` | **Muss 768 sein** (für Nomic Embeddings). |
| `MINDNET_VOCAB_PATH` | *(Pfad)* | **Neu (WP-22):** Absoluter Pfad zur `01_edge_vocabulary.md`. Definiert den Ort des Dictionarys. |
| `MINDNET_VAULT_ROOT` | `./vault` | Basis-Pfad für Datei-Operationen. Dient als Fallback-Basis, falls `MINDNET_VOCAB_PATH` nicht gesetzt ist. |
| `MINDNET_TYPES_FILE` | `config/types.yaml` | Pfad zur Typ-Registry. |
| `MINDNET_RETRIEVER_CONFIG`| `config/retriever.yaml`| Pfad zur Scoring-Konfiguration. |
| `MINDNET_DECISION_CONFIG` | `config/decision_engine.yaml` | Pfad zur Router & Intent Config. |
@ -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_LLM_TIMEOUT` | `300.0` | Timeout in Sekunden (Erhöht für CPU Cold-Starts). |
| `MINDNET_API_TIMEOUT` | `300.0` | Frontend Timeout (Erhöht für Smart Edge Wartezeiten). |
| `MINDNET_LLM_BACKGROUND_LIMIT`| `2` | **Traffic Control (Neu):** Max. parallele Import-Tasks (Semaphore). |
| `MINDNET_VAULT_ROOT` | `./vault` | Pfad für Write-Back Operationen (Drafts). |
| `MINDNET_CHANGE_DETECTION_MODE` | `full` | **Change Detection (Neu):** `full` (Text + Meta) oder `body` (nur Text). |
| `MINDNET_LLM_BACKGROUND_LIMIT`| `2` | **Traffic Control:** Max. parallele Hintergrund-Tasks (Semaphore). |
| `MINDNET_CHANGE_DETECTION_MODE` | `full` | `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)
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.
* *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` (z.B. `concept`) aus `types.yaml`.
2. **Type Config:** Der Standardwert für den `type` aus `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. |
| **project** | `sliding_smart_edges` | 0.97 | Ja | Aktive Vorhaben. |
| **decision** | `structured_smart_edges_strict` | 1.00 | Ja | Entscheidungen (ADRs). Atomar. |
| **experience** | `sliding_smart_edges` | 0.90 | Ja | Persönliche Learnings. |
| **journal** | `sliding_standard` | 0.80 | Nein | Logs / Dailies. |
| **value** | `structured_smart_edges_strict` | 1.00 | Ja | Werte/Prinzipien. Atomar. |
| **decision** | `structured_strict` | 1.00 | Ja | Entscheidungen. Atomar. |
| **value** | `structured_strict` | 1.00 | Ja | Werte/Prinzipien. Atomar. |
| **project** | `sliding_smart` | 0.97 | Ja | Aktive Vorhaben. |
| **experience** | `sliding_smart` | 0.90 | Ja | Persönliche Learnings. |
| **concept** | `sliding_smart` | 0.60 | Ja | Abstrakte Begriffe. |
| **principle** | `structured_L3` | 0.95 | Nein | Prinzipien (Tiefer Split). |
| **belief** | `sliding_short` | 0.90 | Nein | Glaubenssätze. |
| **risk** | `sliding_short` | 0.90 | Nein | Risiken. |
| **journal** | `sliding_standard` | 0.80 | Nein | Logs / Dailies. |
| **person** | `sliding_standard` | 0.50 | Nein | Profile. |
| **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. |
| **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`)
Steuert die Gewichtung der Scoring-Formel (WP04a).
**Beispielkonfiguration:**
Steuert die Gewichtung der Scoring-Formel und die neuen Lifecycle-Modifier.
```yaml
scoring:
semantic_weight: 1.0 # Basis-Relevanz (Cosine Similarity)
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:
# Multiplikatoren für den Edge-Bonus
depends_on: 1.5 # Harte Abhängigkeiten stark gewichten
blocks: 1.5 # Risiken stark gewichten
caused_by: 1.2 # Kausalitäten stärken
related_to: 0.5 # Weiche Themen schwächer gewichten
blocks: 1.5 # Blocker/Risiken stark gewichten
caused_by: 1.2 # Kausalitäten moderat stärken
based_on: 1.3 # Werte-Bezug stärken
related_to: 0.5 # Weiche Assoziation schwächen
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. |
| `next` / `prev` | Ja | Struktur | Lesereihenfolge. |
| `depends_on` | Nein | Inline / Default | Harte Abhängigkeit. |
| `related_to` | Ja | Callout / Default | Thematische Nähe. |
| `similar_to` | Ja | Inline | Inhaltliche Ähnlichkeit. |
| `caused_by` | Nein | Inline | Kausalität. |
| `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). |
| `caused_by` | Nein | Inline | Kausalität: A löst B aus. |
| `based_on` | Nein | Matrix | Fundament: A fußt auf B. |
| `blocks` | Nein | Inline | Blocker: A verhindert B. |
| `solves` | Nein | Inline | Lösung: A ist Lösung für Problem B. |
| `next` / `prev` | Ja | Struktur | Sequenzielle Lesereihenfolge. |
---
## 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
# Strategie-Definitionen
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"]
intents:
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"]
```
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
audience: developer, devops
scope: backend, ingestion, smart_edges
scope: backend, ingestion, smart_edges, edge_registry
status: active
version: 2.7.0
version: 2.7.1
context: "Detaillierte technische Beschreibung der Import-Pipeline, Chunking-Strategien und CLI-Befehle."
---
# 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**.
@ -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`.
* **CLI:** Iteriert über Dateien und nutzt `asyncio.Semaphore` zur Drosselung.
2. **Markdown lesen:** Rekursives Scannen des Vaults.
3. **Frontmatter extrahieren:** Validierung von Pflichtfeldern (`id`, `type`, `title`).
4. **Config Resolution:**
3. **Frontmatter Check & Hard Skip (WP-22):**
* 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`.
* **Priorität:** 1. Frontmatter (Override) -> 2. `types.yaml` (Type) -> 3. Default.
5. **Note-Payload generieren:**
* Erstellen des JSON-Objekts für `mindnet_notes`.
* **Priorität:** 1. Frontmatter (Override) -> 2. `types.yaml` (Type) -> 3. Global Default.
6. **Note-Payload generieren:**
* Erstellen des JSON-Objekts inklusive `status` (für Scoring).
* **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.
* 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. **Smart Edge Allocation (WP15):**
8. **Chunking anwenden:** Zerlegung des Textes basierend auf dem ermittelten Profil (siehe Kap. 3).
9. **Smart Edge Allocation (WP15):**
* Wenn `enable_smart_edge_allocation: true`: Der `SemanticAnalyzer` sendet Chunks an das LLM.
* **Traffic Control:** Request nutzt `priority="background"`. Semaphore (Limit via `.env`) drosselt die Last.
* **Resilienz:** Bei Timeout (Ollama) greift ein Fallback (Broadcasting an alle Chunks).
9. **Inline-Kanten finden:** Parsing von `[[rel:...]]`.
10. **Callout-Kanten finden:** Parsing von `> [!edge]`.
11. **Default-Edges erzeugen:** Anwendung der `edge_defaults` aus Registry.
12. **Strukturkanten erzeugen:** `belongs_to`, `next`, `prev`.
13. **Embedding (Async):** Generierung via `nomic-embed-text` (768 Dim).
14. **Diagnose:** Integritäts-Check nach dem Lauf.
10. **Inline-Kanten finden:** Parsing von `[[rel:...]]`.
11. **Alias-Auflösung & Kanonisierung (WP-22):**
* Jede Kante wird via `edge_registry.resolve()` normalisiert.
* Aliase (z.B. `basiert_auf`) werden zu kanonischen Typen (z.B. `based_on`) aufgelöst.
* Unbekannte Typen werden in `unknown_edges.jsonl` protokolliert.
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).
### 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
# 0. Modell sicherstellen
@ -87,18 +99,18 @@ python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --
## 3. Chunking & Payload
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 |
| :--- | :--- | :--- | :--- |
| `sliding_short` | `sliding_window` | Max: 350, Target: 200 | Kurze Logs, Chats, Risiken. |
| `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). |
| `structured_smart_edges` | `by_heading` | `strict: false` (Soft) | Strukturierte Texte, wo kleine Abschnitte gemergt werden dürfen. |
| `structured_smart_edges_strict` | `by_heading` | `strict: true` (Hard) | **Atomare Einheiten**: Entscheidungen, Werte, Profile. |
| `structured_smart_edges_strict_L3`| `by_heading` | `strict: true`, `level: 3` | Tief geschachtelte Prinzipien (Tier 2/MP1 Logik). |
| `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, Merging erlaubt. |
| `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). |
### 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).
* *Safety Net:* Wird ein Abschnitt zu lang (> `max` Token), wird auch ohne Überschrift getrennt.
* **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.
* *Safety Net:* Auch hier greift das `max` Token Limit.
### 3.3 Payload-Felder (Qdrant)
* `text`: Der reine Inhalt (Anzeige im UI). Überschriften bleiben erhalten.
* `window`: Inhalt plus Overlap (für Embedding). Bei `by_heading` wird der Kontext (Eltern-Überschrift) oft vorangestellt.
* `text`: Der reine Inhalt (Anzeige im UI).
* `window`: Inhalt plus Overlap (für Embedding).
* `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.
| Prio | Quelle | Rule ID | Confidence |
| :--- | :--- | :--- | :--- |
| **1** | Inline | `inline:rel` | 0.95 |
| **2** | Callout | `callout:edge` | 0.90 |
| **3** | Wikilink | `explicit:wikilink` | 1.00 |
| **4** | Smart Edge | `smart:llm_filter` | 0.90 |
| **5** | Type Default | `edge_defaults` | 0.70 |
| **6** | Struktur | `structure` | 1.00 |
| Prio | Quelle | Rule ID | Confidence | Erläuterung |
| :--- | :--- | :--- | :--- | :--- |
| **1** | Wikilink | `explicit:wikilink` | **1.00** | Harte menschliche Setzung. |
| **2** | Inline | `inline:rel` | **0.95** | Typisierte menschliche Kante. |
| **3** | Callout | `callout:edge` | **0.90** | Explizite Meta-Information. |
| **4** | Smart Edge | `smart:llm_filter` | **0.90** | KI-validierte Verbindung. |
| **5** | Type Default | `edge_defaults` | **0.70** | Heuristik aus der Registry. |
| **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.
```bash
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).
```bash
python3 -m scripts.edges_full_check

View File

@ -1,25 +1,25 @@
---
doc_type: technical_reference
audience: developer, data_scientist
scope: backend, retrieval, scoring
scope: backend, retrieval, scoring, modularization
status: active
version: 2.6
context: "Formeln und Algorithmen des Hybrid Retrievers und Explanation Layer."
version: 2.7.1
context: "Detaillierte Dokumentation der Scoring-Algorithmen, inklusive WP-22 Lifecycle-Modifier, Intent-Boosting und Modularisierung."
---
# 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}$):**
* **Basis:** Cosine Similarity des Vektors.
@ -27,87 +27,103 @@ $$
* **Wertebereich:** 0.0 bis 1.0.
**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.
* **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}$):**
* **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.
**4. Centrality Bonus ($B_{cent}$):**
* **Kontext:** Berechnet im lokalen Subgraphen.
* **Logik:** Eine vereinfachte PageRank-Metrik (Degree Centrality) im temporären Graphen.
* **Zweck:** Belohnt "Hubs", die viele Verbindungen zu anderen Treffern haben.
* **Logik:** Vereinfachte PageRank-Metrik (Degree Centrality).
* **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)**
* Suche die Top-K (z.B. 20) Kandidaten via Embeddings in Qdrant.
* Dies sind die "Seeds" für den Graphen.
* Der Orchestrator sucht Top-K (Standard: 20) Kandidaten via Embeddings in Qdrant.
* Diese bilden die "Seeds" für den Graphen.
**Phase 2: Graph Expansion**
* Nutze `graph_adapter.expand(seeds, depth=1)`.
* Lade alle direkten Nachbarn (Incoming & Outgoing) der Seeds aus der `_edges` Collection.
* Konstruiere einen temporären `NetworkX`-Graphen im Speicher (`Subgraph`).
* Lade direkte Nachbarn aus der `_edges` Collection.
* Konstruiere einen `NetworkX`-Graphen im Speicher.
**Phase 3: Re-Ranking**
* Berechne für jeden Knoten im Subgraphen die Boni ($B_{edge}$, $B_{cent}$).
* Wende die Scoring-Formel an.
* Sortiere die Liste neu absteigend nach `TotalScore`.
* Schneide die Liste beim finalen Limit (z.B. 5) ab.
**Phase 3: Re-Ranking (Modular)**
* Der Orchestrator übergibt den Graphen und die Seeds an die `ScoringEngine`.
* Berechne Boni ($B_{edge}$, $B_{cent}$) sowie die neuen Lifecycle- und Intent-Modifier.
* Sortierung absteigend nach `TotalScore` und Limitierung auf Top-Resultate (z.B. 5).
---
## 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
{
"score_breakdown": {
"semantic": 0.85,
"type_boost": 1.0,
"lifecycle_modifier": 0.5,
"edge_bonus": 0.4,
"intent_boost": 0.5,
"centrality": 0.1
},
"reasons": [
"Hohe textuelle Übereinstimmung (>0.85).",
"Bevorzugt, da Typ 'decision' (Gewicht 1.0).",
"Wird referenziert von 'Projekt Alpha' via 'depends_on'."
"Status 'draft' reduziert Relevanz (Modifier 0.5).",
"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`)
Diese Datei steuert die Balance der Formel.
Steuert die Gewichtung der mathematischen Komponenten.
```yaml
scoring:
semantic_weight: 1.0 # Basis-Relevanz (sollte immer ca. 1.0 sein)
edge_weight: 0.7 # Einfluss des Graphen (0.5 - 1.0 empfohlen)
centrality_weight: 0.5 # Einfluss der Zentralität (Hub-Bonus)
semantic_weight: 1.0 # Basis-Relevanz
edge_weight: 0.7 # Graphen-Einfluss
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:
depends_on: 1.5 # Harte Abhängigkeiten sehr stark gewichten
blocks: 1.5 # Risiken/Blocker stark gewichten
caused_by: 1.2 # Kausalitäten moderat stärken
related_to: 0.5 # Weiche Themen schwächer gewichten
depends_on: 1.5 # Harte Abhängigkeiten
blocks: 1.5 # Risiken/Blocker
caused_by: 1.2 # Kausalitäten
based_on: 1.3 # Werte-Bezug
related_to: 0.5 # Weiche Themen
references: 0.8 # Standard-Referenzen
```

View File

@ -1,10 +1,10 @@
---
doc_type: operations_manual
audience: admin, devops
scope: deployment, maintenance, backup
scope: deployment, maintenance, backup, edge_registry
status: active
version: 2.6
context: "Installationsanleitung, Systemd-Units und Wartungsprozesse."
version: 2.7.0
context: "Installationsanleitung, Systemd-Units und Wartungsprozesse für Mindnet v2.7."
---
# Admin Operations Guide
@ -58,6 +58,7 @@ User=llmadmin
Group=llmadmin
WorkingDirectory=/home/llmadmin/mindnet
# 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
Restart=always
RestartSec=5
@ -96,7 +97,7 @@ WantedBy=multi-user.target
---
## 3. Wartung & Cronjobs
## 3. Wartung & Monitoring
### 3.1 Regelmäßiger Import (Cron)
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
```
### 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'"**
* 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
pip uninstall streamlit-cytoscapejs
pip install st-cytoscape
# Oder sicherheitshalber:
pip install -r requirements.txt
```
**Fehler: "Vector dimension error: expected 768, got 384"**

View File

@ -2,6 +2,119 @@
doc_type: roadmap
audience: product_owner, developer
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
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-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. |
---
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
* **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)
### 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.
**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.
- 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: Mittel
- Komplexität: Niedrig/Mittel

View File

@ -253,4 +253,66 @@ Bitte bestätige die Übernahme, erstelle die `edge_mappings.yaml` Struktur und
* Wir haben den `HybridRouter` (aus WP-06) schon. Er "versteht" bereits, was der User will (`DECISION` vs `EMPATHY`).
* 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). |