diff --git a/app/core/graph/graph_db_adapter.py b/app/core/graph/graph_db_adapter.py index ab98156..2f3ca8b 100644 --- a/app/core/graph/graph_db_adapter.py +++ b/app/core/graph/graph_db_adapter.py @@ -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 diff --git a/app/core/graph/graph_derive_edges.py b/app/core/graph/graph_derive_edges.py index ed05304..3e85658 100644 --- a/app/core/graph/graph_derive_edges.py +++ b/app/core/graph/graph_derive_edges.py @@ -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,19 +22,138 @@ 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. @@ -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()) \ No newline at end of file diff --git a/app/core/graph/graph_subgraph.py b/app/core/graph/graph_subgraph.py index 42add94..58e075a 100644 --- a/app/core/graph/graph_subgraph.py +++ b/app/core/graph/graph_subgraph.py @@ -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) diff --git a/app/core/graph/graph_utils.py b/app/core/graph/graph_utils.py index 29ad4f7..e3fabb0 100644 --- a/app/core/graph/graph_utils.py +++ b/app/core/graph/graph_utils.py @@ -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 } diff --git a/app/core/ingestion/ingestion_processor.py b/app/core/ingestion/ingestion_processor.py index f6b103e..fbc5a5d 100644 --- a/app/core/ingestion/ingestion_processor.py +++ b/app/core/ingestion/ingestion_processor.py @@ -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: diff --git a/app/core/retrieval/retriever.py b/app/core/retrieval/retriever.py index df48239..9423ba3 100644 --- a/app/core/retrieval/retriever.py +++ b/app/core/retrieval/retriever.py @@ -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 @@ -335,21 +437,32 @@ def hybrid_retrieve(req: QueryRequest) -> QueryResponse: for src, edges in subgraph.adj.items(): for e in edges: subgraph.in_degree[e["target"]] += 1 + + # WP-24c v4.1.0: Chunk-Level In-Degree als Attribut speichern + subgraph.chunk_level_in_degree = chunk_level_in_degree - # --- WP-22: Kanten-Gewichtung (Provenance & Intent Boost) --- + # --- 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}") diff --git a/app/models/dto.py b/app/models/dto.py index f0a1258..23221e1 100644 --- a/app/models/dto.py +++ b/app/models/dto.py @@ -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 @@ -67,7 +68,10 @@ class QueryRequest(BaseModel): explain: bool = False # WP-22/25: Dynamische Gewichtung der Graphen-Highways - boost_edges: Optional[Dict[str, float]] = None + 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): @@ -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): diff --git a/docs/01_User_Manual/LLM_VALIDIERUNG_VON_LINKS.md b/docs/01_User_Manual/LLM_VALIDIERUNG_VON_LINKS.md new file mode 100644 index 0000000..271d7b3 --- /dev/null +++ b/docs/01_User_Manual/LLM_VALIDIERUNG_VON_LINKS.md @@ -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) diff --git a/docs/01_User_Manual/NOTE_SCOPE_ZONEN.md b/docs/01_User_Manual/NOTE_SCOPE_ZONEN.md new file mode 100644 index 0000000..ba81c49 --- /dev/null +++ b/docs/01_User_Manual/NOTE_SCOPE_ZONEN.md @@ -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) diff --git a/docs/03_Technical_References/AUDIT_RETRIEVER_V4.1.0.md b/docs/03_Technical_References/AUDIT_RETRIEVER_V4.1.0.md new file mode 100644 index 0000000..74bf62d --- /dev/null +++ b/docs/03_Technical_References/AUDIT_RETRIEVER_V4.1.0.md @@ -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 diff --git a/scripts/debug_edge_loss.py b/scripts/debug_edge_loss.py index 02a22b2..532c6b3 100644 --- a/scripts/debug_edge_loss.py +++ b/scripts/debug_edge_loss.py @@ -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']] diff --git a/scripts/edges_dryrun.py b/scripts/edges_dryrun.py index d0623f5..2c7bcf0 100644 --- a/scripts/edges_dryrun.py +++ b/scripts/edges_dryrun.py @@ -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: diff --git a/scripts/payload_dryrun.py b/scripts/payload_dryrun.py index 066a195..80eb0e9 100644 --- a/scripts/payload_dryrun.py +++ b/scripts/payload_dryrun.py @@ -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: diff --git a/tests/inspect_one_note.py b/tests/inspect_one_note.py index ce61bdc..8325ed5 100644 --- a/tests/inspect_one_note.py +++ b/tests/inspect_one_note.py @@ -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}"