Merge branch 'WP22'
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 3s
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 3s
This commit is contained in:
commit
7f707cffb9
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
120
app/core/retriever_scoring.py
Normal file
120
app/core/retriever_scoring.py
Normal 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
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
111
app/services/edge_registry.py
Normal file
111
app/services/edge_registry.py
Normal 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()
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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).*
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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"**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
145
tests/test_WP22_intelligence.py
Normal file
145
tests/test_WP22_intelligence.py
Normal 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()
|
||||
31
vault_master/01_User_Manual/01_edge_vocabulary.md
Normal file
31
vault_master/01_User_Manual/01_edge_vocabulary.md
Normal 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). |
|
||||
Loading…
Reference in New Issue
Block a user