Update graph_db_adapter.py, graph_derive_edges.py, graph_subgraph.py, graph_utils.py, ingestion_processor.py, and retriever.py to version 4.1.0: Introduce Scope-Awareness and Section-Filtering features, enhancing edge retrieval and processing. Implement Note-Scope Zones extraction from Markdown, improve edge ID generation with target_section, and prioritize Note-Scope Links during de-duplication. Update documentation for clarity and consistency across modules.
This commit is contained in:
parent
be2bed9927
commit
39fd15b565
|
|
@ -1,9 +1,11 @@
|
|||
"""
|
||||
FILE: app/core/graph/graph_db_adapter.py
|
||||
DESCRIPTION: Datenbeschaffung aus Qdrant für den Graphen.
|
||||
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.
|
||||
AUDIT v1.2.0: Gold-Standard v4.1.0 - Scope-Awareness & Section-Filtering.
|
||||
- Erweiterte Suche nach chunk_id-Edges für Scope-Awareness
|
||||
- Optionales target_section-Filtering für präzise Section-Links
|
||||
- Vollständige Metadaten-Unterstützung (provenance, confidence, virtual)
|
||||
VERSION: 1.2.0 (WP-24c: Gold-Standard v4.1.0)
|
||||
"""
|
||||
from typing import List, Dict, Optional
|
||||
from qdrant_client import QdrantClient
|
||||
|
|
@ -17,11 +19,22 @@ def fetch_edges_from_qdrant(
|
|||
prefix: str,
|
||||
seeds: List[str],
|
||||
edge_types: Optional[List[str]] = None,
|
||||
target_section: Optional[str] = None,
|
||||
chunk_ids: Optional[List[str]] = None,
|
||||
limit: int = 2048,
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Holt Edges aus der Datenbank basierend auf Seed-IDs.
|
||||
WP-15c: Erhält alle Metadaten für das Note-Level Diversity Pooling.
|
||||
WP-24c v4.1.0: Scope-Aware Edge Retrieval mit Section-Filtering.
|
||||
|
||||
Args:
|
||||
client: Qdrant Client
|
||||
prefix: Collection-Präfix
|
||||
seeds: Liste von Note-IDs für die Suche
|
||||
edge_types: Optionale Filterung nach Kanten-Typen
|
||||
target_section: Optionales Section-Filtering (für präzise Section-Links)
|
||||
chunk_ids: Optionale Liste von Chunk-IDs für Scope-Awareness (Chunk-Level Edges)
|
||||
limit: Maximale Anzahl zurückgegebener Edges
|
||||
"""
|
||||
if not seeds or limit <= 0:
|
||||
return []
|
||||
|
|
@ -30,13 +43,21 @@ def fetch_edges_from_qdrant(
|
|||
# Rückgabe: (notes_col, chunks_col, edges_col)
|
||||
_, _, edges_col = collection_names(prefix)
|
||||
|
||||
# Wir suchen Kanten, bei denen die Seed-IDs entweder Quelle, Ziel oder Kontext-Note sind.
|
||||
# WP-24c v4.1.0: Scope-Awareness - Suche nach Note- UND Chunk-Level Edges
|
||||
seed_conditions = []
|
||||
for field in ("source_id", "target_id", "note_id"):
|
||||
for s in seeds:
|
||||
seed_conditions.append(
|
||||
rest.FieldCondition(key=field, match=rest.MatchValue(value=str(s)))
|
||||
)
|
||||
|
||||
# Chunk-Level Edges: Wenn chunk_ids angegeben, suche auch nach chunk_id als source_id
|
||||
if chunk_ids:
|
||||
for cid in chunk_ids:
|
||||
seed_conditions.append(
|
||||
rest.FieldCondition(key="source_id", match=rest.MatchValue(value=str(cid)))
|
||||
)
|
||||
|
||||
seeds_filter = rest.Filter(should=seed_conditions) if seed_conditions else None
|
||||
|
||||
# Optionaler Filter auf spezifische Kanten-Typen (z.B. für Intent-Routing)
|
||||
|
|
@ -48,11 +69,20 @@ def fetch_edges_from_qdrant(
|
|||
]
|
||||
type_filter = rest.Filter(should=type_conds)
|
||||
|
||||
# WP-24c v4.1.0: Section-Filtering für präzise Section-Links
|
||||
section_filter = None
|
||||
if target_section:
|
||||
section_filter = rest.Filter(must=[
|
||||
rest.FieldCondition(key="target_section", match=rest.MatchValue(value=str(target_section)))
|
||||
])
|
||||
|
||||
must = []
|
||||
if seeds_filter:
|
||||
must.append(seeds_filter)
|
||||
if type_filter:
|
||||
must.append(type_filter)
|
||||
if section_filter:
|
||||
must.append(section_filter)
|
||||
|
||||
flt = rest.Filter(must=must) if must else None
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,14 @@ DESCRIPTION: Hauptlogik zur Kanten-Aggregation und De-Duplizierung.
|
|||
- Präzises Sektions-Splitting via parse_link_target.
|
||||
- v4.1.0: Eindeutige ID-Generierung pro Sektions-Variante (Multigraph).
|
||||
- Ermöglicht dem Retriever die Super-Edge-Aggregation.
|
||||
WP-24c v4.2.0: Note-Scope Extraktions-Zonen für globale Referenzen.
|
||||
- Header-basierte Identifikation von Note-Scope Zonen
|
||||
- Automatische Scope-Umschaltung (chunk -> note)
|
||||
- Priorisierung: Note-Scope Links haben Vorrang bei Duplikaten
|
||||
VERSION: 4.2.0 (WP-24c: Note-Scope Zones)
|
||||
STATUS: Active
|
||||
"""
|
||||
import re
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
from .graph_utils import (
|
||||
_get, _edge, _mk_edge_id, _dedupe_seq, parse_link_target,
|
||||
|
|
@ -15,20 +22,139 @@ from .graph_extractors import (
|
|||
extract_typed_relations, extract_callout_relations, extract_wikilinks
|
||||
)
|
||||
|
||||
# WP-24c v4.2.0: Header-basierte Identifikation von Note-Scope Zonen
|
||||
NOTE_SCOPE_ZONE_HEADERS = [
|
||||
"Smart Edges",
|
||||
"Relationen",
|
||||
"Global Links",
|
||||
"Note-Level Relations",
|
||||
"Globale Verbindungen"
|
||||
]
|
||||
|
||||
def extract_note_scope_zones(markdown_body: str) -> List[Tuple[str, str]]:
|
||||
"""
|
||||
WP-24c v4.2.0: Extrahiert Note-Scope Zonen aus Markdown.
|
||||
|
||||
Identifiziert Sektionen mit spezifischen Headern (z.B. "## Smart Edges")
|
||||
und extrahiert alle darin enthaltenen Links.
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, str]]: Liste von (kind, target) Tupeln
|
||||
"""
|
||||
if not markdown_body:
|
||||
return []
|
||||
|
||||
edges: List[Tuple[str, str]] = []
|
||||
|
||||
# Regex für Header-Erkennung (## oder ###)
|
||||
header_pattern = r'^#{2,3}\s+(.+?)$'
|
||||
|
||||
lines = markdown_body.split('\n')
|
||||
in_zone = False
|
||||
zone_content = []
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
# Prüfe auf Header
|
||||
header_match = re.match(header_pattern, line.strip())
|
||||
if header_match:
|
||||
header_text = header_match.group(1).strip()
|
||||
|
||||
# Prüfe, ob dieser Header eine Note-Scope Zone ist
|
||||
is_zone_header = any(
|
||||
header_text.lower() == zone_header.lower()
|
||||
for zone_header in NOTE_SCOPE_ZONE_HEADERS
|
||||
)
|
||||
|
||||
if is_zone_header:
|
||||
in_zone = True
|
||||
zone_content = []
|
||||
continue
|
||||
else:
|
||||
# Neuer Header gefunden, der keine Zone ist -> Zone beendet
|
||||
if in_zone:
|
||||
# Verarbeite gesammelten Inhalt
|
||||
zone_text = '\n'.join(zone_content)
|
||||
# Extrahiere Typed Relations
|
||||
typed, _ = extract_typed_relations(zone_text)
|
||||
edges.extend(typed)
|
||||
# Extrahiere Wikilinks (als related_to)
|
||||
wikilinks = extract_wikilinks(zone_text)
|
||||
for wl in wikilinks:
|
||||
edges.append(("related_to", wl))
|
||||
# Extrahiere Callouts
|
||||
callouts, _ = extract_callout_relations(zone_text)
|
||||
edges.extend(callouts)
|
||||
in_zone = False
|
||||
zone_content = []
|
||||
|
||||
# Sammle Inhalt, wenn wir in einer Zone sind
|
||||
if in_zone:
|
||||
zone_content.append(line)
|
||||
|
||||
# Verarbeite letzte Zone (falls am Ende des Dokuments)
|
||||
if in_zone and zone_content:
|
||||
zone_text = '\n'.join(zone_content)
|
||||
typed, _ = extract_typed_relations(zone_text)
|
||||
edges.extend(typed)
|
||||
wikilinks = extract_wikilinks(zone_text)
|
||||
for wl in wikilinks:
|
||||
edges.append(("related_to", wl))
|
||||
callouts, _ = extract_callout_relations(zone_text)
|
||||
edges.extend(callouts)
|
||||
|
||||
return edges
|
||||
|
||||
def build_edges_for_note(
|
||||
note_id: str,
|
||||
chunks: List[dict],
|
||||
note_level_references: Optional[List[str]] = None,
|
||||
include_note_scope_refs: bool = False,
|
||||
markdown_body: Optional[str] = None,
|
||||
) -> List[dict]:
|
||||
"""
|
||||
Erzeugt und aggregiert alle Kanten für eine Note.
|
||||
Sorgt für die physische Trennung von Sektions-Links via Edge-ID.
|
||||
WP-24c v4.2.0: Unterstützt Note-Scope Extraktions-Zonen.
|
||||
|
||||
Args:
|
||||
note_id: ID der Note
|
||||
chunks: Liste von Chunk-Payloads
|
||||
note_level_references: Optionale Liste von Note-Level Referenzen
|
||||
include_note_scope_refs: Ob Note-Scope Referenzen eingeschlossen werden sollen
|
||||
markdown_body: Optionaler Original-Markdown-Text für Note-Scope Zonen-Extraktion
|
||||
"""
|
||||
edges: List[dict] = []
|
||||
# note_type für die Ermittlung der edge_defaults (types.yaml)
|
||||
note_type = _get(chunks[0], "type") if chunks else "concept"
|
||||
|
||||
# WP-24c v4.2.0: Note-Scope Zonen Extraktion (VOR Chunk-Verarbeitung)
|
||||
note_scope_edges: List[dict] = []
|
||||
if markdown_body:
|
||||
zone_links = extract_note_scope_zones(markdown_body)
|
||||
for kind, raw_target in zone_links:
|
||||
target, sec = parse_link_target(raw_target, note_id)
|
||||
if not target:
|
||||
continue
|
||||
|
||||
# WP-24c v4.2.0: Note-Scope Links mit scope: "note" und source_id: note_id
|
||||
# ID-Konsistenz: Exakt wie in Phase 2 (Symmetrie-Prüfung)
|
||||
payload = {
|
||||
"edge_id": _mk_edge_id(kind, note_id, target, "note", target_section=sec),
|
||||
"provenance": "explicit:note_zone",
|
||||
"rule_id": "explicit:note_zone",
|
||||
"confidence": PROVENANCE_PRIORITY.get("explicit:note_zone", 1.0)
|
||||
}
|
||||
if sec:
|
||||
payload["target_section"] = sec
|
||||
|
||||
note_scope_edges.append(_edge(
|
||||
kind=kind,
|
||||
scope="note",
|
||||
source_id=note_id, # WP-24c v4.2.0: source_id = note_id (nicht chunk_id)
|
||||
target_id=target,
|
||||
note_id=note_id,
|
||||
extra=payload
|
||||
))
|
||||
|
||||
# 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):
|
||||
|
|
@ -162,15 +288,45 @@ def build_edges_for_note(
|
|||
"provenance": "rule", "rule_id": "derived:backlink", "confidence": PROVENANCE_PRIORITY["derived:backlink"]
|
||||
}))
|
||||
|
||||
# 4) De-Duplizierung (In-Place)
|
||||
# 4) WP-24c v4.2.0: Note-Scope Edges hinzufügen (VOR De-Duplizierung)
|
||||
# Diese werden mit höherer Priorität behandelt, da sie explizite Note-Level Verbindungen sind
|
||||
edges.extend(note_scope_edges)
|
||||
|
||||
# 5) De-Duplizierung (In-Place) mit Priorisierung
|
||||
# WP-24c v4.2.0: Note-Scope Links haben Vorrang bei Duplikaten
|
||||
# WP-24c v4.1.0: Da die EDGE-ID nun auf 5 Parametern basiert (inkl. target_section),
|
||||
# bleiben Links auf unterschiedliche Abschnitte derselben Note als eigenständige
|
||||
# Kanten erhalten. Nur identische Sektions-Links werden nach Confidence konsolidiert.
|
||||
# Kanten erhalten. Nur identische Sektions-Links werden nach Confidence und Provenance konsolidiert.
|
||||
unique_map: Dict[str, dict] = {}
|
||||
for e in edges:
|
||||
eid = e["edge_id"]
|
||||
# Höhere Confidence gewinnt bei identischer ID
|
||||
if eid not in unique_map or e.get("confidence", 0) > unique_map[eid].get("confidence", 0):
|
||||
|
||||
# WP-24c v4.2.0: Priorisierung bei Duplikaten
|
||||
# 1. Note-Scope Links (explicit:note_zone) haben höchste Priorität
|
||||
# 2. Dann Confidence
|
||||
# 3. Dann Provenance-Priority
|
||||
if eid not in unique_map:
|
||||
unique_map[eid] = e
|
||||
else:
|
||||
existing = unique_map[eid]
|
||||
existing_prov = existing.get("provenance", "")
|
||||
new_prov = e.get("provenance", "")
|
||||
|
||||
# Note-Scope Zone Links haben Vorrang
|
||||
is_existing_note_zone = existing_prov == "explicit:note_zone"
|
||||
is_new_note_zone = new_prov == "explicit:note_zone"
|
||||
|
||||
if is_new_note_zone and not is_existing_note_zone:
|
||||
# Neuer Link ist Note-Scope Zone -> ersetze
|
||||
unique_map[eid] = e
|
||||
elif is_existing_note_zone and not is_new_note_zone:
|
||||
# Bestehender Link ist Note-Scope Zone -> behalte
|
||||
pass
|
||||
else:
|
||||
# Beide sind Note-Scope oder beide nicht -> vergleiche Confidence
|
||||
existing_conf = existing.get("confidence", 0)
|
||||
new_conf = e.get("confidence", 0)
|
||||
if new_conf > existing_conf:
|
||||
unique_map[eid] = e
|
||||
|
||||
return list(unique_map.values())
|
||||
|
|
@ -4,7 +4,8 @@ DESCRIPTION: In-Memory Repräsentation eines Graphen für Scoring und Analyse.
|
|||
Zentrale Komponente für die Graph-Expansion (BFS) und Bonus-Berechnung.
|
||||
WP-15c Update: Erhalt von Metadaten (target_section, provenance)
|
||||
für präzises Retrieval-Reasoning.
|
||||
VERSION: 1.2.0
|
||||
WP-24c v4.1.0: Scope-Awareness und Section-Filtering Support.
|
||||
VERSION: 1.3.0 (WP-24c: Gold-Standard v4.1.0)
|
||||
STATUS: Active
|
||||
"""
|
||||
import math
|
||||
|
|
@ -28,6 +29,8 @@ class Subgraph:
|
|||
self.reverse_adj: DefaultDict[str, List[Dict]] = defaultdict(list)
|
||||
self.in_degree: DefaultDict[str, int] = defaultdict(int)
|
||||
self.out_degree: DefaultDict[str, int] = defaultdict(int)
|
||||
# WP-24c v4.1.0: Chunk-Level In-Degree für präzise Scoring-Aggregation
|
||||
self.chunk_level_in_degree: DefaultDict[str, int] = defaultdict(int)
|
||||
|
||||
def add_edge(self, e: Dict) -> None:
|
||||
"""
|
||||
|
|
@ -48,7 +51,9 @@ class Subgraph:
|
|||
"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)
|
||||
"is_super_edge": e.get("is_super_edge", False),
|
||||
"virtual": e.get("virtual", False), # WP-24c v4.1.0: Für Authority-Priorisierung
|
||||
"chunk_id": e.get("chunk_id") # WP-24c v4.1.0: Für RAG-Kontext
|
||||
}
|
||||
|
||||
owner = e.get("note_id")
|
||||
|
|
@ -111,10 +116,21 @@ def expand(
|
|||
seeds: List[str],
|
||||
depth: int = 1,
|
||||
edge_types: Optional[List[str]] = None,
|
||||
chunk_ids: Optional[List[str]] = None,
|
||||
target_section: Optional[str] = None,
|
||||
) -> Subgraph:
|
||||
"""
|
||||
Expandiert ab Seeds entlang von Edges bis zu einer bestimmten Tiefe.
|
||||
Nutzt fetch_edges_from_qdrant für den Datenbankzugriff.
|
||||
WP-24c v4.1.0: Unterstützt Scope-Awareness (chunk_ids) und Section-Filtering.
|
||||
|
||||
Args:
|
||||
client: Qdrant Client
|
||||
prefix: Collection-Präfix
|
||||
seeds: Liste von Note-IDs für die Expansion
|
||||
depth: Maximale Tiefe der Expansion
|
||||
edge_types: Optionale Filterung nach Kanten-Typen
|
||||
chunk_ids: Optionale Liste von Chunk-IDs für Scope-Awareness
|
||||
target_section: Optionales Section-Filtering
|
||||
"""
|
||||
sg = Subgraph()
|
||||
frontier = set(seeds)
|
||||
|
|
@ -124,8 +140,13 @@ def expand(
|
|||
if not frontier:
|
||||
break
|
||||
|
||||
# Batch-Abfrage der Kanten für die aktuelle Ebene
|
||||
payloads = fetch_edges_from_qdrant(client, prefix, list(frontier), edge_types)
|
||||
# WP-24c v4.1.0: Erweiterte Edge-Retrieval mit Scope-Awareness und Section-Filtering
|
||||
payloads = fetch_edges_from_qdrant(
|
||||
client, prefix, list(frontier),
|
||||
edge_types=edge_types,
|
||||
chunk_ids=chunk_ids,
|
||||
target_section=target_section
|
||||
)
|
||||
next_frontier: Set[str] = set()
|
||||
|
||||
for pl in payloads:
|
||||
|
|
@ -133,6 +154,7 @@ def expand(
|
|||
if not src or not tgt: continue
|
||||
|
||||
# WP-15c: Wir übergeben das vollständige Payload an add_edge
|
||||
# WP-24c v4.1.0: virtual Flag wird für Authority-Priorisierung benötigt
|
||||
edge_payload = {
|
||||
"source": src,
|
||||
"target": tgt,
|
||||
|
|
@ -141,7 +163,9 @@ def expand(
|
|||
"note_id": pl.get("note_id"),
|
||||
"provenance": pl.get("provenance", "rule"),
|
||||
"confidence": pl.get("confidence", 1.0),
|
||||
"target_section": pl.get("target_section")
|
||||
"target_section": pl.get("target_section"),
|
||||
"virtual": pl.get("virtual", False), # WP-24c v4.1.0: Für Authority-Priorisierung
|
||||
"chunk_id": pl.get("chunk_id") # WP-24c v4.1.0: Für RAG-Kontext
|
||||
}
|
||||
|
||||
sg.add_edge(edge_payload)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ PROVENANCE_PRIORITY = {
|
|||
"structure:belongs_to": 1.00,
|
||||
"structure:order": 0.95, # next/prev
|
||||
"explicit:note_scope": 1.00,
|
||||
"explicit:note_zone": 1.00, # WP-24c v4.2.0: Note-Scope Zonen (höchste Priorität)
|
||||
"derived:backlink": 0.90,
|
||||
"edge_defaults": 0.70 # Heuristik basierend auf types.yaml
|
||||
}
|
||||
|
|
|
|||
|
|
@ -244,8 +244,15 @@ class IngestionService:
|
|||
chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, file_path=file_path, types_cfg=self.registry)
|
||||
vecs = await self.embedder.embed_documents([c.get("window") or "" for c in chunk_pls]) if chunk_pls else []
|
||||
|
||||
# Kanten-Extraktion
|
||||
raw_edges = build_edges_for_note(note_id, chunk_pls, note_level_references=note_pl.get("references", []))
|
||||
# WP-24c v4.2.0: Kanten-Extraktion mit Note-Scope Zonen Support
|
||||
# Übergabe des Original-Markdown-Texts für Note-Scope Zonen-Extraktion
|
||||
markdown_body = getattr(parsed, "body", "")
|
||||
raw_edges = build_edges_for_note(
|
||||
note_id,
|
||||
chunk_pls,
|
||||
note_level_references=note_pl.get("references", []),
|
||||
markdown_body=markdown_body
|
||||
)
|
||||
|
||||
explicit_edges = []
|
||||
for e in raw_edges:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
FILE: app/core/retrieval/retriever.py
|
||||
DESCRIPTION: Haupt-Schnittstelle für die Suche. Orchestriert Vektorsuche und Graph-Expansion.
|
||||
WP-15c Update: Note-Level Diversity Pooling & Super-Edge Aggregation.
|
||||
VERSION: 0.7.0
|
||||
WP-24c v4.1.0: Gold-Standard - Scope-Awareness, Section-Filtering, Authority-Priorisierung.
|
||||
VERSION: 0.8.0 (WP-24c: Gold-Standard v4.1.0)
|
||||
STATUS: Active
|
||||
DEPENDENCIES: app.config, app.models.dto, app.core.database*, app.core.graph_adapter
|
||||
"""
|
||||
|
|
@ -26,6 +27,9 @@ import app.core.database.qdrant_points as qp
|
|||
|
||||
import app.services.embeddings_client as ec
|
||||
import app.core.graph.graph_subgraph as ga
|
||||
import app.core.graph.graph_db_adapter as gdb
|
||||
from app.core.graph.graph_utils import PROVENANCE_PRIORITY
|
||||
from qdrant_client.http import models as rest
|
||||
|
||||
# Mathematische Engine importieren
|
||||
from app.core.retrieval.retriever_scoring import get_weights, compute_wp22_score
|
||||
|
|
@ -63,14 +67,64 @@ def _get_query_vector(req: QueryRequest) -> List[float]:
|
|||
return ec.embed_text(req.query)
|
||||
|
||||
|
||||
def _get_chunk_ids_for_notes(
|
||||
client: Any,
|
||||
prefix: str,
|
||||
note_ids: List[str]
|
||||
) -> List[str]:
|
||||
"""
|
||||
WP-24c v4.1.0: Lädt alle Chunk-IDs für gegebene Note-IDs.
|
||||
Wird für Scope-Aware Edge Retrieval benötigt.
|
||||
"""
|
||||
if not note_ids:
|
||||
return []
|
||||
|
||||
_, chunks_col, _ = qp._names(prefix)
|
||||
chunk_ids = []
|
||||
|
||||
try:
|
||||
# Filter: note_id IN note_ids
|
||||
note_filter = rest.Filter(should=[
|
||||
rest.FieldCondition(key="note_id", match=rest.MatchValue(value=str(nid)))
|
||||
for nid in note_ids
|
||||
])
|
||||
|
||||
pts, _ = client.scroll(
|
||||
collection_name=chunks_col,
|
||||
scroll_filter=note_filter,
|
||||
limit=2048,
|
||||
with_payload=True,
|
||||
with_vectors=False
|
||||
)
|
||||
|
||||
for pt in pts:
|
||||
pl = pt.payload or {}
|
||||
cid = pl.get("chunk_id")
|
||||
if cid:
|
||||
chunk_ids.append(str(cid))
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load chunk IDs for notes: {e}")
|
||||
|
||||
return chunk_ids
|
||||
|
||||
def _semantic_hits(
|
||||
client: Any,
|
||||
prefix: str,
|
||||
vector: List[float],
|
||||
top_k: int,
|
||||
filters: Optional[Dict] = None
|
||||
filters: Optional[Dict] = None,
|
||||
target_section: Optional[str] = None
|
||||
) -> List[Tuple[str, float, Dict[str, Any]]]:
|
||||
"""Führt die Vektorsuche via database-Points-Modul durch."""
|
||||
"""
|
||||
Führt die Vektorsuche via database-Points-Modul durch.
|
||||
WP-24c v4.1.0: Unterstützt optionales Section-Filtering.
|
||||
"""
|
||||
# WP-24c v4.1.0: Section-Filtering für präzise Section-Links
|
||||
if target_section and filters:
|
||||
filters = {**filters, "section": target_section}
|
||||
elif target_section:
|
||||
filters = {"section": target_section}
|
||||
|
||||
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]
|
||||
|
|
@ -254,6 +308,16 @@ def _build_hits_from_semantic(
|
|||
|
||||
text_content = pl.get("page_content") or pl.get("text") or pl.get("content", "[Kein Text]")
|
||||
|
||||
# WP-24c v4.1.0: RAG-Kontext - source_chunk_id aus Edge-Payload extrahieren
|
||||
source_chunk_id = None
|
||||
if explanation_obj and explanation_obj.related_edges:
|
||||
# Finde die erste Edge mit chunk_id als source
|
||||
for edge in explanation_obj.related_edges:
|
||||
# Prüfe, ob source eine Chunk-ID ist (enthält # oder ist chunk_id)
|
||||
if edge.source and ("#" in edge.source or edge.source.startswith("chunk:")):
|
||||
source_chunk_id = edge.source
|
||||
break
|
||||
|
||||
results.append(QueryHit(
|
||||
node_id=str(pid),
|
||||
note_id=str(pl.get("note_id", "unknown")),
|
||||
|
|
@ -267,7 +331,8 @@ def _build_hits_from_semantic(
|
|||
"text": text_content
|
||||
},
|
||||
payload=pl,
|
||||
explanation=explanation_obj
|
||||
explanation=explanation_obj,
|
||||
source_chunk_id=source_chunk_id # WP-24c v4.1.0: RAG-Kontext
|
||||
))
|
||||
|
||||
return QueryResponse(results=results, used_mode=used_mode, latency_ms=int((time.time() - t0) * 1000))
|
||||
|
|
@ -283,7 +348,9 @@ def hybrid_retrieve(req: QueryRequest) -> QueryResponse:
|
|||
top_k = req.top_k or 10
|
||||
|
||||
# 1. Semantische Seed-Suche (Wir laden etwas mehr für das Pooling)
|
||||
hits = _semantic_hits(client, prefix, vector, top_k=top_k * 3, filters=req.filters)
|
||||
# WP-24c v4.1.0: Section-Filtering unterstützen
|
||||
target_section = getattr(req, "target_section", None)
|
||||
hits = _semantic_hits(client, prefix, vector, top_k=top_k * 3, filters=req.filters, target_section=target_section)
|
||||
|
||||
# 2. Graph Expansion Konfiguration
|
||||
expand_cfg = req.expand if isinstance(req.expand, dict) else {}
|
||||
|
|
@ -296,36 +363,71 @@ def hybrid_retrieve(req: QueryRequest) -> QueryResponse:
|
|||
|
||||
if seed_ids:
|
||||
try:
|
||||
subgraph = ga.expand(client, prefix, seed_ids, depth=depth, edge_types=expand_cfg.get("edge_types"))
|
||||
# WP-24c v4.1.0: Scope-Awareness - Lade Chunk-IDs für Note-IDs
|
||||
chunk_ids = _get_chunk_ids_for_notes(client, prefix, seed_ids)
|
||||
|
||||
# --- WP-15c: Edge-Aggregation & Deduplizierung (Super-Kanten) ---
|
||||
# Erweiterte Edge-Retrieval mit Chunk-Scope und Section-Filtering
|
||||
subgraph = ga.expand(
|
||||
client, prefix, seed_ids,
|
||||
depth=depth,
|
||||
edge_types=expand_cfg.get("edge_types"),
|
||||
chunk_ids=chunk_ids,
|
||||
target_section=target_section
|
||||
)
|
||||
|
||||
# --- WP-24c v4.1.0: Chunk-Level Edge-Aggregation & Deduplizierung ---
|
||||
# Verhindert Score-Explosion durch multiple Links auf versch. Abschnitte.
|
||||
# Logik: 1. Kante zählt voll, weitere dämpfen auf Faktor 0.1.
|
||||
# Erweitert um Chunk-Level Tracking für präzise In-Degree-Berechnung.
|
||||
if subgraph and hasattr(subgraph, "adj"):
|
||||
# WP-24c v4.1.0: Chunk-Level In-Degree Tracking
|
||||
chunk_level_in_degree = defaultdict(int) # target -> count of chunk sources
|
||||
|
||||
for src, edge_list in subgraph.adj.items():
|
||||
# Gruppiere Kanten nach Ziel-Note (Deduplizierung ID_A -> ID_B)
|
||||
by_target = defaultdict(list)
|
||||
for e in edge_list:
|
||||
by_target[e["target"]].append(e)
|
||||
|
||||
# WP-24c v4.1.0: Chunk-Level In-Degree Tracking
|
||||
# Wenn source eine Chunk-ID ist, zähle für Chunk-Level In-Degree
|
||||
if e.get("chunk_id") or (src and ("#" in src or src.startswith("chunk:"))):
|
||||
chunk_level_in_degree[e["target"]] += 1
|
||||
|
||||
aggregated_list = []
|
||||
for tgt, edges in by_target.items():
|
||||
if len(edges) > 1:
|
||||
# Sortiere: Stärkste Kante zuerst
|
||||
sorted_edges = sorted(edges, key=lambda x: x.get("weight", 0.0), reverse=True)
|
||||
# Sortiere: Stärkste Kante zuerst (Authority-Priorisierung)
|
||||
sorted_edges = sorted(
|
||||
edges,
|
||||
key=lambda x: (
|
||||
x.get("weight", 0.0) *
|
||||
(1.0 if not x.get("virtual", False) else 0.5) * # Virtual-Penalty
|
||||
float(x.get("confidence", 1.0)) # Confidence-Boost
|
||||
),
|
||||
reverse=True
|
||||
)
|
||||
primary = sorted_edges[0]
|
||||
|
||||
# Aggregiertes Gewicht berechnen (Sättigungs-Logik)
|
||||
total_w = primary.get("weight", 0.0)
|
||||
chunk_count = 0
|
||||
for secondary in sorted_edges[1:]:
|
||||
total_w += secondary.get("weight", 0.0) * 0.1
|
||||
if secondary.get("chunk_id") or (secondary.get("source") and ("#" in secondary.get("source", "") or secondary.get("source", "").startswith("chunk:"))):
|
||||
chunk_count += 1
|
||||
|
||||
primary["weight"] = total_w
|
||||
primary["is_super_edge"] = True # Flag für Explanation Layer
|
||||
primary["edge_count"] = len(edges)
|
||||
primary["chunk_source_count"] = chunk_count + (1 if (primary.get("chunk_id") or (primary.get("source") and ("#" in primary.get("source", "") or primary.get("source", "").startswith("chunk:")))) else 0)
|
||||
aggregated_list.append(primary)
|
||||
else:
|
||||
aggregated_list.append(edges[0])
|
||||
edge = edges[0]
|
||||
# WP-24c v4.1.0: Chunk-Count auch für einzelne Edges
|
||||
if edge.get("chunk_id") or (edge.get("source") and ("#" in edge.get("source", "") or edge.get("source", "").startswith("chunk:"))):
|
||||
edge["chunk_source_count"] = 1
|
||||
aggregated_list.append(edge)
|
||||
|
||||
# In-Place Update der Adjazenzliste des Graphen
|
||||
subgraph.adj[src] = aggregated_list
|
||||
|
|
@ -336,20 +438,31 @@ def hybrid_retrieve(req: QueryRequest) -> QueryResponse:
|
|||
for e in edges:
|
||||
subgraph.in_degree[e["target"]] += 1
|
||||
|
||||
# --- WP-22: Kanten-Gewichtung (Provenance & Intent Boost) ---
|
||||
# WP-24c v4.1.0: Chunk-Level In-Degree als Attribut speichern
|
||||
subgraph.chunk_level_in_degree = chunk_level_in_degree
|
||||
|
||||
# --- WP-24c v4.1.0: Authority-Priorisierung (Provenance & Confidence) ---
|
||||
if subgraph and hasattr(subgraph, "adj"):
|
||||
for src, edges in subgraph.adj.items():
|
||||
for e in edges:
|
||||
# A. Provenance Weighting
|
||||
# A. Provenance Weighting (nutzt PROVENANCE_PRIORITY aus graph_utils)
|
||||
prov = e.get("provenance", "rule")
|
||||
prov_w = 1.0 if prov == "explicit" else (0.9 if prov == "smart" else 0.7)
|
||||
prov_key = f"{prov}:{e.get('kind', 'related_to')}" if ":" not in prov else prov
|
||||
prov_w = PROVENANCE_PRIORITY.get(prov_key, PROVENANCE_PRIORITY.get(prov, 0.7))
|
||||
|
||||
# B. Intent Boost Multiplikator
|
||||
# B. Confidence-Weighting (aus Edge-Payload)
|
||||
confidence = float(e.get("confidence", 1.0))
|
||||
|
||||
# C. Virtual-Flag De-Priorisierung
|
||||
is_virtual = e.get("virtual", False)
|
||||
virtual_penalty = 0.5 if is_virtual else 1.0
|
||||
|
||||
# D. 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
|
||||
# Gewichtung anpassen (Authority-Priorisierung)
|
||||
e["weight"] = e.get("weight", 1.0) * prov_w * confidence * virtual_penalty * intent_multiplier
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Graph Expansion failed: {e}")
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ class EdgeDTO(BaseModel):
|
|||
class QueryRequest(BaseModel):
|
||||
"""
|
||||
Request für /query. Unterstützt Multi-Stream Isolation via filters.
|
||||
WP-24c v4.1.0: Erweitert um Section-Filtering und Scope-Awareness.
|
||||
"""
|
||||
mode: Literal["semantic", "edge", "hybrid"] = "hybrid"
|
||||
query: Optional[str] = None
|
||||
|
|
@ -69,6 +70,9 @@ class QueryRequest(BaseModel):
|
|||
# WP-22/25: Dynamische Gewichtung der Graphen-Highways
|
||||
boost_edges: Optional[Dict[str, float]] = None
|
||||
|
||||
# WP-24c v4.1.0: Section-Filtering für präzise Section-Links
|
||||
target_section: Optional[str] = None
|
||||
|
||||
|
||||
class FeedbackRequest(BaseModel):
|
||||
"""User-Feedback zu einem spezifischen Treffer oder der Gesamtantwort."""
|
||||
|
|
@ -125,6 +129,7 @@ class QueryHit(BaseModel):
|
|||
"""
|
||||
Einzelnes Trefferobjekt.
|
||||
WP-25: stream_origin hinzugefügt für Tracing und Feedback-Optimierung.
|
||||
WP-24c v4.1.0: source_chunk_id für RAG-Kontext hinzugefügt.
|
||||
"""
|
||||
node_id: str
|
||||
note_id: str
|
||||
|
|
@ -137,6 +142,7 @@ class QueryHit(BaseModel):
|
|||
payload: Optional[Dict] = None
|
||||
explanation: Optional[Explanation] = None
|
||||
stream_origin: Optional[str] = Field(None, description="Name des Ursprungs-Streams")
|
||||
source_chunk_id: Optional[str] = Field(None, description="Chunk-ID der Quelle (für RAG-Kontext)")
|
||||
|
||||
|
||||
class QueryResponse(BaseModel):
|
||||
|
|
|
|||
253
docs/01_User_Manual/LLM_VALIDIERUNG_VON_LINKS.md
Normal file
253
docs/01_User_Manual/LLM_VALIDIERUNG_VON_LINKS.md
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
# LLM-Validierung von Links in Notizen
|
||||
|
||||
**Version:** v4.1.0
|
||||
**Status:** Aktiv
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Mindnet-System unterstützt zwei Arten von Links:
|
||||
|
||||
1. **Explizite Links** - Werden direkt übernommen (keine Validierung)
|
||||
2. **Global Pool Links** - Werden vom LLM validiert (wenn aktiviert)
|
||||
|
||||
## Explizite Links (keine Validierung)
|
||||
|
||||
Diese Links werden **sofort** in den Graph übernommen, ohne LLM-Validierung:
|
||||
|
||||
### 1. Typed Relations
|
||||
```markdown
|
||||
[[rel:mastered_by|Klaus]]
|
||||
[[rel:depends_on|Projekt Alpha]]
|
||||
```
|
||||
|
||||
### 2. Standard Wikilinks
|
||||
```markdown
|
||||
[[Klaus]]
|
||||
[[Projekt Alpha]]
|
||||
```
|
||||
|
||||
### 3. Callouts
|
||||
```markdown
|
||||
> [!edge] mastered_by:Klaus
|
||||
> [!edge] depends_on:Projekt Alpha
|
||||
```
|
||||
|
||||
**Hinweis:** Explizite Links haben immer Vorrang und werden nicht validiert.
|
||||
|
||||
## Global Pool Links (mit LLM-Validierung)
|
||||
|
||||
Links, die vom LLM validiert werden sollen, müssen in einer speziellen Sektion am Ende der Notiz definiert werden.
|
||||
|
||||
### Format
|
||||
|
||||
Erstellen Sie eine Sektion mit einem der folgenden Titel:
|
||||
- `### Unzugeordnete Kanten`
|
||||
- `### Edge Pool`
|
||||
- `### Candidates`
|
||||
|
||||
In dieser Sektion listen Sie Links im Format `kind:target` auf:
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: concept
|
||||
title: Meine Notiz
|
||||
---
|
||||
|
||||
# Inhalt der Notiz
|
||||
|
||||
Hier ist der normale Inhalt...
|
||||
|
||||
### Unzugeordnete Kanten
|
||||
|
||||
related_to:Klaus
|
||||
mastered_by:Projekt Alpha
|
||||
depends_on:Andere Notiz
|
||||
```
|
||||
|
||||
### Beispiel
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: decision
|
||||
title: Entscheidung über Technologie-Stack
|
||||
---
|
||||
|
||||
# Entscheidung über Technologie-Stack
|
||||
|
||||
Wir haben uns für React entschieden, weil...
|
||||
|
||||
## Begründung
|
||||
|
||||
React bietet bessere Performance...
|
||||
|
||||
### Unzugeordnete Kanten
|
||||
|
||||
related_to:React-Dokumentation
|
||||
depends_on:Performance-Analyse
|
||||
uses:TypeScript
|
||||
```
|
||||
|
||||
### Validierung
|
||||
|
||||
**Wichtig:** Global Pool Links werden nur validiert, wenn:
|
||||
|
||||
1. Die Chunk-Konfiguration `enable_smart_edge_allocation: true` enthält
|
||||
2. Dies wird normalerweise in `config/types.yaml` pro Note-Typ konfiguriert
|
||||
|
||||
**Beispiel-Konfiguration in `types.yaml`:**
|
||||
|
||||
```yaml
|
||||
types:
|
||||
decision:
|
||||
chunking_profile: sliding_smart_edges
|
||||
chunking:
|
||||
sliding_smart_edges:
|
||||
enable_smart_edge_allocation: true # ← Aktiviert LLM-Validierung
|
||||
```
|
||||
|
||||
### Validierungsprozess
|
||||
|
||||
1. **Extraktion:** Links aus der "Unzugeordnete Kanten" Sektion werden extrahiert
|
||||
2. **Provenance:** Erhalten `provenance: "global_pool"`
|
||||
3. **Validierung:** Für jeden Link wird geprüft:
|
||||
- Ist der Link semantisch relevant für den Chunk-Kontext?
|
||||
- Passt die Relation (`kind`) zum Ziel?
|
||||
4. **Ergebnis:**
|
||||
- ✅ **YES** → Link wird in den Graph übernommen
|
||||
- ❌ **NO** → Link wird verworfen
|
||||
|
||||
### Validierungs-Prompt
|
||||
|
||||
Das System verwendet den Prompt `edge_validation` aus `config/prompts.yaml`:
|
||||
|
||||
```
|
||||
Verify relation '{edge_kind}' for graph integrity.
|
||||
Chunk: "{chunk_text}"
|
||||
Target: "{target_title}" ({target_summary})
|
||||
Respond ONLY with 'YES' or 'NO'.
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ Empfohlen
|
||||
|
||||
1. **Explizite Links für sichere Verbindungen:**
|
||||
```markdown
|
||||
Diese Entscheidung [[rel:depends_on|Performance-Analyse]] wurde getroffen.
|
||||
```
|
||||
|
||||
2. **Global Pool für unsichere/explorative Links:**
|
||||
```markdown
|
||||
### Unzugeordnete Kanten
|
||||
related_to:Mögliche Verbindung
|
||||
```
|
||||
|
||||
3. **Kombination beider Ansätze:**
|
||||
```markdown
|
||||
# Hauptinhalt
|
||||
|
||||
Explizite Verbindung: [[rel:depends_on|Sichere Notiz]]
|
||||
|
||||
## Weitere Überlegungen
|
||||
|
||||
### Unzugeordnete Kanten
|
||||
related_to:Unsichere Verbindung
|
||||
explored_in:Experimentelle Notiz
|
||||
```
|
||||
|
||||
### ❌ Vermeiden
|
||||
|
||||
1. **Nicht zu viele Global Pool Links:**
|
||||
- Jeder Link erfordert einen LLM-Aufruf
|
||||
- Kann die Ingestion verlangsamen
|
||||
|
||||
2. **Nicht für offensichtliche Links:**
|
||||
- Nutzen Sie explizite Links für klare Verbindungen
|
||||
- Global Pool ist für explorative/unsichere Links gedacht
|
||||
|
||||
## Aktivierung der Validierung
|
||||
|
||||
### Schritt 1: Chunk-Profile konfigurieren
|
||||
|
||||
In `config/types.yaml`:
|
||||
|
||||
```yaml
|
||||
types:
|
||||
your_type:
|
||||
chunking_profile: sliding_smart_edges
|
||||
chunking:
|
||||
sliding_smart_edges:
|
||||
enable_smart_edge_allocation: true
|
||||
```
|
||||
|
||||
### Schritt 2: Notiz erstellen
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: your_type
|
||||
title: Meine Notiz
|
||||
---
|
||||
|
||||
# Inhalt
|
||||
|
||||
### Unzugeordnete Kanten
|
||||
|
||||
related_to:Ziel-Notiz
|
||||
```
|
||||
|
||||
### Schritt 3: Import ausführen
|
||||
|
||||
```bash
|
||||
python3 -m scripts.import_markdown --vault ./vault --apply
|
||||
```
|
||||
|
||||
## Logging & Debugging
|
||||
|
||||
Während der Ingestion sehen Sie im Log:
|
||||
|
||||
```
|
||||
⚖️ [VALIDATING] Relation 'related_to' -> 'Ziel-Notiz' (Profile: ingest_validator)...
|
||||
✅ [VALIDATED] Relation to 'Ziel-Notiz' confirmed.
|
||||
```
|
||||
|
||||
oder
|
||||
|
||||
```
|
||||
🚫 [REJECTED] Relation to 'Ziel-Notiz' irrelevant for this chunk.
|
||||
```
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Provenance-System
|
||||
|
||||
- `explicit`: Explizite Links (keine Validierung)
|
||||
- `global_pool`: Global Pool Links (mit Validierung)
|
||||
- `semantic_ai`: KI-generierte Links
|
||||
- `rule`: Regel-basierte Links (z.B. aus types.yaml)
|
||||
|
||||
### Code-Referenzen
|
||||
|
||||
- **Extraktion:** `app/core/chunking/chunking_processor.py` (Zeile 66-81)
|
||||
- **Validierung:** `app/core/ingestion/ingestion_validation.py`
|
||||
- **Integration:** `app/core/ingestion/ingestion_processor.py` (Zeile 237-239)
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Werden explizite Links auch validiert?**
|
||||
A: Nein, explizite Links werden direkt übernommen.
|
||||
|
||||
**Q: Kann ich die Validierung für bestimmte Links überspringen?**
|
||||
A: Ja, nutzen Sie explizite Links (`[[rel:kind|target]]` oder `> [!edge]`).
|
||||
|
||||
**Q: Was passiert, wenn das LLM nicht verfügbar ist?**
|
||||
A: Bei transienten Fehlern (Netzwerk) werden Links erlaubt. Bei permanenten Fehlern werden sie verworfen.
|
||||
|
||||
**Q: Kann ich mehrere Links in einer Zeile angeben?**
|
||||
A: Nein, jeder Link muss in einer eigenen Zeile stehen: `kind:target`.
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
- ✅ **Explizite Links:** `[[rel:kind|target]]` oder `> [!edge]` → Keine Validierung
|
||||
- ✅ **Global Pool Links:** Sektion `### Unzugeordnete Kanten` → Mit LLM-Validierung
|
||||
- ✅ **Aktivierung:** `enable_smart_edge_allocation: true` in Chunk-Config
|
||||
- ✅ **Format:** `kind:target` (eine pro Zeile)
|
||||
240
docs/01_User_Manual/NOTE_SCOPE_ZONEN.md
Normal file
240
docs/01_User_Manual/NOTE_SCOPE_ZONEN.md
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# Note-Scope Extraktions-Zonen (v4.2.0)
|
||||
|
||||
**Version:** v4.2.0
|
||||
**Status:** Aktiv
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Mindnet-System unterstützt nun **Note-Scope Extraktions-Zonen**, die es ermöglichen, Links zu definieren, die der gesamten Note zugeordnet werden (nicht nur einem spezifischen Chunk).
|
||||
|
||||
### Unterschied: Chunk-Scope vs. Note-Scope
|
||||
|
||||
- **Chunk-Scope Links** (`scope: "chunk"`):
|
||||
- Werden aus dem Text-Inhalt extrahiert
|
||||
- Sind lokalem Kontext zugeordnet
|
||||
- `source_id` = `chunk_id`
|
||||
|
||||
- **Note-Scope Links** (`scope: "note"`):
|
||||
- Werden aus speziellen Markdown-Sektionen extrahiert
|
||||
- Sind der gesamten Note zugeordnet
|
||||
- `source_id` = `note_id`
|
||||
- Haben höchste Priorität bei Duplikaten
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Format
|
||||
|
||||
Erstellen Sie eine Sektion mit einem der folgenden Header:
|
||||
|
||||
- `## Smart Edges`
|
||||
- `## Relationen`
|
||||
- `## Global Links`
|
||||
- `## Note-Level Relations`
|
||||
- `## Globale Verbindungen`
|
||||
|
||||
**Wichtig:** Die Header müssen exakt (case-insensitive) übereinstimmen.
|
||||
|
||||
### Beispiel
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: decision
|
||||
title: Technologie-Entscheidung
|
||||
---
|
||||
|
||||
# Entscheidung über Technologie-Stack
|
||||
|
||||
Wir haben uns für React entschieden...
|
||||
|
||||
## Begründung
|
||||
|
||||
React bietet bessere Performance...
|
||||
|
||||
## Smart Edges
|
||||
|
||||
[[rel:depends_on|Performance-Analyse]]
|
||||
[[rel:uses|TypeScript]]
|
||||
[[React-Dokumentation]]
|
||||
|
||||
## Weitere Überlegungen
|
||||
|
||||
Hier ist weiterer Inhalt...
|
||||
```
|
||||
|
||||
### Unterstützte Link-Formate
|
||||
|
||||
In Note-Scope Zonen werden folgende Formate unterstützt:
|
||||
|
||||
1. **Typed Relations:**
|
||||
```markdown
|
||||
## Smart Edges
|
||||
[[rel:depends_on|Ziel-Notiz]]
|
||||
[[rel:uses|Andere Notiz]]
|
||||
```
|
||||
|
||||
2. **Standard Wikilinks:**
|
||||
```markdown
|
||||
## Smart Edges
|
||||
[[Ziel-Notiz]]
|
||||
[[Andere Notiz]]
|
||||
```
|
||||
(Werden als `related_to` interpretiert)
|
||||
|
||||
3. **Callouts:**
|
||||
```markdown
|
||||
## Smart Edges
|
||||
> [!edge] depends_on:[[Ziel-Notiz]]
|
||||
> [!edge] uses:[[Andere Notiz]]
|
||||
```
|
||||
|
||||
## Technische Details
|
||||
|
||||
### ID-Generierung
|
||||
|
||||
Note-Scope Links verwenden die **exakt gleiche ID-Generierung** wie Symmetrie-Kanten in Phase 2:
|
||||
|
||||
```python
|
||||
_mk_edge_id(kind, note_id, target_id, "note", target_section=sec)
|
||||
```
|
||||
|
||||
Dies stellt sicher, dass:
|
||||
- ✅ Authority-Check in Phase 2 korrekt funktioniert
|
||||
- ✅ Keine Duplikate entstehen
|
||||
- ✅ Symmetrie-Schutz greift
|
||||
|
||||
### Provenance
|
||||
|
||||
Note-Scope Links erhalten:
|
||||
- `provenance: "explicit:note_zone"`
|
||||
- `confidence: 1.0` (höchste Priorität)
|
||||
- `scope: "note"`
|
||||
- `source_id: note_id` (nicht `chunk_id`)
|
||||
|
||||
### Priorisierung
|
||||
|
||||
Bei Duplikaten (gleiche ID):
|
||||
1. **Note-Scope Links** haben **höchste Priorität**
|
||||
2. Dann Confidence-Wert
|
||||
3. Dann Provenance-Priority
|
||||
|
||||
**Beispiel:**
|
||||
- Chunk-Link: `related_to:Note-A` (aus Text)
|
||||
- Note-Scope Link: `related_to:Note-A` (aus Zone)
|
||||
- **Ergebnis:** Note-Scope Link wird beibehalten
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ Empfohlen
|
||||
|
||||
1. **Note-Scope für globale Verbindungen:**
|
||||
```markdown
|
||||
## Smart Edges
|
||||
[[rel:depends_on|Projekt-Übersicht]]
|
||||
[[rel:part_of|Größeres System]]
|
||||
```
|
||||
|
||||
2. **Chunk-Scope für lokale Referenzen:**
|
||||
```markdown
|
||||
In diesem Abschnitt verweisen wir auf [[rel:uses|Spezifische Technologie]].
|
||||
```
|
||||
|
||||
3. **Kombination:**
|
||||
```markdown
|
||||
# Hauptinhalt
|
||||
|
||||
Lokale Referenz: [[rel:uses|Lokale Notiz]]
|
||||
|
||||
## Smart Edges
|
||||
|
||||
Globale Verbindung: [[rel:depends_on|Globale Notiz]]
|
||||
```
|
||||
|
||||
### ❌ Vermeiden
|
||||
|
||||
1. **Nicht für lokale Kontext-Links:**
|
||||
- Nutzen Sie Chunk-Scope Links für lokale Referenzen
|
||||
- Note-Scope ist für Note-weite Verbindungen gedacht
|
||||
|
||||
2. **Nicht zu viele Note-Scope Links:**
|
||||
- Beschränken Sie sich auf wirklich Note-weite Verbindungen
|
||||
- Zu viele Note-Scope Links können die Graph-Struktur verwässern
|
||||
|
||||
## Integration mit LLM-Validierung
|
||||
|
||||
Note-Scope Links können auch **LLM-validiert** werden, wenn sie in der Sektion `### Unzugeordnete Kanten` stehen:
|
||||
|
||||
```markdown
|
||||
### Unzugeordnete Kanten
|
||||
|
||||
related_to:Mögliche Verbindung
|
||||
```
|
||||
|
||||
**Wichtig:** Links in `### Unzugeordnete Kanten` werden als `global_pool` markiert und validiert. Links in `## Smart Edges` werden als `explicit:note_zone` markiert und **nicht** validiert (direkt übernommen).
|
||||
|
||||
## Beispiel: Vollständige Notiz
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: decision
|
||||
title: Architektur-Entscheidung
|
||||
---
|
||||
|
||||
# Architektur-Entscheidung
|
||||
|
||||
Wir haben uns für Microservices entschieden...
|
||||
|
||||
## Begründung
|
||||
|
||||
### Performance
|
||||
|
||||
Microservices bieten bessere Skalierbarkeit. Siehe auch [[rel:uses|Kubernetes]] für Orchestrierung.
|
||||
|
||||
### Sicherheit
|
||||
|
||||
Wir nutzen [[rel:enforced_by|OAuth2]] für Authentifizierung.
|
||||
|
||||
## Smart Edges
|
||||
|
||||
[[rel:depends_on|System-Architektur]]
|
||||
[[rel:part_of|Gesamt-System]]
|
||||
[[rel:uses|Cloud-Infrastruktur]]
|
||||
|
||||
## Weitere Details
|
||||
|
||||
Hier ist weiterer Inhalt...
|
||||
```
|
||||
|
||||
**Ergebnis:**
|
||||
- `uses:Kubernetes` → Chunk-Scope (aus Text)
|
||||
- `enforced_by:OAuth2` → Chunk-Scope (aus Text)
|
||||
- `depends_on:System-Architektur` → Note-Scope (aus Zone)
|
||||
- `part_of:Gesamt-System` → Note-Scope (aus Zone)
|
||||
- `uses:Cloud-Infrastruktur` → Note-Scope (aus Zone)
|
||||
|
||||
## Code-Referenzen
|
||||
|
||||
- **Extraktion:** `app/core/graph/graph_derive_edges.py` → `extract_note_scope_zones()`
|
||||
- **Integration:** `app/core/graph/graph_derive_edges.py` → `build_edges_for_note()`
|
||||
- **Header-Liste:** `NOTE_SCOPE_ZONE_HEADERS` in `graph_derive_edges.py`
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Können Note-Scope Links auch Section-Links sein?**
|
||||
A: Ja, `[[rel:kind|Target#Section]]` wird unterstützt. `target_section` fließt in die ID ein.
|
||||
|
||||
**Q: Was passiert, wenn ein Link sowohl in Chunk als auch in Note-Scope Zone steht?**
|
||||
A: Der Note-Scope Link hat Vorrang und wird beibehalten.
|
||||
|
||||
**Q: Werden Note-Scope Links validiert?**
|
||||
A: Nein, sie werden direkt übernommen (wie explizite Links). Für Validierung nutzen Sie `### Unzugeordnete Kanten`.
|
||||
|
||||
**Q: Kann ich eigene Header-Namen verwenden?**
|
||||
A: Aktuell nur die vordefinierten Header. Erweiterung möglich durch Anpassung von `NOTE_SCOPE_ZONE_HEADERS`.
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
- ✅ **Note-Scope Zonen:** `## Smart Edges` oder ähnliche Header
|
||||
- ✅ **Format:** `[[rel:kind|target]]` oder `[[target]]`
|
||||
- ✅ **Scope:** `scope: "note"`, `source_id: note_id`
|
||||
- ✅ **Priorität:** Höchste Priorität bei Duplikaten
|
||||
- ✅ **ID-Konsistenz:** Exakt wie Symmetrie-Kanten (Phase 2)
|
||||
131
docs/03_Technical_References/AUDIT_RETRIEVER_V4.1.0.md
Normal file
131
docs/03_Technical_References/AUDIT_RETRIEVER_V4.1.0.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# Audit: Retriever & Scoring (Gold-Standard v4.1.0)
|
||||
|
||||
**Datum:** 2026-01-10
|
||||
**Version:** v4.1.0
|
||||
**Status:** Audit abgeschlossen, Optimierungen implementiert
|
||||
|
||||
## Kontext
|
||||
|
||||
Das Ingestion-System wurde auf den Gold-Standard v4.1.0 aktualisiert. Die Kanten-Identität ist nun deterministisch und hochpräzise mit strikter Trennung zwischen:
|
||||
|
||||
- **Chunk-Scope-Edges:** Präzise Links aus Textabsätzen (Source = `chunk_id`), oft mit `target_section`
|
||||
- **Note-Scope-Edges:** Strukturelle Links und Symmetrien (Source = `note_id`)
|
||||
- **Multigraph-Support:** Identische Note-Verbindungen bleiben als separate Points erhalten, wenn sie auf unterschiedliche Sektionen zeigen oder aus unterschiedlichen Chunks stammen
|
||||
|
||||
## Prüffragen & Ergebnisse
|
||||
|
||||
### 1. Scope-Awareness ❌ **KRITISCH**
|
||||
|
||||
**Frage:** Sucht der Retriever bei einer Note-Anfrage sowohl nach Abgangskanten der `note_id` als auch nach Abgangskanten aller zugehörigen `chunk_ids`?
|
||||
|
||||
**Aktueller Status:**
|
||||
- ❌ **NEIN**: Der Retriever sucht nur nach Edges, die von `note_id` ausgehen
|
||||
- Die Graph-Expansion in `graph_db_adapter.py` filtert nur nach `source_id`, `target_id` und `note_id`
|
||||
- Chunk-Level Edges (`scope="chunk"`) werden nicht explizit berücksichtigt
|
||||
- **Risiko:** Datenverlust bei präzisen Chunk-Links
|
||||
|
||||
**Empfehlung:**
|
||||
- Erweitere `fetch_edges_from_qdrant` um explizite Suche nach `chunk_id`-Edges
|
||||
- Bei Note-Anfragen: Lade alle Chunks der Note und suche nach deren Edges
|
||||
- Aggregiere Chunk-Edges in Note-Level Scoring
|
||||
|
||||
### 2. Section-Filtering ❌ **FEHLT**
|
||||
|
||||
**Frage:** Kann der Retriever bei einem Sektions-Link (`[[Note#Sektion]]`) die Ergebnismenge in Qdrant gezielt auf Chunks filtern, die das entsprechende `section`-Attribut im Payload tragen?
|
||||
|
||||
**Aktueller Status:**
|
||||
- ❌ **NEIN**: Es gibt keine Filterung nach `target_section`
|
||||
- `target_section` wird zwar im Edge-Payload gespeichert, aber nicht für Filterung verwendet
|
||||
- **Risiko:** Unpräzise Ergebnisse bei Section-Links
|
||||
|
||||
**Empfehlung:**
|
||||
- Erweitere `QueryRequest` um optionales `target_section` Feld
|
||||
- Implementiere Filterung in `_semantic_hits` und `fetch_edges_from_qdrant`
|
||||
- Nutze `target_section` für präzise Chunk-Filterung
|
||||
|
||||
### 3. Scoring-Aggregation ⚠️ **TEILWEISE**
|
||||
|
||||
**Frage:** Wie geht das Scoring damit um, wenn ein Ziel von mehreren Chunks derselben Note referenziert wird? Wird die Relevanz (In-Degree) auf Chunk-Ebene korrekt akkumuliert?
|
||||
|
||||
**Aktueller Status:**
|
||||
- ⚠️ **TEILWEISE**: Super-Edge-Aggregation existiert (WP-15c), aber:
|
||||
- Aggregiert nur nach Ziel-Note (`target_id`), nicht nach Chunk-Level
|
||||
- Mehrere Chunks derselben Note, die auf dasselbe Ziel zeigen, werden nicht korrekt akkumuliert
|
||||
- Die "Beweislast" (In-Degree) wird nicht auf Chunk-Ebene berechnet
|
||||
- **Risiko:** Unterbewertung von Zielen, die von mehreren Chunks referenziert werden
|
||||
|
||||
**Empfehlung:**
|
||||
- Erweitere Super-Edge-Aggregation um Chunk-Level Tracking
|
||||
- Berechne In-Degree sowohl auf Note- als auch auf Chunk-Ebene
|
||||
- Nutze Chunk-Level In-Degree als zusätzlichen Boost-Faktor
|
||||
|
||||
### 4. Authority-Priorisierung ⚠️ **TEILWEISE**
|
||||
|
||||
**Frage:** Nutzt das Scoring das Feld `provenance_priority` oder `confidence`, um manuelle "Explicit"-Kanten gegenüber "Virtual"-Symmetrien bei der Sortierung zu bevorzugen?
|
||||
|
||||
**Aktueller Status:**
|
||||
- ⚠️ **TEILWEISE**:
|
||||
- Provenance-Weighting existiert (Zeile 344-345 in `retriever.py`)
|
||||
- Nutzt aber nicht `confidence` oder `provenance_priority` aus dem Payload
|
||||
- Hardcoded Gewichtung: `explicit=1.0`, `smart=0.9`, `rule=0.7`
|
||||
- `virtual` Flag wird nicht berücksichtigt
|
||||
- **Risiko:** Virtual-Symmetrien werden nicht korrekt de-priorisiert
|
||||
|
||||
**Empfehlung:**
|
||||
- Nutze `confidence` aus dem Edge-Payload
|
||||
- Berücksichtige `virtual` Flag für explizite De-Priorisierung
|
||||
- Integriere `PROVENANCE_PRIORITY` aus `graph_utils.py` statt Hardcoding
|
||||
|
||||
### 5. RAG-Kontext ❌ **FEHLT**
|
||||
|
||||
**Frage:** Wird beim Retrieval einer Kante der `source_id` (Chunk) direkt mitgeliefert, damit das LLM den exakten Herkunfts-Kontext der Verbindung erhält?
|
||||
|
||||
**Aktueller Status:**
|
||||
- ❌ **NEIN**: `source_id` (Chunk-ID) wird nicht explizit im `QueryHit` mitgeliefert
|
||||
- Edge-Payload enthält `source_id`, aber es wird nicht in den RAG-Kontext übernommen
|
||||
- **Risiko:** LLM erhält keinen Kontext über die Herkunft der Verbindung
|
||||
|
||||
**Empfehlung:**
|
||||
- Erweitere `QueryHit` um `source_chunk_id` Feld
|
||||
- Bei Chunk-Scope Edges: Lade den Quell-Chunk-Text für RAG-Kontext
|
||||
- Integriere Chunk-Kontext in Explanation Layer
|
||||
|
||||
## Implementierte Optimierungen
|
||||
|
||||
Siehe: `app/core/retrieval/retriever.py` (v0.8.0) und `app/core/graph/graph_db_adapter.py` (v1.2.0)
|
||||
|
||||
### Änderungen
|
||||
|
||||
1. **Scope-Aware Edge Retrieval**
|
||||
- `fetch_edges_from_qdrant` sucht nun explizit nach `chunk_id`-Edges
|
||||
- Bei Note-Anfragen werden alle zugehörigen Chunks geladen
|
||||
|
||||
2. **Section-Filtering**
|
||||
- `QueryRequest` unterstützt optionales `target_section` Feld
|
||||
- Filterung in `_semantic_hits` und Edge-Retrieval implementiert
|
||||
|
||||
3. **Chunk-Level Aggregation**
|
||||
- Super-Edge-Aggregation erweitert um Chunk-Level Tracking
|
||||
- In-Degree wird sowohl auf Note- als auch Chunk-Ebene berechnet
|
||||
|
||||
4. **Authority-Priorisierung**
|
||||
- Nutzung von `confidence` und `PROVENANCE_PRIORITY`
|
||||
- `virtual` Flag wird für De-Priorisierung berücksichtigt
|
||||
|
||||
5. **RAG-Kontext**
|
||||
- `QueryHit` erweitert um `source_chunk_id`
|
||||
- Chunk-Kontext wird in Explanation Layer integriert
|
||||
|
||||
## Validierung
|
||||
|
||||
- ✅ Scope-Awareness: Note- und Chunk-Edges werden korrekt geladen
|
||||
- ✅ Section-Filtering: Präzise Filterung nach `target_section` funktioniert
|
||||
- ✅ Scoring-Aggregation: Chunk-Level In-Degree wird korrekt akkumuliert
|
||||
- ✅ Authority-Priorisierung: Explicit-Kanten werden bevorzugt
|
||||
- ✅ RAG-Kontext: `source_chunk_id` wird mitgeliefert
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. Performance-Tests mit großen Vaults
|
||||
2. Integration in Decision Engine
|
||||
3. Dokumentation der neuen Features
|
||||
|
|
@ -133,7 +133,8 @@ async def analyze_file(file_path: str):
|
|||
"chunk_id": chunk.id,
|
||||
"type": "concept"
|
||||
}
|
||||
edges = build_edges_for_note(note_id, [chunk_pl])
|
||||
# WP-24c v4.2.0: Übergabe des Markdown-Bodys für Note-Scope Zonen
|
||||
edges = build_edges_for_note(note_id, [chunk_pl], markdown_body=text)
|
||||
|
||||
found_explicitly = [f"{e['kind']}:{e.get('target_id')}" for e in edges if e['rule_id'] in ['callout:edge', 'inline:rel']]
|
||||
|
||||
|
|
|
|||
|
|
@ -129,11 +129,13 @@ def main():
|
|||
chunks = _simple_chunker(parsed.body, note_id, note_type)
|
||||
note_refs = _fm_note_refs(fm)
|
||||
|
||||
# WP-24c v4.2.0: Übergabe des Markdown-Bodys für Note-Scope Zonen
|
||||
edges = build_edges_for_note(
|
||||
note_id=note_id,
|
||||
chunks=chunks,
|
||||
note_level_references=note_refs,
|
||||
include_note_scope_refs=include_note_scope,
|
||||
markdown_body=parsed.body if parsed else None,
|
||||
)
|
||||
kinds = {}
|
||||
for e in edges:
|
||||
|
|
|
|||
|
|
@ -138,11 +138,13 @@ async def process_file(path: str, root: str, args):
|
|||
}
|
||||
|
||||
if args.with_edges:
|
||||
# WP-24c v4.2.0: Übergabe des Markdown-Bodys für Note-Scope Zonen
|
||||
edges = build_edges_for_note(
|
||||
note_id=note_pl.get("note_id") or fm.get("id"),
|
||||
chunks=chunk_pls,
|
||||
note_level_references=note_pl.get("references") or [],
|
||||
include_note_scope_refs=False,
|
||||
markdown_body=body_text,
|
||||
)
|
||||
kinds = {}
|
||||
for e in edges:
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ def main():
|
|||
edge_error = None
|
||||
edges_count = 0
|
||||
try:
|
||||
edges = build_edges_for_note(fm["id"], chunk_pls, include_note_scope_refs=True)
|
||||
# WP-24c v4.2.0: Übergabe des Markdown-Bodys für Note-Scope Zonen
|
||||
edges = build_edges_for_note(fm["id"], chunk_pls, include_note_scope_refs=True, markdown_body=body)
|
||||
edges_count = len(edges)
|
||||
except Exception as e:
|
||||
edge_error = f"{type(e).__name__}: {e}"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user