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

Reviewed-on: #18
This commit is contained in:
Lars 2025-12-31 16:56:36 +01:00
commit 67d7154328
10 changed files with 650 additions and 112 deletions

View File

@ -1,13 +1,15 @@
""" """
FILE: app/core/graph/graph_db_adapter.py FILE: app/core/graph/graph_db_adapter.py
DESCRIPTION: Datenbeschaffung aus Qdrant für den Graphen. DESCRIPTION: Datenbeschaffung aus Qdrant für den Graphen.
AUDIT v1.1.0: Nutzt nun die zentrale database-Infrastruktur für Namen. AUDIT v1.1.1: Volle Unterstützung für WP-15c Metadaten.
Stellt sicher, dass 'target_section' und 'provenance' für die
Super-Edge-Aggregation im Retriever geladen werden.
""" """
from typing import List, Dict, Optional from typing import List, Dict, Optional
from qdrant_client import QdrantClient from qdrant_client import QdrantClient
from qdrant_client.http import models as rest from qdrant_client.http import models as rest
# ENTSCHEIDENDER FIX: Nutzt die neue Infrastruktur für konsistente Collection-Namen # Nutzt die zentrale Infrastruktur für konsistente Collection-Namen (WP-14)
from app.core.database import collection_names from app.core.database import collection_names
def fetch_edges_from_qdrant( def fetch_edges_from_qdrant(
@ -19,14 +21,16 @@ def fetch_edges_from_qdrant(
) -> List[Dict]: ) -> List[Dict]:
""" """
Holt Edges aus der Datenbank basierend auf Seed-IDs. Holt Edges aus der Datenbank basierend auf Seed-IDs.
Filtert auf source_id, target_id oder note_id. WP-15c: Erhält alle Metadaten für das Note-Level Diversity Pooling.
""" """
if not seeds or limit <= 0: if not seeds or limit <= 0:
return [] return []
# Konsistente Namensauflösung via database-Paket # Konsistente Namensauflösung via database-Paket
# Rückgabe: (notes_col, chunks_col, edges_col)
_, _, edges_col = collection_names(prefix) _, _, edges_col = collection_names(prefix)
# Wir suchen Kanten, bei denen die Seed-IDs entweder Quelle, Ziel oder Kontext-Note sind.
seed_conditions = [] seed_conditions = []
for field in ("source_id", "target_id", "note_id"): for field in ("source_id", "target_id", "note_id"):
for s in seeds: for s in seeds:
@ -35,6 +39,7 @@ def fetch_edges_from_qdrant(
) )
seeds_filter = rest.Filter(should=seed_conditions) if seed_conditions else None seeds_filter = rest.Filter(should=seed_conditions) if seed_conditions else None
# Optionaler Filter auf spezifische Kanten-Typen (z.B. für Intent-Routing)
type_filter = None type_filter = None
if edge_types: if edge_types:
type_conds = [ type_conds = [
@ -52,6 +57,7 @@ def fetch_edges_from_qdrant(
flt = rest.Filter(must=must) if must else None flt = rest.Filter(must=must) if must else None
# Abfrage via Qdrant Scroll API # Abfrage via Qdrant Scroll API
# WICHTIG: with_payload=True lädt alle Metadaten (target_section, provenance etc.)
pts, _ = client.scroll( pts, _ = client.scroll(
collection_name=edges_col, collection_name=edges_col,
scroll_filter=flt, scroll_filter=flt,
@ -60,4 +66,6 @@ def fetch_edges_from_qdrant(
with_vectors=False, with_vectors=False,
) )
# Wir geben das vollständige Payload zurück, damit der Retriever
# alle Signale für die Super-Edge-Aggregation und das Scoring hat.
return [dict(p.payload) for p in pts if p.payload] return [dict(p.payload) for p in pts if p.payload]

View File

@ -1,10 +1,10 @@
""" """
FILE: app/core/graph/graph_derive_edges.py FILE: app/core/graph/graph_derive_edges.py
DESCRIPTION: Hauptlogik zur Kanten-Aggregation und De-Duplizierung. DESCRIPTION: Hauptlogik zur Kanten-Aggregation und De-Duplizierung.
AUDIT: WP-15b/c Audit:
- Nutzt parse_link_target - Präzises Sektions-Splitting via parse_link_target.
- Übergibt Section als 'variant' an ID-Gen - Eindeutige ID-Generierung pro Sektions-Variante (Multigraph).
- Dedup basiert jetzt auf Edge-ID (erlaubt Multigraph für Sections) - Ermöglicht dem Retriever die Super-Edge-Aggregation.
""" """
from typing import List, Optional, Dict, Tuple from typing import List, Optional, Dict, Tuple
from .graph_utils import ( from .graph_utils import (
@ -21,31 +21,45 @@ def build_edges_for_note(
note_level_references: Optional[List[str]] = None, note_level_references: Optional[List[str]] = None,
include_note_scope_refs: bool = False, include_note_scope_refs: bool = False,
) -> List[dict]: ) -> List[dict]:
"""Erzeugt und aggregiert alle Kanten für eine Note (WP-15b).""" """
Erzeugt und aggregiert alle Kanten für eine Note.
Sorgt für die physische Trennung von Sektions-Links via Edge-ID.
"""
edges: List[dict] = [] edges: List[dict] = []
# note_type für die Ermittlung der edge_defaults (types.yaml)
note_type = _get(chunks[0], "type") if chunks else "concept" note_type = _get(chunks[0], "type") if chunks else "concept"
# 1) Struktur-Kanten (belongs_to, next/prev) # 1) Struktur-Kanten (Internal: belongs_to, next/prev)
# Diese erhalten die Provenienz 'structure' und sind in der Registry geschützt.
for idx, ch in enumerate(chunks): for idx, ch in enumerate(chunks):
cid = _get(ch, "chunk_id", "id") cid = _get(ch, "chunk_id", "id")
if not cid: continue if not cid: continue
# Verbindung Chunk -> Note
edges.append(_edge("belongs_to", "chunk", cid, note_id, note_id, { edges.append(_edge("belongs_to", "chunk", cid, note_id, note_id, {
"chunk_id": cid, "edge_id": _mk_edge_id("belongs_to", cid, note_id, "chunk", "structure:belongs_to"), "chunk_id": cid,
"provenance": "structure", "rule_id": "structure:belongs_to", "confidence": PROVENANCE_PRIORITY["structure:belongs_to"] "edge_id": _mk_edge_id("belongs_to", cid, note_id, "chunk", "structure:belongs_to"),
"provenance": "structure",
"rule_id": "structure:belongs_to",
"confidence": PROVENANCE_PRIORITY["structure:belongs_to"]
})) }))
# Horizontale Verkettung (Ordnung)
if idx < len(chunks) - 1: if idx < len(chunks) - 1:
next_id = _get(chunks[idx+1], "chunk_id", "id") next_id = _get(chunks[idx+1], "chunk_id", "id")
if next_id: if next_id:
edges.append(_edge("next", "chunk", cid, next_id, note_id, { edges.append(_edge("next", "chunk", cid, next_id, note_id, {
"chunk_id": cid, "edge_id": _mk_edge_id("next", cid, next_id, "chunk", "structure:order"), "chunk_id": cid,
"edge_id": _mk_edge_id("next", cid, next_id, "chunk", "structure:order"),
"provenance": "structure", "rule_id": "structure:order", "confidence": PROVENANCE_PRIORITY["structure:order"] "provenance": "structure", "rule_id": "structure:order", "confidence": PROVENANCE_PRIORITY["structure:order"]
})) }))
edges.append(_edge("prev", "chunk", next_id, cid, note_id, { edges.append(_edge("prev", "chunk", next_id, cid, note_id, {
"chunk_id": next_id, "edge_id": _mk_edge_id("prev", next_id, cid, "chunk", "structure:order"), "chunk_id": next_id,
"edge_id": _mk_edge_id("prev", next_id, cid, "chunk", "structure:order"),
"provenance": "structure", "rule_id": "structure:order", "confidence": PROVENANCE_PRIORITY["structure:order"] "provenance": "structure", "rule_id": "structure:order", "confidence": PROVENANCE_PRIORITY["structure:order"]
})) }))
# 2) Inhaltliche Kanten # 2) Inhaltliche Kanten (Explicit & Candidate Pool)
reg = load_types_registry() reg = load_types_registry()
defaults = get_edge_defaults_for(note_type, reg) defaults = get_edge_defaults_for(note_type, reg)
refs_all: List[str] = [] refs_all: List[str] = []
@ -55,7 +69,7 @@ def build_edges_for_note(
if not cid: continue if not cid: continue
raw = _get(ch, "window") or _get(ch, "text") or "" raw = _get(ch, "window") or _get(ch, "text") or ""
# Typed & Candidate Pool (WP-15b Integration) # A. Typed Relations (Inline [[rel:kind|target]])
typed, rem = extract_typed_relations(raw) typed, rem = extract_typed_relations(raw)
for k, raw_t in typed: for k, raw_t in typed:
t, sec = parse_link_target(raw_t, note_id) t, sec = parse_link_target(raw_t, note_id)
@ -63,14 +77,14 @@ def build_edges_for_note(
payload = { payload = {
"chunk_id": cid, "chunk_id": cid,
# Variant=sec sorgt für eindeutige ID pro Abschnitt # WP-Fix: Variant=sec sorgt für eindeutige ID pro Sektion
"edge_id": _mk_edge_id(k, cid, t, "chunk", "inline:rel", variant=sec), "edge_id": _mk_edge_id(k, cid, t, "chunk", "inline:rel", variant=sec),
"provenance": "explicit", "rule_id": "inline:rel", "confidence": PROVENANCE_PRIORITY["inline:rel"] "provenance": "explicit", "rule_id": "inline:rel", "confidence": PROVENANCE_PRIORITY["inline:rel"]
} }
if sec: payload["target_section"] = sec if sec: payload["target_section"] = sec
edges.append(_edge(k, "chunk", cid, t, note_id, payload)) edges.append(_edge(k, "chunk", cid, t, note_id, payload))
# B. Candidate Pool (WP-15b Validierte KI-Kanten)
pool = ch.get("candidate_pool") or ch.get("candidate_edges") or [] pool = ch.get("candidate_pool") or ch.get("candidate_edges") or []
for cand in pool: for cand in pool:
raw_t, k, p = cand.get("to"), cand.get("kind", "related_to"), cand.get("provenance", "semantic_ai") raw_t, k, p = cand.get("to"), cand.get("kind", "related_to"), cand.get("provenance", "semantic_ai")
@ -82,10 +96,9 @@ def build_edges_for_note(
"provenance": p, "rule_id": f"candidate:{p}", "confidence": PROVENANCE_PRIORITY.get(p, 0.90) "provenance": p, "rule_id": f"candidate:{p}", "confidence": PROVENANCE_PRIORITY.get(p, 0.90)
} }
if sec: payload["target_section"] = sec if sec: payload["target_section"] = sec
edges.append(_edge(k, "chunk", cid, t, note_id, payload)) edges.append(_edge(k, "chunk", cid, t, note_id, payload))
# Callouts & Wikilinks # C. Callouts (> [!edge])
call_pairs, rem2 = extract_callout_relations(rem) call_pairs, rem2 = extract_callout_relations(rem)
for k, raw_t in call_pairs: for k, raw_t in call_pairs:
t, sec = parse_link_target(raw_t, note_id) t, sec = parse_link_target(raw_t, note_id)
@ -97,9 +110,9 @@ def build_edges_for_note(
"provenance": "explicit", "rule_id": "callout:edge", "confidence": PROVENANCE_PRIORITY["callout:edge"] "provenance": "explicit", "rule_id": "callout:edge", "confidence": PROVENANCE_PRIORITY["callout:edge"]
} }
if sec: payload["target_section"] = sec if sec: payload["target_section"] = sec
edges.append(_edge(k, "chunk", cid, t, note_id, payload)) edges.append(_edge(k, "chunk", cid, t, note_id, payload))
# D. Standard Wikilinks & Typ-Defaults
refs = extract_wikilinks(rem2) refs = extract_wikilinks(rem2)
for raw_r in refs: for raw_r in refs:
r, sec = parse_link_target(raw_r, note_id) r, sec = parse_link_target(raw_r, note_id)
@ -111,9 +124,9 @@ def build_edges_for_note(
"provenance": "explicit", "rule_id": "explicit:wikilink", "confidence": PROVENANCE_PRIORITY["explicit:wikilink"] "provenance": "explicit", "rule_id": "explicit:wikilink", "confidence": PROVENANCE_PRIORITY["explicit:wikilink"]
} }
if sec: payload["target_section"] = sec if sec: payload["target_section"] = sec
edges.append(_edge("references", "chunk", cid, r, note_id, payload)) edges.append(_edge("references", "chunk", cid, r, note_id, payload))
# Automatische Kanten-Vererbung aus types.yaml
for rel in defaults: for rel in defaults:
if rel != "references": if rel != "references":
def_payload = { def_payload = {
@ -124,13 +137,10 @@ def build_edges_for_note(
if sec: def_payload["target_section"] = sec if sec: def_payload["target_section"] = sec
edges.append(_edge(rel, "chunk", cid, r, note_id, def_payload)) edges.append(_edge(rel, "chunk", cid, r, note_id, def_payload))
# Für Note-Scope Sammlung nutzen wir den Original-String zur Dedup, aber gesäubert
refs_all.extend([parse_link_target(r, note_id)[0] for r in refs]) refs_all.extend([parse_link_target(r, note_id)[0] for r in refs])
# 3) Note-Scope & De-Duplizierung # 3) Note-Scope (Grobe Struktur-Verbindungen)
if include_note_scope_refs: if include_note_scope_refs:
# refs_all ist jetzt schon gesäubert (nur Targets)
# note_level_references müssen auch gesäubert werden
cleaned_note_refs = [parse_link_target(r, note_id)[0] for r in (note_level_references or [])] cleaned_note_refs = [parse_link_target(r, note_id)[0] for r in (note_level_references or [])]
refs_note = _dedupe_seq((refs_all or []) + cleaned_note_refs) refs_note = _dedupe_seq((refs_all or []) + cleaned_note_refs)
@ -140,17 +150,19 @@ def build_edges_for_note(
"edge_id": _mk_edge_id("references", note_id, r, "note", "explicit:note_scope"), "edge_id": _mk_edge_id("references", note_id, r, "note", "explicit:note_scope"),
"provenance": "explicit", "confidence": PROVENANCE_PRIORITY["explicit:note_scope"] "provenance": "explicit", "confidence": PROVENANCE_PRIORITY["explicit:note_scope"]
})) }))
# Backlinks zur Stärkung der Bidirektionalität
edges.append(_edge("backlink", "note", r, note_id, note_id, { edges.append(_edge("backlink", "note", r, note_id, note_id, {
"edge_id": _mk_edge_id("backlink", r, note_id, "note", "derived:backlink"), "edge_id": _mk_edge_id("backlink", r, note_id, "note", "derived:backlink"),
"provenance": "rule", "confidence": PROVENANCE_PRIORITY["derived:backlink"] "provenance": "rule", "confidence": PROVENANCE_PRIORITY["derived:backlink"]
})) }))
# Deduplizierung: Wir nutzen jetzt die EDGE-ID als Schlüssel. # 4) De-Duplizierung (In-Place)
# Da die Edge-ID nun 'variant' (Section) enthält, bleiben unterschiedliche Sections erhalten. # Da die EDGE-ID nun die Sektion (variant) enthält, bleiben Links auf
# unterschiedliche Abschnitte derselben Note erhalten.
unique_map: Dict[str, dict] = {} unique_map: Dict[str, dict] = {}
for e in edges: for e in edges:
eid = e["edge_id"] eid = e["edge_id"]
# Bei Konflikt (gleiche ID = exakt gleiche Kante und Section) gewinnt die höhere Confidence # Höhere Confidence gewinnt bei identischer ID
if eid not in unique_map or e.get("confidence", 0) > unique_map[eid].get("confidence", 0): if eid not in unique_map or e.get("confidence", 0) > unique_map[eid].get("confidence", 0):
unique_map[eid] = e unique_map[eid] = e

View File

@ -2,8 +2,9 @@
FILE: app/core/graph/graph_subgraph.py FILE: app/core/graph/graph_subgraph.py
DESCRIPTION: In-Memory Repräsentation eines Graphen für Scoring und Analyse. DESCRIPTION: In-Memory Repräsentation eines Graphen für Scoring und Analyse.
Zentrale Komponente für die Graph-Expansion (BFS) und Bonus-Berechnung. Zentrale Komponente für die Graph-Expansion (BFS) und Bonus-Berechnung.
MODULARISIERUNG: Teil des graph-Pakets (WP-14). WP-15c Update: Erhalt von Metadaten (target_section, provenance)
VERSION: 1.1.0 für präzises Retrieval-Reasoning.
VERSION: 1.2.0
STATUS: Active STATUS: Active
""" """
import math import math
@ -22,6 +23,7 @@ class Subgraph:
""" """
def __init__(self) -> None: def __init__(self) -> None:
# adj speichert nun vollständige Payloads statt nur Tripel
self.adj: DefaultDict[str, List[Dict]] = defaultdict(list) self.adj: DefaultDict[str, List[Dict]] = defaultdict(list)
self.reverse_adj: DefaultDict[str, List[Dict]] = defaultdict(list) self.reverse_adj: DefaultDict[str, List[Dict]] = defaultdict(list)
self.in_degree: DefaultDict[str, int] = defaultdict(int) self.in_degree: DefaultDict[str, int] = defaultdict(int)
@ -30,31 +32,49 @@ class Subgraph:
def add_edge(self, e: Dict) -> None: def add_edge(self, e: Dict) -> None:
""" """
Fügt eine Kante hinzu und aktualisiert Indizes. Fügt eine Kante hinzu und aktualisiert Indizes.
Unterstützt Kontext-Notes für verbesserte Graph-Konnektivität. WP-15c: Speichert das vollständige Payload für den Explanation Layer.
""" """
src = e.get("source") src = e.get("source")
tgt = e.get("target") tgt = e.get("target")
kind = e.get("kind") kind = e.get("kind")
weight = e.get("weight", EDGE_BASE_WEIGHTS.get(kind, 0.0))
# Das gesamte Payload wird als Kanten-Objekt behalten
# Wir stellen sicher, dass alle relevanten Metadaten vorhanden sind
edge_data = {
"source": src,
"target": tgt,
"kind": kind,
"weight": e.get("weight", EDGE_BASE_WEIGHTS.get(kind, 0.0)),
"provenance": e.get("provenance", "rule"),
"confidence": e.get("confidence", 1.0),
"target_section": e.get("target_section"), # Essentiell für Präzision
"is_super_edge": e.get("is_super_edge", False)
}
owner = e.get("note_id") owner = e.get("note_id")
if not src or not tgt: if not src or not tgt:
return return
# 1. Forward-Kante # 1. Forward-Kante
self.adj[src].append({"target": tgt, "kind": kind, "weight": weight}) self.adj[src].append(edge_data)
self.out_degree[src] += 1 self.out_degree[src] += 1
self.in_degree[tgt] += 1 self.in_degree[tgt] += 1
# 2. Reverse-Kante (für WP-04b Explanation Layer) # 2. Reverse-Kante (für Explanation Layer & Backlinks)
self.reverse_adj[tgt].append({"source": src, "kind": kind, "weight": weight}) self.reverse_adj[tgt].append(edge_data)
# 3. Kontext-Note Handling (erhöht die Zentralität der Parent-Note) # 3. Kontext-Note Handling (erhöht die Zentralität der Parent-Note)
if owner and owner != src: if owner and owner != src:
self.adj[owner].append({"target": tgt, "kind": kind, "weight": weight}) # Wir erstellen eine virtuelle Kontext-Kante
ctx_edge = edge_data.copy()
ctx_edge["source"] = owner
ctx_edge["via_context"] = True
self.adj[owner].append(ctx_edge)
self.out_degree[owner] += 1 self.out_degree[owner] += 1
if owner != tgt: if owner != tgt:
self.reverse_adj[tgt].append({"source": owner, "kind": kind, "weight": weight, "via_context": True}) self.reverse_adj[tgt].append(ctx_edge)
self.in_degree[owner] += 1 self.in_degree[owner] += 1
def aggregate_edge_bonus(self, node_id: str) -> float: def aggregate_edge_bonus(self, node_id: str) -> float:
@ -73,14 +93,15 @@ class Subgraph:
indeg = self.in_degree.get(node_id, 0) indeg = self.in_degree.get(node_id, 0)
if indeg <= 0: if indeg <= 0:
return 0.0 return 0.0
# math.log1p(x) entspricht log(1+x)
return min(math.log1p(indeg) / 10.0, 0.15) return min(math.log1p(indeg) / 10.0, 0.15)
def get_outgoing_edges(self, node_id: str) -> List[Dict[str, Any]]: def get_outgoing_edges(self, node_id: str) -> List[Dict[str, Any]]:
"""Gibt alle ausgehenden Kanten einer Node zurück.""" """Gibt alle ausgehenden Kanten einer Node inkl. Metadaten zurück."""
return self.adj.get(node_id, []) return self.adj.get(node_id, [])
def get_incoming_edges(self, node_id: str) -> List[Dict[str, Any]]: def get_incoming_edges(self, node_id: str) -> List[Dict[str, Any]]:
"""Gibt alle eingehenden Kanten einer Node zurück.""" """Gibt alle eingehenden Kanten einer Node inkl. Metadaten zurück."""
return self.reverse_adj.get(node_id, []) return self.reverse_adj.get(node_id, [])
@ -111,13 +132,19 @@ def expand(
src, tgt = pl.get("source_id"), pl.get("target_id") src, tgt = pl.get("source_id"), pl.get("target_id")
if not src or not tgt: continue if not src or not tgt: continue
sg.add_edge({ # WP-15c: Wir übergeben das vollständige Payload an add_edge
edge_payload = {
"source": src, "source": src,
"target": tgt, "target": tgt,
"kind": pl.get("kind", "edge"), "kind": pl.get("kind", "edge"),
"weight": calculate_edge_weight(pl), "weight": calculate_edge_weight(pl),
"note_id": pl.get("note_id"), "note_id": pl.get("note_id"),
}) "provenance": pl.get("provenance", "rule"),
"confidence": pl.get("confidence", 1.0),
"target_section": pl.get("target_section")
}
sg.add_edge(edge_payload)
# BFS Logik: Neue Ziele in die nächste Frontier aufnehmen # BFS Logik: Neue Ziele in die nächste Frontier aufnehmen
if tgt not in visited: if tgt not in visited:

View File

@ -1,9 +1,8 @@
""" """
FILE: app/core/retrieval/retriever.py FILE: app/core/retrieval/retriever.py
DESCRIPTION: Haupt-Schnittstelle für die Suche. Orchestriert Vektorsuche und Graph-Expansion. DESCRIPTION: Haupt-Schnittstelle für die Suche. Orchestriert Vektorsuche und Graph-Expansion.
Nutzt retriever_scoring.py für die WP-22 Logik. WP-15c Update: Note-Level Diversity Pooling & Super-Edge Aggregation.
MODULARISIERUNG: Verschoben in das retrieval-Paket für WP-14. VERSION: 0.7.0
VERSION: 0.6.16
STATUS: Active STATUS: Active
DEPENDENCIES: app.config, app.models.dto, app.core.database*, app.core.graph_adapter DEPENDENCIES: app.config, app.models.dto, app.core.database*, app.core.graph_adapter
""" """
@ -13,6 +12,7 @@ import os
import time import time
import logging import logging
from typing import Any, Dict, List, Tuple, Iterable, Optional from typing import Any, Dict, List, Tuple, Iterable, Optional
from collections import defaultdict
from app.config import get_settings from app.config import get_settings
from app.models.dto import ( from app.models.dto import (
@ -89,7 +89,6 @@ def _build_explanation(
) -> Explanation: ) -> Explanation:
""" """
Transformiert mathematische Scores und Graph-Signale in eine menschenlesbare Erklärung. Transformiert mathematische Scores und Graph-Signale in eine menschenlesbare Erklärung.
Behebt Pydantic ValidationErrors durch explizite String-Sicherung.
""" """
_, edge_w_cfg, _ = get_weights() _, edge_w_cfg, _ = get_weights()
base_val = scoring_debug["base_val"] base_val = scoring_debug["base_val"]
@ -116,12 +115,22 @@ def _build_explanation(
elif semantic_score > 0.70: elif semantic_score > 0.70:
reasons.append(Reason(kind="semantic", message="Inhaltliche Übereinstimmung.", score_impact=base_val)) reasons.append(Reason(kind="semantic", message="Inhaltliche Übereinstimmung.", score_impact=base_val))
# 3. Gründe für Typ und Lifecycle # 3. Gründe für Typ und Lifecycle (WP-25 Vorbereitung)
type_weight = float(payload.get("retriever_weight", 1.0)) type_weight = float(payload.get("retriever_weight", 1.0))
if type_weight != 1.0: if type_weight != 1.0:
msg = "Bevorzugt" if type_weight > 1.0 else "De-priorisiert" 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))) reasons.append(Reason(kind="type", message=f"{msg} durch Typ-Profil.", score_impact=base_val * (type_weight - 1.0)))
# NEU: Explizite Ausweisung des Lifecycle-Status (WP-22)
status_mult = scoring_debug.get("status_multiplier", 1.0)
if status_mult != 1.0:
status_msg = "Belohnt (Stable)" if status_mult > 1.0 else "De-priorisiert (Draft)"
reasons.append(Reason(
kind="status",
message=f"{status_msg} durch Content-Lifecycle.",
score_impact=semantic_score * (status_mult - 1.0)
))
# 4. Kanten-Verarbeitung (Graph-Intelligence) # 4. Kanten-Verarbeitung (Graph-Intelligence)
if subgraph and target_note_id and scoring_debug["edge_bonus"] > 0: if subgraph and target_note_id and scoring_debug["edge_bonus"] > 0:
raw_edges = [] raw_edges = []
@ -131,7 +140,6 @@ def _build_explanation(
raw_edges.extend(subgraph.get_outgoing_edges(target_note_id) or []) raw_edges.extend(subgraph.get_outgoing_edges(target_note_id) or [])
for edge in raw_edges: for edge in raw_edges:
# FIX: Zwingende String-Konvertierung für Pydantic-Stabilität
src = str(edge.get("source") or "note_root") src = str(edge.get("source") or "note_root")
tgt = str(edge.get("target") or target_note_id or "unknown_target") tgt = str(edge.get("target") or target_note_id or "unknown_target")
kind = str(edge.get("kind", "related_to")) kind = str(edge.get("kind", "related_to"))
@ -187,10 +195,14 @@ def _build_hits_from_semantic(
explain: bool = False, explain: bool = False,
dynamic_edge_boosts: Dict[str, float] = None dynamic_edge_boosts: Dict[str, float] = None
) -> QueryResponse: ) -> QueryResponse:
"""Wandelt semantische Roh-Treffer in bewertete QueryHits um.""" """
Wandelt semantische Roh-Treffer in bewertete QueryHits um.
WP-15c: Implementiert Note-Level Diversity Pooling.
"""
t0 = time.time() t0 = time.time()
enriched = [] enriched = []
# Erstes Scoring für alle Kandidaten
for pid, semantic_score, payload in hits: for pid, semantic_score, payload in hits:
edge_bonus, cent_bonus = 0.0, 0.0 edge_bonus, cent_bonus = 0.0, 0.0
target_id = payload.get("note_id") target_id = payload.get("note_id")
@ -202,15 +214,30 @@ def _build_hits_from_semantic(
except Exception: except Exception:
pass pass
# Mathematisches Scoring via WP-22 Engine
debug_data = compute_wp22_score( debug_data = compute_wp22_score(
semantic_score, payload, edge_bonus, cent_bonus, dynamic_edge_boosts semantic_score, payload, edge_bonus, cent_bonus, dynamic_edge_boosts
) )
enriched.append((pid, semantic_score, payload, debug_data)) enriched.append((pid, semantic_score, payload, debug_data))
# Sortierung nach finalem mathematischen Score # 1. Sortierung nach finalem mathematischen Score
enriched_sorted = sorted(enriched, key=lambda h: h[3]["total"], reverse=True) enriched_sorted = sorted(enriched, key=lambda h: h[3]["total"], reverse=True)
limited_hits = enriched_sorted[: max(1, top_k)]
# 2. WP-15c: Note-Level Diversity Pooling
# Wir behalten pro note_id nur den Hit mit dem höchsten total_score.
# Dies verhindert, dass 10 Chunks derselben Note andere KeyNotes verdrängen.
unique_note_hits = []
seen_notes = set()
for item in enriched_sorted:
_, _, payload, _ = item
note_id = str(payload.get("note_id", "unknown"))
if note_id not in seen_notes:
unique_note_hits.append(item)
seen_notes.add(note_id)
# 3. Begrenzung auf top_k nach dem Diversity-Pooling
limited_hits = unique_note_hits[: max(1, top_k)]
results: List[QueryHit] = [] results: List[QueryHit] = []
for pid, s_score, pl, dbg in limited_hits: for pid, s_score, pl, dbg in limited_hits:
@ -225,7 +252,6 @@ def _build_hits_from_semantic(
applied_boosts=dynamic_edge_boosts applied_boosts=dynamic_edge_boosts
) )
# Payload Text-Feld normalisieren
text_content = pl.get("page_content") or pl.get("text") or pl.get("content", "[Kein Text]") text_content = pl.get("page_content") or pl.get("text") or pl.get("content", "[Kein Text]")
results.append(QueryHit( results.append(QueryHit(
@ -250,14 +276,14 @@ def _build_hits_from_semantic(
def hybrid_retrieve(req: QueryRequest) -> QueryResponse: def hybrid_retrieve(req: QueryRequest) -> QueryResponse:
""" """
Die Haupt-Einstiegsfunktion für die hybride Suche. Die Haupt-Einstiegsfunktion für die hybride Suche.
Kombiniert Vektorsuche mit Graph-Expansion und WP-22 Gewichtung. WP-15c: Implementiert Edge-Aggregation (Super-Kanten).
""" """
client, prefix = _get_client_and_prefix() client, prefix = _get_client_and_prefix()
vector = list(req.query_vector) if req.query_vector else _get_query_vector(req) vector = list(req.query_vector) if req.query_vector else _get_query_vector(req)
top_k = req.top_k or 10 top_k = req.top_k or 10
# 1. Semantische Seed-Suche # 1. Semantische Seed-Suche (Wir laden etwas mehr für das Pooling)
hits = _semantic_hits(client, prefix, vector, top_k=top_k, filters=req.filters) hits = _semantic_hits(client, prefix, vector, top_k=top_k * 3, filters=req.filters)
# 2. Graph Expansion Konfiguration # 2. Graph Expansion Konfiguration
expand_cfg = req.expand if isinstance(req.expand, dict) else {} expand_cfg = req.expand if isinstance(req.expand, dict) else {}
@ -266,39 +292,76 @@ def hybrid_retrieve(req: QueryRequest) -> QueryResponse:
subgraph: ga.Subgraph | None = None subgraph: ga.Subgraph | None = None
if depth > 0 and hits: 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")}) seed_ids = list({h[2].get("note_id") for h in hits if h[2].get("note_id")})
if seed_ids: if seed_ids:
try: try:
# Subgraph aus RAM/DB laden
subgraph = ga.expand(client, prefix, seed_ids, depth=depth, edge_types=expand_cfg.get("edge_types")) 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 --- # --- WP-15c: Edge-Aggregation & Deduplizierung (Super-Kanten) ---
if subgraph and hasattr(subgraph, "graph"): # Verhindert Score-Explosion durch multiple Links auf versch. Abschnitte.
for _, _, data in subgraph.graph.edges(data=True): # Logik: 1. Kante zählt voll, weitere dämpfen auf Faktor 0.1.
# A. Provenance Weighting (WP-22 Bonus für Herkunft) if subgraph and hasattr(subgraph, "adj"):
prov = data.get("provenance", "rule") for src, edge_list in subgraph.adj.items():
# Belohnung: Explizite Links (1.0) > Smart (0.9) > Rule (0.7) # Gruppiere Kanten nach Ziel-Note (Deduplizierung ID_A -> ID_B)
prov_w = 1.0 if prov == "explicit" else (0.9 if prov == "smart" else 0.7) by_target = defaultdict(list)
for e in edge_list:
by_target[e["target"]].append(e)
# B. Intent Boost Multiplikator (Vom Router dynamisch injiziert) aggregated_list = []
kind = data.get("kind") for tgt, edges in by_target.items():
intent_multiplier = boost_edges.get(kind, 1.0) if len(edges) > 1:
# Sortiere: Stärkste Kante zuerst
sorted_edges = sorted(edges, key=lambda x: x.get("weight", 0.0), reverse=True)
primary = sorted_edges[0]
# Aggregiertes Gewicht berechnen (Sättigungs-Logik)
total_w = primary.get("weight", 0.0)
for secondary in sorted_edges[1:]:
total_w += secondary.get("weight", 0.0) * 0.1
primary["weight"] = total_w
primary["is_super_edge"] = True # Flag für Explanation Layer
primary["edge_count"] = len(edges)
aggregated_list.append(primary)
else:
aggregated_list.append(edges[0])
# Finales Gewicht setzen (Basis * Provenance * Intent) # In-Place Update der Adjazenzliste des Graphen
data["weight"] = data.get("weight", 1.0) * prov_w * intent_multiplier subgraph.adj[src] = aggregated_list
# Re-Sync der In-Degrees für Centrality-Bonus (Aggregation konsistent halten)
subgraph.in_degree = defaultdict(int)
for src, edges in subgraph.adj.items():
for e in edges:
subgraph.in_degree[e["target"]] += 1
# --- WP-22: Kanten-Gewichtung (Provenance & Intent Boost) ---
if subgraph and hasattr(subgraph, "adj"):
for src, edges in subgraph.adj.items():
for e in edges:
# A. Provenance Weighting
prov = e.get("provenance", "rule")
prov_w = 1.0 if prov == "explicit" else (0.9 if prov == "smart" else 0.7)
# B. Intent Boost Multiplikator
kind = e.get("kind")
intent_multiplier = boost_edges.get(kind, 1.0)
# Gewichtung anpassen
e["weight"] = e.get("weight", 1.0) * prov_w * intent_multiplier
except Exception as e: except Exception as e:
logger.error(f"Graph Expansion failed: {e}") logger.error(f"Graph Expansion failed: {e}")
subgraph = None subgraph = None
# 3. Scoring & Explanation Generierung # 3. Scoring & Explanation Generierung
# top_k wird erst hier final angewandt
return _build_hits_from_semantic(hits, top_k, "hybrid", subgraph, req.explain, boost_edges) return _build_hits_from_semantic(hits, top_k, "hybrid", subgraph, req.explain, boost_edges)
def semantic_retrieve(req: QueryRequest) -> QueryResponse: def semantic_retrieve(req: QueryRequest) -> QueryResponse:
"""Standard Vektorsuche ohne Graph-Einfluss (WP-02 Fallback).""" """Standard Vektorsuche ohne Graph-Einfluss."""
client, prefix = _get_client_and_prefix() client, prefix = _get_client_and_prefix()
vector = _get_query_vector(req) vector = _get_query_vector(req)
hits = _semantic_hits(client, prefix, vector, req.top_k or 10, req.filters) hits = _semantic_hits(client, prefix, vector, req.top_k or 10, req.filters)
@ -308,5 +371,4 @@ def semantic_retrieve(req: QueryRequest) -> QueryResponse:
class Retriever: class Retriever:
"""Schnittstelle für die asynchrone Suche.""" """Schnittstelle für die asynchrone Suche."""
async def search(self, request: QueryRequest) -> QueryResponse: async def search(self, request: QueryRequest) -> QueryResponse:
"""Führt eine hybride Suche aus."""
return hybrid_retrieve(request) return hybrid_retrieve(request)

View File

@ -1,11 +1,10 @@
""" """
FILE: app/core/retrieval/retriever_scoring.py FILE: app/core/retrieval/retriever_scoring.py
DESCRIPTION: Mathematische Kern-Logik für das WP-22 Scoring. DESCRIPTION: Mathematische Kern-Logik für das WP-22/WP-15c Scoring.
Berechnet Relevanz-Scores basierend auf Semantik, Graph-Intelligence und Content Lifecycle. Berechnet Relevanz-Scores basierend auf Semantik, Graph-Intelligence und Content Lifecycle.
MODULARISIERUNG: Verschoben in das retrieval-Paket für WP-14. FIX v1.0.3: Optimierte Interaktion zwischen Typ-Boost und Status-Dämpfung.
VERSION: 1.0.2 VERSION: 1.0.3
STATUS: Active STATUS: Active
DEPENDENCIES: app.config, typing
""" """
import os import os
import logging import logging
@ -23,10 +22,6 @@ logger = logging.getLogger(__name__)
def get_weights() -> Tuple[float, float, float]: def get_weights() -> Tuple[float, float, float]:
""" """
Liefert die Basis-Gewichtung (semantic, edge, centrality) aus der Konfiguration. 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 from app.config import get_settings
settings = get_settings() settings = get_settings()
@ -58,7 +53,7 @@ def get_status_multiplier(payload: Dict[str, Any]) -> float:
- stable: 1.2 (Belohnung für verifiziertes Wissen) - stable: 1.2 (Belohnung für verifiziertes Wissen)
- active: 1.0 (Standard-Gewichtung) - active: 1.0 (Standard-Gewichtung)
- draft: 0.5 (Bestrafung für unfertige Fragmente) - draft: 0.5 (Dämpfung für unfertige Fragmente)
""" """
status = str(payload.get("status", "active")).lower().strip() status = str(payload.get("status", "active")).lower().strip()
if status == "stable": if status == "stable":
@ -75,35 +70,42 @@ def compute_wp22_score(
dynamic_edge_boosts: Optional[Dict[str, float]] = None dynamic_edge_boosts: Optional[Dict[str, float]] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Die zentrale mathematische Scoring-Formel der Mindnet Intelligence. Die zentrale mathematische Scoring-Formel (WP-15c optimiert).
Implementiert das WP-22 Hybrid-Scoring (Semantic * Lifecycle * Graph). Implementiert das Hybrid-Scoring (Semantic * Lifecycle * Graph).
FORMEL: LOGIK:
Score = (Similarity * StatusMult) * (1 + (TypeWeight - 1) + ((EdgeW * EB + CentW * CB) * IntentBoost)) 1. Base = Similarity * StatusMult (Lifecycle-Filter).
2. Boosts = (TypeBoost - 1) + (GraphBoni * IntentFactor).
3. Final = Base * (1 + Boosts).
Returns: Der edge_bonus_raw enthält bereits die Super-Edge-Aggregation (WP-15c).
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() sem_w, edge_w_cfg, cent_w_cfg = get_weights()
status_mult = get_status_multiplier(payload) status_mult = get_status_multiplier(payload)
# Retriever Weight (Type Boost aus types.yaml, z.B. 1.1 für Decisions) # Retriever Weight (Typ-Boost aus types.yaml, z.B. 1.1 für Decisions)
node_weight = float(payload.get("retriever_weight", 1.0)) node_weight = float(payload.get("retriever_weight", 1.0))
# 1. Berechnung des Base Scores (Semantik gewichtet durch Lifecycle-Status) # 1. Berechnung des Base Scores (Semantik gewichtet durch Lifecycle-Status)
# WICHTIG: Der Status wirkt hier als Multiplikator auf die Basis-Relevanz.
base_val = float(semantic_score) * status_mult base_val = float(semantic_score) * status_mult
# 2. Graph Boost Factor (Teil C: Intent-spezifische Verstärkung) # 2. Graph Boost Factor (Intent-spezifische Verstärkung aus decision_engine.yaml)
# Erhöht das Gewicht des gesamten Graphen um 50%, wenn ein spezifischer Intent vorliegt. # 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 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 # 3. Einzelne Graph-Komponenten berechnen
# WP-15c Hinweis: edge_bonus_raw ist durch den retriever.py bereits gedämpft/aggregiert.
edge_impact_final = (edge_w_cfg * edge_bonus_raw) * graph_boost_factor edge_impact_final = (edge_w_cfg * edge_bonus_raw) * graph_boost_factor
cent_impact_final = (cent_w_cfg * cent_bonus_raw) * graph_boost_factor cent_impact_final = (cent_w_cfg * cent_bonus_raw) * graph_boost_factor
# 4. Finales Zusammenführen (Merging) # 4. Finales Zusammenführen (Merging)
# (node_weight - 1.0) sorgt dafür, dass ein Gewicht von 1.0 keinen Einfluss hat (neutral). # (node_weight - 1.0) wandelt das Gewicht in einen relativen Bonus um (z.B. 1.2 -> +0.2).
total = base_val * (1.0 + (node_weight - 1.0) + edge_impact_final + cent_impact_final) # Alle Boni werden addiert und wirken dann auf den base_val.
type_impact = node_weight - 1.0
total_boost = 1.0 + type_impact + edge_impact_final + cent_impact_final
total = base_val * total_boost
# Sicherstellen, dass der Score niemals 0 oder negativ ist (Floor) # Sicherstellen, dass der Score niemals 0 oder negativ ist (Floor)
final_score = max(0.0001, float(total)) final_score = max(0.0001, float(total))
@ -114,7 +116,7 @@ def compute_wp22_score(
"cent_bonus": float(cent_bonus_raw), "cent_bonus": float(cent_bonus_raw),
"status_multiplier": status_mult, "status_multiplier": status_mult,
"graph_boost_factor": graph_boost_factor, "graph_boost_factor": graph_boost_factor,
"type_impact": node_weight - 1.0, "type_impact": type_impact,
"base_val": base_val, "base_val": base_val,
"edge_impact_final": edge_impact_final, "edge_impact_final": edge_impact_final,
"cent_impact_final": cent_impact_final "cent_impact_final": cent_impact_final

View File

@ -4,7 +4,7 @@ audience: architect, product_owner
scope: graph, logic, provenance scope: graph, logic, provenance
status: active status: active
version: 2.9.1 version: 2.9.1
context: "Fachliche Beschreibung des Wissensgraphen: Knoten, Kanten, Provenance, Matrix-Logik und WP-22 Scoring-Prinzipien." context: "Fachliche Beschreibung des Wissensgraphen: Knoten, Kanten, Provenance, Matrix-Logik, WP-15c Multigraph-Support und WP-22 Scoring-Prinzipien."
--- ---
# Konzept: Die Graph-Logik # Konzept: Die Graph-Logik
@ -59,7 +59,7 @@ Um eine konsistente mathematische Gewichtung zu garantieren, werden alle Kanten
### 2.2 Provenance (Herkunft & Vertrauen) ### 2.2 Provenance (Herkunft & Vertrauen)
Nicht alle Kanten sind gleich viel wert. Mindnet unterscheidet drei Qualitätsstufen (**Provenance**), um bei der Berechnung des Edge-Bonus Prioritäten zu setzen. Nicht alle Kanten sind gleich viel wert. Mindnet unterscheidet mehrere Qualitätsstufen (**Provenance**), um bei der Berechnung des Edge-Bonus Prioritäten zu setzen.
**1. Explicit (Der Mensch hat es gesagt)** **1. Explicit (Der Mensch hat es gesagt)**
* *Quelle:* Inline-Links (`[[rel:...]]`) oder Wikilinks im Text. * *Quelle:* Inline-Links (`[[rel:...]]`) oder Wikilinks im Text.
@ -76,6 +76,19 @@ Nicht alle Kanten sind gleich viel wert. Mindnet unterscheidet drei Qualitätsst
* *Vertrauen:* **Niedrig (0.7)**. * *Vertrauen:* **Niedrig (0.7)**.
* *Bedeutung:* Systemseitige Heuristik. Diese Verbindungen dienen der Entdeckung neuer Pfade, haben aber weniger Gewicht als explizite Links. * *Bedeutung:* Systemseitige Heuristik. Diese Verbindungen dienen der Entdeckung neuer Pfade, haben aber weniger Gewicht als explizite Links.
**4. Structure (System-interne Verkettung)**
* *Quelle:* Automatische Struktur-Kanten (`belongs_to`, `next`, `prev`).
* *Vertrauen:* **Hoch (1.0)**.
* *Bedeutung:* Diese Kanten werden ausschließlich durch interne Prozesse erzeugt und sind durch die **Provenance Firewall** geschützt. Sie können weder durch Nutzer noch durch KI manipuliert werden.
### 2.3 Provenance Firewall (WP-15c)
Die **Edge Registry** (v0.8.0) implementiert eine strikte Trennung zwischen System-Kanten und Inhalts-Kanten:
* **Geschützte System-Kanten:** `next`, `prev`, `belongs_to` dürfen nur mit `provenance="structure"` gesetzt werden.
* **Blockierung:** Alle anderen Provenienzen (`explicit`, `semantic_ai`, `inherited`, `global_pool`, `rule`) werden bei System-Kanten blockiert und auf `related_to` zurückgesetzt.
* **Zweck:** Sichert die Graph-Integrität und verhindert Manipulationen an der internen Struktur.
--- ---
## 3. Matrix-Logik (Kontext-Sensitivität) ## 3. Matrix-Logik (Kontext-Sensitivität)
@ -122,12 +135,16 @@ Der Intent-Router injiziert spezifische Multiplikatoren für kanonische Typen:
Seit v2.9.1 unterstützt Mindnet **Deep-Links** zu spezifischen Abschnitten innerhalb einer Note. Seit v2.9.1 unterstützt Mindnet **Deep-Links** zu spezifischen Abschnitten innerhalb einer Note.
### 6.1 Link-Parsing ### 6.1 Link-Parsing & Self-Links (WP-15c)
Links wie `[[Note#Section]]` werden in zwei Komponenten aufgeteilt:
Die Logik erkennt nun präzise **Obsidian-Anker** (`[[Note#Section]]`) und **Self-Links** (`[[#Section]]`):
* **Obsidian-Anker:** `[[Note#Section]]` wird in `target_id="Note"` und `target_section="Section"` aufgeteilt.
* **Self-Links:** `[[#Section]]` wird zu `target_id="current_note_id"` und `target_section="Section"` aufgelöst.
* **`target_id`:** Enthält nur den Note-Namen (z.B. "Mein Leitbild") * **`target_id`:** Enthält nur den Note-Namen (z.B. "Mein Leitbild")
* **`target_section`:** Enthält den Abschnitts-Namen (z.B. "P3 Disziplin") * **`target_section`:** Enthält den Abschnitts-Namen (z.B. "P3 Disziplin")
**Vorteil:** Verhindert "Phantom-Knoten", die durch das Einbeziehen des Anchors in die `target_id` entstanden wären. **Vorteil:** Verhindert "Phantom-Knoten", die durch das Einbeziehen des Anchors in die `target_id` entstanden wären. Ermöglicht präzise Verlinkung innerhalb derselben Note.
### 6.2 Multigraph-Support ### 6.2 Multigraph-Support
Die Edge-ID enthält nun einen `variant`-Parameter (die Section), sodass mehrere Kanten zwischen denselben Knoten existieren können, wenn sie auf verschiedene Sections zeigen: Die Edge-ID enthält nun einen `variant`-Parameter (die Section), sodass mehrere Kanten zwischen denselben Knoten existieren können, wenn sie auf verschiedene Sections zeigen:

View File

@ -120,10 +120,12 @@ Gerichtete Kanten zwischen Knoten. Stark erweitert in v2.6 für Provenienz-Track
} }
``` ```
**Section-Support:** **Section-Support (WP-15c):**
* Links wie `[[Note#Section]]` werden in `target_id="Note"` und `target_section="Section"` aufgeteilt. * Links wie `[[Note#Section]]` werden in `target_id="Note"` und `target_section="Section"` aufgeteilt.
* Die Edge-ID enthält die Section als `variant`, sodass mehrere Kanten zwischen denselben Knoten existieren können, wenn sie auf verschiedene Sections zeigen. * **Self-Links:** `[[#Section]]` wird zu `target_id="current_note_id"` und `target_section="Section"` aufgelöst.
* Die Edge-ID enthält die Section als `variant`, sodass mehrere Kanten zwischen denselben Knoten existieren können, wenn sie auf verschiedene Sections zeigen (Multigraph-Modus).
* Semantische Deduplizierung basiert auf `src->tgt:kind@sec` Key, um "Phantom-Knoten" zu vermeiden. * Semantische Deduplizierung basiert auf `src->tgt:kind@sec` Key, um "Phantom-Knoten" zu vermeiden.
* **Metadaten-Persistenz:** `target_section`, `provenance` und `confidence` werden durchgängig im In-Memory Subgraph und Datenbank-Adapter erhalten.
**Erforderliche Indizes:** **Erforderliche Indizes:**
Es müssen Payload-Indizes für folgende Felder existieren: Es müssen Payload-Indizes für folgende Felder existieren:

View File

@ -3,22 +3,29 @@ doc_type: technical_reference
audience: developer, data_scientist audience: developer, data_scientist
scope: backend, retrieval, scoring, modularization scope: backend, retrieval, scoring, modularization
status: active status: active
version: 2.9.0 version: 2.9.1
context: "Detaillierte Dokumentation der Scoring-Algorithmen, inklusive WP-22 Lifecycle-Modifier, Intent-Boosting und WP-14 Modularisierung." context: "Detaillierte Dokumentation der Scoring-Algorithmen, inklusive WP-22 Lifecycle-Modifier, Intent-Boosting, WP-15c Diversity Engine und WP-14 Modularisierung."
--- ---
# Retrieval & Scoring Algorithmen # Retrieval & Scoring Algorithmen
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 (WP-14). 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 (WP-14).
## 1. Scoring Formel (v2.7.0) ## 1. Scoring Formel (WP-15c / v1.0.3)
Der Gesamtscore eines Treffers berechnet sich als gewichtete Summe. Alle Gewichte ($W$) und Modifier ($M$) sind in `retriever.yaml` und `decision_engine.yaml` konfigurierbar. Seit WP-15c nutzt Mindnet eine **hybride Multiplikations-Formel** für präziseres Scoring. Alle Gewichte ($W$) und Modifier ($M$) sind in `retriever.yaml` und `decision_engine.yaml` konfigurierbar.
$$ $$
TotalScore = (W_{sem} \cdot S_{sem} \cdot W_{type} \cdot M_{status}) + (W_{edge} \cdot B_{edge}) + (W_{cent} \cdot B_{cent}) + B_{intent} Final = (Semantic \cdot StatusMult) \cdot (1 + TypeBoost + EdgeBonus + CentBonus)
$$ $$
**Komponenten:**
* **Base Score:** `Semantic * StatusMult` (Lifecycle-Filter)
* **Boosts:** `TypeBoost + EdgeBonus + CentBonus` (additiv, dann multiplikativ auf Base)
* **Graph Boost Factor:** Intent-spezifische Verstärkung (1.5x bei aktivem Intent)
**Vorteil:** Status wirkt als Multiplikator auf die Basis-Relevanz, Graph-Boni werden proportional verstärkt.
### Die Komponenten (Klassisch v2.4) ### Die Komponenten (Klassisch v2.4)
**1. Semantic Score ($S_{sem}$):** **1. Semantic Score ($S_{sem}$):**
@ -43,13 +50,15 @@ $$
### Die WP-22 Erweiterungen (v2.7.0) ### Die WP-22 Erweiterungen (v2.7.0)
**5. Status Modifier ($M_{status}$):** **5. Status Modifier ($M_{status}$) - Status-Gatekeeper:**
* **Herkunft:** Feld `status` aus dem Frontmatter (verarbeitet in `retriever_scoring.get_status_multiplier`). * **Herkunft:** Feld `status` aus dem Frontmatter (verarbeitet in `retriever_scoring.get_status_multiplier`).
* **Zweck:** Bestraft unfertiges Wissen (Drafts) oder bevorzugt stabiles Wissen. * **Zweck:** Wirkt als **Multiplikator** auf die Basis-Semantik. Bestraft unfertiges Wissen (Drafts) oder bevorzugt stabiles Wissen.
* **Werte (Auftrag WP-22):** * `stable`: **1.2** (Belohnung für verifiziertes Wissen). * **Werte (WP-15c):**
* `active`: **1.0** (Standard-Gewichtung). * `stable`: **1.2** (Belohnung für verifiziertes Wissen)
* `draft`: **0.5** (Malus für unfertige Fragmente). * `active`: **1.0** (Standard-Gewichtung)
* `system`: Exkludiert (siehe Ingestion Lifecycle Filter). * `draft`: **0.5** (Malus für unfertige Fragmente)
* `system`: Exkludiert (siehe Ingestion Lifecycle Filter)
* **Impact:** Der Status wirkt direkt auf die semantische Ähnlichkeit, bevor Graph-Boni berechnet werden.
**6. Intent Boost ($B_{intent}$):** **6. Intent Boost ($B_{intent}$):**
* **Herkunft:** Dynamische Injektion durch die Decision Engine basierend auf der Nutzerfrage. * **Herkunft:** Dynamische Injektion durch die Decision Engine basierend auf der Nutzerfrage.
@ -70,16 +79,25 @@ Seit v2.9 ist die Retrieval-Engine im spezialisierten Paket `app.core.retrieval`
* Diese delegiert an `app.core.graph.graph_subgraph`, um direkte Nachbarn aus der `_edges` Collection zu laden. * Diese delegiert an `app.core.graph.graph_subgraph`, um direkte Nachbarn aus der `_edges` Collection zu laden.
* Konstruktion eines in-memory Graphen zur Berechnung topologischer Boni. * Konstruktion eines in-memory Graphen zur Berechnung topologischer Boni.
**Phase 3: Re-Ranking (Modular)** **Phase 3: Graph-Intelligenz & Super-Edge Aggregation (WP-15c)**
* **Super-Edge Aggregation:** Parallele Kanten zwischen zwei Notizen (z.B. auf verschiedene Sections) werden mathematisch zu einer "Super-Edge" aggregiert:
* Primäre Kante zählt voll (höchstes Gewicht)
* Weitere Kanten werden mit Dämpfungsfaktor **0.1** gewichtet
* Verhindert Score-Explosionen durch multiple Links
* **Provenance Weighting:** Kanten werden nach Provenance gewichtet (`explicit`=1.0, `smart`=0.9, `rule`=0.7)
* **Intent Boost:** Dynamische Multiplikatoren für spezifische Kanten-Typen (z.B. `caused_by` bei "Warum"-Fragen)
**Phase 4: Re-Ranking & Diversity Pooling (WP-15c)**
* Der Orchestrator übergibt den Graphen und die Seeds an die `ScoringEngine` (`retriever_scoring.py`). * Der Orchestrator übergibt den Graphen und die Seeds an die `ScoringEngine` (`retriever_scoring.py`).
* Berechnung der finalen Scores unter Berücksichtigung von $B_{edge}$, $B_{cent}$ sowie der Lifecycle- und Intent-Modifier. * Berechnung der finalen Scores unter Berücksichtigung von $B_{edge}$, $B_{cent}$ sowie der Lifecycle- und Intent-Modifier.
* **Note-Level Diversity Pooling:** Pro `note_id` wird nur der relevanteste Treffer behalten (verhindert "Note-Flooding").
* Sortierung absteigend nach `TotalScore` und Limitierung auf die angeforderten Top-Resultate. * Sortierung absteigend nach `TotalScore` und Limitierung auf die angeforderten Top-Resultate.
--- ---
## 3. Explanation Layer (WP-22 Update) ## 3. Explanation Layer (WP-15c)
Bei `explain=True` generiert das System eine detaillierte Begründung inklusive Provenienz-Informationen. Bei `explain=True` generiert das System eine detaillierte Begründung inklusive Provenienz-Informationen. Der Explanation Layer liefert detaillierte Begründungen für jeden Bonus (z.B. Sektions-Links oder Hub-Zentralität), was die Transparenz massiv erhöht.
**Erweiterte JSON-Struktur:** **Erweiterte JSON-Struktur:**

View File

@ -0,0 +1,123 @@
# Branch Merge Commit Message: WP15c
```
feat: Multigraph-Support, Diversity Engine & Provenance Firewall (v2.9.1)
## Graph Topology & Edge Management (WP-15c)
### Section-Präzision & Multigraph-Modus
- Präzise Erkennung von Obsidian-Ankern (`[[Note#Section]]`) und Self-Links (`[[#Section]]`)
- Links werden in `target_id="Note"` und `target_section="Section"` aufgelöst
- Edge-ID enthält `variant` (Section) für eindeutige Identifikation
- Multigraph-Modus: Mehrere Kanten zwischen denselben Notizen möglich, wenn sie auf verschiedene Sections zeigen
- Behebt "Phantom-Knoten" durch korrekte Trennung von Note-Name und Abschnitt
**Geänderte Dateien:**
- `app/core/graph/graph_utils.py`: `parse_link_target()` für Section-Extraktion & Self-Links (v1.1.2)
- `app/core/graph/graph_derive_edges.py`: Multigraph-Support, `target_section` in Edge-Payload (v1.1.2)
- `app/core/graph/graph_subgraph.py`: Metadaten-Persistenz (v1.2.0)
- `app/core/graph/graph_db_adapter.py`: Vollständige Metadaten-Durchreichung (v1.1.1)
### Provenance Firewall (Edge Registry v0.8.0)
- System-Kanten (`next`, `prev`, `belongs_to`) dürfen nur mit `provenance="structure"` gesetzt werden
- Alle anderen Provenienzen (`explicit`, `semantic_ai`, `inherited`, `global_pool`, `rule`) werden blockiert
- Automatischer Fallback auf `related_to` bei unerlaubter Verwendung
- Logging in `data/logs/unknown_edges.jsonl` für Admin-Review
**Geänderte Dateien:**
- `app/services/edge_registry.py`: Provenance Firewall (v0.8.0)
## Retrieval-Intelligenz (The Diversity Engine)
### Note-Level Diversity Pooling
- Pro `note_id` wird nur der relevanteste Treffer im Endergebnis behalten
- Verhindert "Note-Flooding" (Dominanz einer einzigen Notiz durch viele ähnliche Chunks)
- Workflow: Sortierung nach finalem Score → Diversity-Pooling → Begrenzung auf `top_k`
**Geänderte Dateien:**
- `app/core/retrieval/retriever.py`: Note-Level Diversity Pooling (v0.7.0)
### Super-Edge Aggregation
- Parallele Kanten zwischen zwei Notizen werden mathematisch zu einer "Super-Edge" aggregiert
- Primäre Kante zählt voll, weitere Kanten werden mit Dämpfungsfaktor **0.1** gewichtet
- Verhindert Score-Explosionen durch multiple Links auf verschiedene Sections
- Metadaten: `is_super_edge` Flag und `edge_count` für Explanation Layer
**Geänderte Dateien:**
- `app/core/retrieval/retriever.py`: Super-Edge Aggregation (v0.7.0)
### Metadaten-Persistenz
- In-Memory Subgraph und Datenbank-Adapter erweitert für durchgängige Metadaten-Erhaltung
- `target_section`, `provenance` und `confidence` werden vollständig erhalten
- Ermöglicht präzises Retrieval-Reasoning und Explanation Layer
**Geänderte Dateien:**
- `app/core/graph/graph_subgraph.py`: Metadaten-Persistenz (v1.2.0)
- `app/core/graph/graph_db_adapter.py`: Vollständige Metadaten-Durchreichung (v1.1.1)
## Mathematisches Scoring (WP-22 Integration)
### Hybrid Scoring Formula (v1.0.3)
- Neue Formel: `Final = (Semantic * StatusMult) * (1 + TypeBoost + EdgeBonus + CentBonus)`
- Status-Gatekeeper: `stable` = 1.2, `draft` = 0.5, `active` = 1.0
- Graph Boost Factor: Intent-spezifische Verstärkung (1.5x bei aktivem Intent)
- Status wirkt als Multiplikator auf die Basis-Relevanz, Graph-Boni werden proportional verstärkt
**Geänderte Dateien:**
- `app/core/retrieval/retriever_scoring.py`: Hybrid Scoring Formula (v1.0.3)
### Explanation Layer
- Detaillierte Begründungen für jeden Bonus (Sektions-Links, Hub-Zentralität, Super-Edge-Informationen)
- Provenance-Informationen für erhöhte Transparenz
- Massiv erhöhte Transparenz für Debugging und Nutzer-Vertrauen
**Geänderte Dateien:**
- `app/core/retrieval/retriever.py`: Explanation Layer (v0.7.0)
## Ingestion & Profil-Synchronisation
### Registry-First Profiling (v2.13.12)
- Hierarchische Auflösung: Frontmatter > types.yaml Typ-Config > Global Defaults
- Konsistente Verarbeitung je nach Notiz-Typ
- Note-Typen wie `value` erhalten automatisch das korrekte Profil (`structured_smart_edges_strict`)
**Geänderte Dateien:**
- `app/core/ingestion/ingestion_processor.py`: Registry-First Profiling (v2.13.12)
## Impact & Breaking Changes
### Keine Migration erforderlich
**WICHTIG:** Diese Version ist **rückwärtskompatibel**. Bestehende Vaults funktionieren ohne Re-Import.
**Empfehlung:** Optionaler Re-Import für optimale Nutzung der neuen Features:
```bash
python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
```
### Fixes
- ✅ Resolves: "Phantom-Knoten" durch korrekte Trennung von Note-Name und Section
- ✅ Resolves: Score-Explosionen durch multiple Links auf verschiedene Sections
- ✅ Resolves: "Note-Flooding" durch fehlende Diversity-Filterung
- ✅ Resolves: Inkonsistente Metadaten durch fehlende Persistenz im Subgraph
- ✅ Resolves: Manipulation von System-Kanten durch Provenance Firewall
## Dokumentation
Alle relevanten Dokumente aktualisiert:
- `02_concept_graph_logic.md`: Provenance Firewall, Self-Links, Multigraph-Support
- `03_tech_retrieval_scoring.md`: Hybrid Scoring Formula, Note-Level Diversity, Super-Edge Aggregation
- `03_tech_data_model.md`: Section-Support, Metadaten-Persistenz
- `03_tech_ingestion_pipeline.md`: Registry-First Profiling (bereits dokumentiert)
## Versionen
- Edge Registry: v0.8.0
- Graph Utils / Derive Edges: v1.1.2
- Graph Subgraph: v1.2.0
- Graph DB Adapter: v1.1.1
- Retriever: v0.7.0
- Retriever Scoring: v1.0.3
- Ingestion Processor: v2.13.12
Closes #[issue-number]
```

View File

@ -0,0 +1,267 @@
# Release Notes: Mindnet v2.9.1 (WP15c)
**Release Date:** 2025-12-31
**Type:** Feature Release - Multigraph & Diversity Engine
**Branch:** WP15c
---
## 🎯 Übersicht
Diese Version transformiert den Graphen von einer flachen Struktur zu einem **hochpräzisen Multigraphen** mit intelligenter Ergebnis-Filterung (Diversity) und gewichtetem Scoring. Die Änderungen verbessern die Retrieval-Qualität durch präzise Sektions-Links, Note-Level Diversity Pooling und mathematische Super-Edge Aggregation.
---
## ✨ Neue Features
### Section-Präzision & Multigraph-Modus
Mindnet erkennt nun präzise **Obsidian-Anker** (`[[Note#Section]]`) und **Self-Links** (`[[#Section]]`):
**Obsidian-Anker:**
```markdown
[[rel:based_on Mein Leitbild#P3 Disziplin]]
```
**Self-Links:**
```markdown
[[#P3 Disziplin]] → Verlinkt innerhalb derselben Note
```
**Technische Details:**
- Links werden in `target_id="Note"` und `target_section="Section"` aufgelöst
- Edge-IDs enthalten `variant` (Section) für eindeutige Identifikation
- **Multigraph-Modus:** Mehrere Kanten zwischen denselben Notizen möglich, wenn sie auf verschiedene Sections zeigen
- Verhindert "Phantom-Knoten" durch korrekte Trennung von Note-Name und Abschnitt
### Provenance Firewall (Edge Registry v0.8.0)
Die **Edge Registry** stellt sicher, dass System-Kanten (`next`, `prev`, `belongs_to`) ausschließlich durch interne Struktur-Prozesse und nicht durch Nutzer oder KI manipuliert werden können:
**Schutz-Mechanismus:**
- System-Kanten dürfen nur mit `provenance="structure"` gesetzt werden
- Alle anderen Provenienzen (`explicit`, `semantic_ai`, `inherited`, `global_pool`, `rule`) werden blockiert
- Automatischer Fallback auf `related_to` bei unerlaubter Verwendung
- Logging in `data/logs/unknown_edges.jsonl` für Admin-Review
**Vorteil:** Garantiert Graph-Integrität und verhindert Manipulationen an der internen Struktur.
### Note-Level Diversity Pooling
Um das **"Note-Flooding"** (Dominanz einer einzigen Notiz durch viele ähnliche Chunks) zu verhindern, wird pro `note_id` nur noch der relevanteste Treffer im Endergebnis behalten:
**Workflow:**
1. Alle Kandidaten werden nach finalem Score sortiert
2. Pro `note_id` wird nur der Hit mit dem höchsten `total_score` behalten
3. Begrenzung auf `top_k` nach dem Diversity-Pooling
**Impact:** Verhindert, dass 10 Chunks derselben Note andere KeyNotes verdrängen. Erhöht die Vielfalt der Suchergebnisse.
### Super-Edge Aggregation
Parallele Kanten zwischen zwei Notizen werden mathematisch zu einer **"Super-Edge"** aggregiert:
**Aggregations-Logik:**
- Primäre Kante (höchstes Gewicht) zählt voll
- Jede weitere Kante (z.B. auf eine andere Sektion) wird mit Dämpfungsfaktor **0.1** gewichtet
- Formel: `total_weight = primary_weight + sum(secondary_weights * 0.1)`
**Vorteil:** Verhindert Score-Explosionen durch multiple Links auf verschiedene Sections derselben Note.
**Metadaten:**
- `is_super_edge`: Flag für Explanation Layer
- `edge_count`: Anzahl der aggregierten Kanten
---
## 🔧 Verbesserungen
### Mathematisches Scoring (WP-22 Integration)
Die Engine berechnet den finalen Score basierend auf einer **hybriden Multiplikations-Formel**:
```
Final = (Semantic * StatusMult) * (1 + TypeBoost + EdgeBonus + CentBonus)
```
**Komponenten:**
- **Base Score:** `Semantic * StatusMult` (Lifecycle-Filter)
- **Status-Gatekeeper:** `stable` = 1.2, `draft` = 0.5, `active` = 1.0
- **Boosts:** `TypeBoost + EdgeBonus + CentBonus` (additiv, dann multiplikativ auf Base)
- **Graph Boost Factor:** Intent-spezifische Verstärkung (1.5x bei aktivem Intent)
**Vorteil:** Status wirkt als Multiplikator auf die Basis-Relevanz, Graph-Boni werden proportional verstärkt.
### Explanation Layer
Der Retriever liefert detaillierte Begründungen für jeden Bonus:
- Sektions-Links (z.B. "Link zu 'Mein Leitbild#P3 Disziplin'")
- Hub-Zentralität (z.B. "Note ist zentraler Knoten mit 5 eingehenden Kanten")
- Super-Edge-Informationen (z.B. "3 parallele Kanten aggregiert")
- Provenance-Informationen (z.B. "Explizite Kante vom Nutzer")
**Impact:** Massiv erhöhte Transparenz für Debugging und Nutzer-Vertrauen.
### Metadaten-Persistenz
Der In-Memory Subgraph und der Datenbank-Adapter wurden so erweitert, dass Metadaten durchgängig erhalten bleiben:
**Erhaltene Metadaten:**
- `target_section`: Abschnitts-Name für präzise Verlinkung
- `provenance`: Herkunft der Kante (explicit, smart, rule, structure)
- `confidence`: Vertrauenswürdigkeit (0.0 - 1.0)
- `is_super_edge`: Flag für aggregierte Kanten
**Vorteil:** Ermöglicht präzises Retrieval-Reasoning und Explanation Layer.
### Profil-Synchronisation (Ingestion v2.13.12)
Der Ingestion-Prozessor löst Chunking-Profile hierarchisch über die `types.yaml` auf:
**Hierarchie:**
1. **Frontmatter** (höchste Priorität)
2. **types.yaml Typ-Config**
3. **Global Defaults**
**Impact:** Konsistente Verarbeitung je nach Notiz-Typ. Note-Typen wie `value` erhalten automatisch das korrekte Profil (`structured_smart_edges_strict`).
---
## 🐛 Bugfixes
- ✅ **Behoben:** "Phantom-Knoten" durch korrekte Trennung von Note-Name und Section in `target_id`
- ✅ **Behoben:** Score-Explosionen durch multiple Links auf verschiedene Sections
- ✅ **Behoben:** "Note-Flooding" durch fehlende Diversity-Filterung
- ✅ **Behoben:** Inkonsistente Metadaten durch fehlende Persistenz im Subgraph
- ✅ **Behoben:** Manipulation von System-Kanten durch Provenance Firewall
---
## ⚠️ Breaking Changes & Migration
### Keine Migration erforderlich
**WICHTIG:** Diese Version ist **rückwärtskompatibel**. Bestehende Vaults funktionieren ohne Re-Import.
**Empfehlung:** Optionaler Re-Import für optimale Nutzung der neuen Features:
```bash
python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
```
**Was passiert beim Re-Import?**
- Bestehende Links werden neu geparst und in `target_id` + `target_section` aufgeteilt
- Self-Links (`[[#Section]]`) werden korrekt aufgelöst
- Edge-Struktur wird konsolidiert (Multigraph-Support)
---
## 📚 API-Änderungen
### Keine Breaking Changes
Die API bleibt vollständig kompatibel. Neue Metadaten werden optional zurückgegeben:
**EdgeDTO (erweitert):**
```python
class EdgeDTO(BaseModel):
# ... bestehende Felder ...
target_section: Optional[str] = None # Neu: Abschnitts-Name
is_super_edge: Optional[bool] = False # Neu: Aggregations-Flag
```
**QueryResponse (erweitert):**
- Explanation Layer enthält nun Super-Edge-Informationen
- Metadaten wie `target_section` werden in Edge-Objekten zurückgegeben
---
## 📖 Dokumentation
Alle relevanten Dokumente wurden aktualisiert:
- ✅ `02_concept_graph_logic.md`: Provenance Firewall, Self-Links, Multigraph-Support
- ✅ `03_tech_retrieval_scoring.md`: Hybrid Scoring Formula, Note-Level Diversity, Super-Edge Aggregation
- ✅ `03_tech_data_model.md`: Section-Support, Metadaten-Persistenz
- ✅ `03_tech_ingestion_pipeline.md`: Registry-First Profiling (bereits dokumentiert)
---
## 🔄 Technische Details
### Geänderte Module
**Graph Topology:**
- `app/services/edge_registry.py`: Provenance Firewall (v0.8.0)
- `app/core/graph/graph_utils.py`: `parse_link_target()` für Section-Extraktion & Self-Links (v1.1.2)
- `app/core/graph/graph_derive_edges.py`: Multigraph-Support, `target_section` in Edge-Payload (v1.1.2)
- `app/core/graph/graph_subgraph.py`: Metadaten-Persistenz (v1.2.0)
- `app/core/graph/graph_db_adapter.py`: Vollständige Metadaten-Durchreichung (v1.1.1)
**Retrieval:**
- `app/core/retrieval/retriever.py`: Note-Level Diversity Pooling, Super-Edge Aggregation (v0.7.0)
- `app/core/retrieval/retriever_scoring.py`: Hybrid Scoring Formula (v1.0.3)
**Ingestion:**
- `app/core/ingestion/ingestion_processor.py`: Registry-First Profiling (v2.13.12)
### Versionsnummern
- Edge Registry: **v0.8.0**
- Graph Utils / Derive Edges: **v1.1.2**
- Graph Subgraph: **v1.2.0**
- Graph DB Adapter: **v1.1.1**
- Retriever: **v0.7.0**
- Retriever Scoring: **v1.0.3**
- Ingestion Processor: **v2.13.12**
---
## 🚀 Upgrade-Pfad
### Für Administratoren
1. **Code aktualisieren:**
```bash
git pull origin main
source .venv/bin/activate
pip install -r requirements.txt
```
2. **Services neu starten:**
```bash
sudo systemctl restart mindnet-prod
sudo systemctl restart mindnet-ui-prod
```
3. **Optional: Re-Import für optimale Nutzung:**
```bash
python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
```
### Für Entwickler
- Keine Code-Änderungen erforderlich, wenn nur API genutzt wird
- Frontend kann neue Metadaten (`target_section`, `is_super_edge`) optional nutzen
- Explanation Layer enthält nun Super-Edge-Informationen
---
## 📝 Bekannte Einschränkungen
- **Re-Import-Dauer:** Große Vaults (>10.000 Notizen) können 30+ Minuten benötigen (optional)
- **Temporärer Speicher:** Während des Re-Imports kann Qdrant-Speicher temporär ansteigen
---
## 🙏 Danksagungen
Diese Version wurde durch umfangreiche Code-Analyse und Dokumentationsprüfung ermöglicht. Besonderer Fokus lag auf:
- Transformation zu einem hochpräzisen Multigraphen
- Intelligente Ergebnis-Filterung (Diversity Engine)
- Mathematisches Scoring mit gewichteten Boni
- Graph-Integrität durch Provenance Firewall
---
**Vollständige Changelog:** Siehe Git-Commits für detaillierte Änderungen
**Support:** Bei Fragen siehe [Admin Operations Guide](../04_Operations/04_admin_operations.md)