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