From dfff46e45cb2616d4ac34e45ea8ae46272ae63e9 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 10 Jan 2026 22:17:03 +0100 Subject: [PATCH] Update graph_derive_edges.py to version 4.2.1: Implement Clean-Context enhancements, including consolidated callout extraction and smart scope prioritization. Refactor callout handling to avoid duplicates and improve processing efficiency. Update documentation to reflect changes in edge extraction logic and prioritization strategy. --- app/core/graph/graph_derive_edges.py | 142 ++++++++-- .../AUDIT_CLEAN_CONTEXT_V4.2.0.md | 265 ++++++++++++++++++ 2 files changed, 387 insertions(+), 20 deletions(-) create mode 100644 docs/03_Technical_References/AUDIT_CLEAN_CONTEXT_V4.2.0.md diff --git a/app/core/graph/graph_derive_edges.py b/app/core/graph/graph_derive_edges.py index 8d52b63..ea68278 100644 --- a/app/core/graph/graph_derive_edges.py +++ b/app/core/graph/graph_derive_edges.py @@ -9,11 +9,15 @@ DESCRIPTION: Hauptlogik zur Kanten-Aggregation und De-Duplizierung. - 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) + WP-24c v4.2.1: Clean-Context Bereinigung + - Konsolidierte Callout-Extraktion (keine Duplikate) + - Smart Scope-Priorisierung (chunk bevorzugt, außer bei höherer Provenance) + - Effiziente Verarbeitung ohne redundante Scans +VERSION: 4.2.1 (WP-24c: Clean-Context Bereinigung) STATUS: Active """ import re -from typing import List, Optional, Dict, Tuple +from typing import List, Optional, Dict, Tuple, Set from .graph_utils import ( _get, _edge, _mk_edge_id, _dedupe_seq, parse_link_target, PROVENANCE_PRIORITY, load_types_registry, get_edge_defaults_for @@ -104,9 +108,7 @@ def extract_note_scope_zones(markdown_body: str) -> List[Tuple[str, str]]: 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) + # WP-24c v4.2.1: Callouts werden NICHT hier extrahiert, da sie global abgedeckt werden in_zone = False zone_content = [] @@ -122,8 +124,71 @@ def extract_note_scope_zones(markdown_body: str) -> List[Tuple[str, str]]: wikilinks = extract_wikilinks(zone_text) for wl in wikilinks: edges.append(("related_to", wl)) - callouts, _ = extract_callout_relations(zone_text) - edges.extend(callouts) + # WP-24c v4.2.1: Callouts werden NICHT hier extrahiert, da sie global abgedeckt werden + + return edges + +def extract_callouts_from_markdown( + markdown_body: str, + note_id: str, + existing_chunk_callouts: Optional[Set[Tuple[str, str, Optional[str]]]] = None +) -> List[dict]: + """ + WP-24c v4.2.1: Extrahiert Callouts aus dem Original-Markdown. + + Smart Logic: Nur Callouts, die NICHT in Chunks vorkommen (z.B. in Edge-Zonen), + werden mit scope: "note" angelegt. Callouts, die bereits in Chunks erfasst wurden, + werden übersprungen, um Duplikate zu vermeiden. + + Args: + markdown_body: Original-Markdown-Text (vor Chunking-Filterung) + note_id: ID der Note + existing_chunk_callouts: Set von (kind, target, section) Tupeln aus Chunks + + Returns: + List[dict]: Liste von Edge-Payloads mit scope: "note" + """ + if not markdown_body: + return [] + + if existing_chunk_callouts is None: + existing_chunk_callouts = set() + + edges: List[dict] = [] + + # Extrahiere alle Callouts aus dem gesamten Markdown + call_pairs, _ = extract_callout_relations(markdown_body) + + for k, raw_t in call_pairs: + t, sec = parse_link_target(raw_t, note_id) + if not t: + continue + + # WP-24c v4.2.1: Prüfe, ob dieser Callout bereits in einem Chunk vorkommt + callout_key = (k, t, sec) + if callout_key in existing_chunk_callouts: + # Callout ist bereits in Chunk erfasst -> überspringe (wird mit chunk-Scope angelegt) + continue + + # WP-24c v4.2.1: Callout ist NICHT in Chunks -> lege mit scope: "note" an + # (typischerweise in Edge-Zonen, die nicht gechunkt werden) + payload = { + "edge_id": _mk_edge_id(k, note_id, t, "note", target_section=sec), + "provenance": "explicit:callout", + "rule_id": "callout:edge", + "confidence": PROVENANCE_PRIORITY.get("callout:edge", 1.0) + } + if sec: + payload["target_section"] = sec + + edges.append(_edge( + kind=k, + scope="note", + source_id=note_id, + target_id=t, + note_id=note_id, + extra=payload + )) return edges @@ -151,7 +216,10 @@ def build_edges_for_note( # WP-24c v4.2.0: Note-Scope Zonen Extraktion (VOR Chunk-Verarbeitung) note_scope_edges: List[dict] = [] + if markdown_body: + # 1. Note-Scope Zonen (Wikilinks und Typed Relations) + # WP-24c v4.2.1: Callouts werden NICHT hier extrahiert, da sie separat behandelt werden zone_links = extract_note_scope_zones(markdown_body) for kind, raw_target in zone_links: target, sec = parse_link_target(raw_target, note_id) @@ -213,6 +281,9 @@ def build_edges_for_note( reg = load_types_registry() defaults = get_edge_defaults_for(note_type, reg) refs_all: List[str] = [] + + # WP-24c v4.2.1: Sammle alle Callout-Keys aus Chunks für Smart Logic + all_chunk_callout_keys: Set[Tuple[str, str, Optional[str]]] = set() for ch in chunks: cid = _get(ch, "chunk_id", "id") @@ -249,12 +320,15 @@ def build_edges_for_note( if sec: payload["target_section"] = sec edges.append(_edge(k, "chunk", cid, t, note_id, payload)) - # C. Callouts (> [!edge]) + # C. Callouts (> [!edge]) - WP-24c v4.2.1: Sammle für Smart Logic call_pairs, rem2 = extract_callout_relations(rem) for k, raw_t in call_pairs: t, sec = parse_link_target(raw_t, note_id) if not t: continue + # WP-24c v4.2.1: Tracke Callout für spätere Deduplizierung (global sammeln) + all_chunk_callout_keys.add((k, t, sec)) + # WP-24c v4.1.0: target_section fließt nun fest in die ID-Generierung ein payload = { "chunk_id": cid, @@ -312,11 +386,23 @@ def build_edges_for_note( })) # 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) WP-24c v4.2.1: Callout-Extraktion aus Markdown (NACH Chunk-Verarbeitung) + # Smart Logic: Nur Callouts, die NICHT in Chunks vorkommen, werden mit scope: "note" angelegt + callout_edges_from_markdown: List[dict] = [] + if markdown_body: + callout_edges_from_markdown = extract_callouts_from_markdown( + markdown_body, + note_id, + existing_chunk_callouts=all_chunk_callout_keys + ) + edges.extend(callout_edges_from_markdown) - # 5) De-Duplizierung (In-Place) mit Priorisierung - # WP-24c v4.2.0: Note-Scope Links haben Vorrang bei Duplikaten + # 6) De-Duplizierung (In-Place) mit Priorisierung + # WP-24c v4.2.1: Smart Scope-Priorisierung + # - chunk-Scope wird bevorzugt (präzisere Information für RAG) + # - note-Scope gewinnt nur bei höherer Provenance-Priorität (z.B. explicit:note_zone) # 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 und Provenance konsolidiert. @@ -324,18 +410,17 @@ def build_edges_for_note( for e in edges: eid = e["edge_id"] - # 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_scope = existing.get("scope", "chunk") + new_scope = e.get("scope", "chunk") existing_prov = existing.get("provenance", "") new_prov = e.get("provenance", "") - # Note-Scope Zone Links haben Vorrang + # WP-24c v4.2.1: Scope-Priorisierung + # 1. explicit:note_zone hat höchste Priorität (unabhängig von Scope) is_existing_note_zone = existing_prov == "explicit:note_zone" is_new_note_zone = new_prov == "explicit:note_zone" @@ -346,10 +431,27 @@ def build_edges_for_note( # 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: + # 2. chunk-Scope bevorzugen (präzisere Information) + if existing_scope == "chunk" and new_scope == "note": + # Bestehender chunk-Scope -> behalte + pass + elif existing_scope == "note" and new_scope == "chunk": + # Neuer chunk-Scope -> ersetze (präziser) unique_map[eid] = e + else: + # Gleicher Scope -> vergleiche Confidence und Provenance-Priority + existing_conf = existing.get("confidence", 0) + new_conf = e.get("confidence", 0) + + # Provenance-Priority berücksichtigen + existing_priority = PROVENANCE_PRIORITY.get(existing_prov, 0.7) + new_priority = PROVENANCE_PRIORITY.get(new_prov, 0.7) + + # Kombinierter Score: Confidence * Priority + existing_score = existing_conf * existing_priority + new_score = new_conf * new_priority + + if new_score > existing_score: + unique_map[eid] = e return list(unique_map.values()) \ No newline at end of file diff --git a/docs/03_Technical_References/AUDIT_CLEAN_CONTEXT_V4.2.0.md b/docs/03_Technical_References/AUDIT_CLEAN_CONTEXT_V4.2.0.md new file mode 100644 index 0000000..780fd87 --- /dev/null +++ b/docs/03_Technical_References/AUDIT_CLEAN_CONTEXT_V4.2.0.md @@ -0,0 +1,265 @@ +# Audit: Informations-Integrität (Clean-Context v4.2.0) + +**Datum:** 2026-01-10 +**Version:** v4.2.0 +**Status:** Audit abgeschlossen - **KRITISCHES PROBLEM IDENTIFIZIERT** + +## Kontext + +Das System wurde auf den Gold-Standard v4.2.0 optimiert. Ziel ist der "Clean-Context"-Ansatz: Strukturelle Metadaten (speziell `> [!edge]` Callouts und definierte Note-Scope Zonen) werden aus den Text-Chunks entfernt, um das semantische Rauschen im Vektor-Index zu reduzieren. Diese Informationen müssen stattdessen exklusiv über den Graphen (Feld `explanation` im `QueryHit`) an das LLM geliefert werden. + +## Audit-Ergebnisse + +### 1. Extraktion vor Filterung (Temporal Integrity) ⚠️ **TEILWEISE** + +#### ✅ Note-Scope Zonen: **FUNKTIONIERT** + +**Status:** ✅ **KORREKT** + +- `build_edges_for_note()` erhält `markdown_body` (Original-Markdown) als Parameter +- `extract_note_scope_zones()` analysiert den **unbearbeiteten** Markdown-Text +- Extraktion erfolgt **VOR** dem Chunking-Filter +- **Code-Referenz:** `app/core/graph/graph_derive_edges.py` Zeile 152-177 + +```python +# 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) # ← Original-Markdown +``` + +#### ❌ Callouts in Edge-Zonen: **KRITISCHES PROBLEM** + +**Status:** ❌ **FEHLT** + +**Problem:** +- `build_edges_for_note()` extrahiert Callouts aus **gefilterten Chunks** (Zeile 217-265) +- Chunks wurden bereits gefiltert (Edge-Zonen entfernt) in `chunking_processor.py` Zeile 38 +- **Callouts in Edge-Zonen werden NICHT extrahiert!** + +**Code-Referenz:** +```python +# app/core/graph/graph_derive_edges.py Zeile 217-265 +for ch in chunks: # ← chunks sind bereits gefiltert! + raw = _get(ch, "window") or _get(ch, "text") or "" + # ... + # C. Callouts (> [!edge]) + call_pairs, rem2 = extract_callout_relations(rem) # ← rem kommt aus gefilterten chunks +``` + +**Konsequenz:** +- Callouts in Edge-Zonen (z.B. `### Unzugeordnete Kanten` oder `## Smart Edges`) werden **nicht** in den Graph geschrieben +- **Informationsverlust:** Diese Kanten existieren nicht im Graph und können nicht über `explanation` an das LLM geliefert werden + +**Empfehlung:** +- Callouts müssen **auch** aus dem Original-Markdown (`markdown_body`) extrahiert werden +- Ähnlich wie `extract_note_scope_zones()` sollte eine Funktion `extract_callouts_from_markdown()` erstellt werden +- Diese sollte **vor** der Chunk-Verarbeitung aufgerufen werden + +### 2. Payload-Vollständigkeit (Explanation-Mapping) ✅ **FUNKTIONIERT** + +**Status:** ✅ **KORREKT** (wenn Edges im Graph sind) + +**Code-Referenz:** `app/core/retrieval/retriever.py` Zeile 188-238 + +**Verifizierung:** +- ✅ `_build_explanation()` sammelt alle Edges aus dem Subgraph (Zeile 189-215) +- ✅ Edges werden in `EdgeDTO`-Objekte konvertiert (Zeile 205-214) +- ✅ `related_edges` werden im `Explanation`-Objekt gespeichert (Zeile 236) +- ✅ Top 3 Edges werden als `Reason`-Objekte formuliert (Zeile 217-228) + +**Einschränkung:** +- Funktioniert nur, wenn Edges **im Graph sind** +- Da Callouts in Edge-Zonen nicht extrahiert werden (siehe Punkt 1), fehlen sie auch in der Explanation + +### 3. Prompt-Sichtbarkeit (RAG-Interface) ⚠️ **UNKLAR** + +**Status:** ⚠️ **TEILWEISE DOKUMENTIERT** + +**Code-Referenz:** `app/routers/chat.py` Zeile 178-274 + +**Verifizierung:** +- ✅ `explain=True` wird in `QueryRequest` gesetzt (Zeile 211 in `decision_engine.py`) +- ✅ `explanation` wird im `QueryHit` gespeichert (Zeile 334 in `retriever.py`) +- ⚠️ **Unklar:** Wie wird `explanation.related_edges` im LLM-Prompt verwendet? + +**Untersuchung:** +- `chat.py` verwendet `interview_template` Prompt (Zeile 212-222) +- Prompt-Variablen werden aus `QueryHit` extrahiert +- **Fehlend:** Explizite Verwendung von `explanation.related_edges` im Prompt + +**Empfehlung:** +- Prüfen Sie `config/prompts.yaml` für `interview_template` +- Stellen Sie sicher, dass `{related_edges}` oder ähnliche Variablen im Prompt verwendet werden +- Dokumentieren Sie die Prompt-Struktur für RAG-Kontext + +### 4. Edge-Case Analyse ⚠️ **KRITISCH** + +#### Szenario: Callout nur in Edge-Zone (kein Wikilink im Fließtext) + +**Status:** ❌ **INFORMATIONSVERLUST** + +**Beispiel:** +```markdown +--- +type: decision +title: Meine Notiz +--- + +# Hauptinhalt + +Dieser Text wird gechunkt. + +## Smart Edges + +> [!edge] depends_on +> [[Projekt Alpha]] + +## Weiterer Inhalt + +Mehr Text... +``` + +**Aktuelles Verhalten:** +1. ✅ `## Smart Edges` wird als Edge-Zone erkannt +2. ✅ Zone wird vom Chunking ausgeschlossen +3. ❌ **Callout wird NICHT extrahiert** (weil aus gefilterten Chunks extrahiert wird) +4. ❌ **Kante fehlt im Graph** +5. ❌ **Kante fehlt in Explanation** +6. ❌ **LLM erhält keine Information über diese Verbindung** + +**Konsequenz:** +- **Wissens-Vakuum:** Die Information existiert weder im Chunk-Text noch im Graph +- **Semantische Verbindung verloren:** Das LLM kann diese Verbindung nicht berücksichtigen + +## Zusammenfassung der Probleme + +### ❌ **KRITISCH: Callout-Extraktion aus Edge-Zonen fehlt** + +**Problem:** +- Callouts werden nur aus gefilterten Chunks extrahiert +- Callouts in Edge-Zonen werden nicht erfasst +- **Informationsverlust:** Diese Kanten fehlen im Graph + +**Lösung:** +1. Erstellen Sie `extract_callouts_from_markdown(markdown_body: str)` Funktion +2. Rufen Sie diese **vor** der Chunk-Verarbeitung auf +3. Integrieren Sie die extrahierten Callouts in `build_edges_for_note()` + +### ⚠️ **WARNUNG: Prompt-Integration unklar** + +**Problem:** +- Unklar, ob `explanation.related_edges` im LLM-Prompt verwendet werden +- Keine explizite Dokumentation der Prompt-Struktur + +**Empfehlung:** +- Prüfen Sie `config/prompts.yaml` für `interview_template` +- Dokumentieren Sie die Verwendung von `related_edges` im Prompt + +## Empfohlene Fixes + +### Fix 1: Callout-Extraktion aus Original-Markdown + +**Datei:** `app/core/graph/graph_derive_edges.py` + +**Änderung:** +```python +def extract_callouts_from_markdown(markdown_body: str, note_id: str) -> List[dict]: + """ + WP-24c v4.2.0: Extrahiert Callouts aus dem Original-Markdown. + Wird verwendet, um Callouts in Edge-Zonen zu erfassen, die nicht in Chunks sind. + """ + if not markdown_body: + return [] + + edges: List[dict] = [] + + # Extrahiere alle Callouts aus dem gesamten Markdown + call_pairs, _ = extract_callout_relations(markdown_body) + + for k, raw_t in call_pairs: + t, sec = parse_link_target(raw_t, note_id) + if not t: + continue + + # Bestimme scope: "note" wenn in Note-Scope Zone, sonst "chunk" + # (Für jetzt: scope="note" für alle Callouts aus Markdown) + payload = { + "edge_id": _mk_edge_id(k, note_id, t, "note", target_section=sec), + "provenance": "explicit:callout", + "rule_id": "callout:edge", + "confidence": PROVENANCE_PRIORITY.get("callout:edge", 1.0) + } + if sec: + payload["target_section"] = sec + + edges.append(_edge( + kind=k, + scope="note", + source_id=note_id, + target_id=t, + note_id=note_id, + payload=payload + )) + + 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]: + # ... existing code ... + + # WP-24c v4.2.0: Callout-Extraktion aus Original-Markdown (VOR Chunk-Verarbeitung) + if markdown_body: + callout_edges = extract_callouts_from_markdown(markdown_body, note_id) + edges.extend(callout_edges) + + # ... rest of function ... +``` + +### Fix 2: Prompt-Dokumentation + +**Datei:** `config/prompts.yaml` und Dokumentation + +**Empfehlung:** +- Prüfen Sie, ob `interview_template` `{related_edges}` verwendet +- Falls nicht: Erweitern Sie den Prompt um Graph-Kontext +- Dokumentieren Sie die Prompt-Struktur + +## Validierung nach Fix + +Nach Implementierung der Fixes sollte folgendes verifiziert werden: + +1. ✅ **Callouts in Edge-Zonen werden extrahiert** + - Test: Erstellen Sie eine Notiz mit Callout in `## Smart Edges` + - Verifizieren: Edge existiert in Qdrant `_edges` Collection + +2. ✅ **Edges erscheinen in Explanation** + - Test: Query mit `explain=True` + - Verifizieren: `explanation.related_edges` enthält die Callout-Edge + +3. ✅ **LLM erhält Graph-Kontext** + - Test: Chat-Query mit Edge-Information + - Verifizieren: LLM-Antwort berücksichtigt die Graph-Verbindung + +## Fazit + +**Aktueller Status:** ⚠️ **INFORMATIONSVERLUST BEI CALLOUTS IN EDGE-ZONEN** + +**Hauptproblem:** +- Callouts in Edge-Zonen werden nicht extrahiert +- Diese Information geht vollständig verloren + +**Lösung:** +- Implementierung von `extract_callouts_from_markdown()` erforderlich +- Integration in `build_edges_for_note()` vor Chunk-Verarbeitung + +**Nach Fix:** +- ✅ Alle Callouts werden erfasst (auch in Edge-Zonen) +- ✅ Graph-Vollständigkeit gewährleistet +- ✅ Explanation enthält alle relevanten Edges +- ✅ LLM erhält vollständigen Kontext