diff --git a/ANALYSE_TYPES_YAML_ZUGRIFFE.md b/ANALYSE_TYPES_YAML_ZUGRIFFE.md new file mode 100644 index 0000000..d6efc0a --- /dev/null +++ b/ANALYSE_TYPES_YAML_ZUGRIFFE.md @@ -0,0 +1,237 @@ +# Analyse: Zugriffe auf config/types.yaml + +## Zusammenfassung + +Diese Analyse prüft, welche Scripte auf `config/types.yaml` zugreifen und ob sie auf Elemente zugreifen, die in der aktuellen `types.yaml` nicht mehr vorhanden sind. + +**Datum:** 2025-01-XX +**Version types.yaml:** 2.7.0 + +--- + +## ❌ KRITISCHE PROBLEME + +### 1. `edge_defaults` fehlt in types.yaml, wird aber im Code verwendet + +**Status:** ⚠️ **PROBLEM** - Code sucht nach `edge_defaults` in types.yaml, aber dieses Feld existiert nicht mehr. + +**Betroffene Dateien:** + +#### a) `app/core/graph/graph_utils.py` (Zeilen 101-112) +```python +def get_edge_defaults_for(note_type: Optional[str], reg: dict) -> List[str]: + """Ermittelt Standard-Kanten für einen Typ.""" + types_map = reg.get("types", reg) if isinstance(reg, dict) else {} + if note_type and isinstance(types_map, dict): + t = types_map.get(note_type) + if isinstance(t, dict) and isinstance(t.get("edge_defaults"), list): # ❌ Sucht nach edge_defaults + return [str(x) for x in t["edge_defaults"] if isinstance(x, str)] + for key in ("defaults", "default", "global"): + v = reg.get(key) + if isinstance(v, dict) and isinstance(v.get("edge_defaults"), list): # ❌ Sucht nach edge_defaults + return [str(x) for x in v["edge_defaults"] if isinstance(x, str)] + return [] +``` +**Problem:** Funktion gibt immer `[]` zurück, da `edge_defaults` nicht in types.yaml existiert. + +#### b) `app/core/graph/graph_derive_edges.py` (Zeile 64) +```python +defaults = get_edge_defaults_for(note_type, reg) # ❌ Wird verwendet, liefert aber [] +``` +**Problem:** Keine automatischen Default-Kanten werden mehr erzeugt. + +#### c) `app/services/discovery.py` (Zeile 212) +```python +defaults = type_def.get("edge_defaults") # ❌ Sucht nach edge_defaults +return defaults[0] if defaults else "related_to" +``` +**Problem:** Fallback funktioniert, aber nutzt nicht die neue dynamische Lösung. + +#### d) `tests/check_types_registry_edges.py` (Zeile 170) +```python +eddefs = (tdef or {}).get("edge_defaults") or [] # ❌ Sucht nach edge_defaults +``` +**Problem:** Test findet keine `edge_defaults` mehr und gibt Warnung aus. + +**✅ Lösung bereits implementiert:** +- `app/core/ingestion/ingestion_note_payload.py` (WP-24c, Zeilen 124-134) nutzt bereits die neue dynamische Lösung über `edge_registry.get_topology_info()`. + +**Empfehlung:** +- `get_edge_defaults_for()` in `graph_utils.py` sollte auf die EdgeRegistry umgestellt werden. +- `discovery.py` sollte ebenfalls die EdgeRegistry nutzen. + +--- + +### 2. Inkonsistenz: `chunk_profile` vs `chunking_profile` + +**Status:** ⚠️ **WARNUNG** - Meistens abgefangen durch Fallback-Logik. + +**Problem:** +- In `types.yaml` heißt es: `chunking_profile` ✅ +- `app/core/type_registry.py` (Zeile 88) sucht nach: `chunk_profile` ❌ + +```python +def effective_chunk_profile(note_type: Optional[str], reg: Dict[str, Any]) -> Optional[str]: + cfg = get_type_config(note_type, reg) + prof = cfg.get("chunk_profile") # ❌ Sucht nach "chunk_profile", aber types.yaml hat "chunking_profile" + if isinstance(prof, str) and prof.strip(): + return prof.strip().lower() + return None +``` + +**Betroffene Dateien:** +- `app/core/type_registry.py` (Zeile 88) - verwendet `chunk_profile` statt `chunking_profile` + +**✅ Gut gehandhabt:** +- `app/core/ingestion/ingestion_chunk_payload.py` (Zeile 33) - hat Fallback: `t_cfg.get(key) or t_cfg.get(key.replace("ing", ""))` +- `app/core/ingestion/ingestion_note_payload.py` (Zeile 120) - prüft beide Varianten + +**Empfehlung:** +- `type_registry.py` sollte auch `chunking_profile` prüfen (oder beide Varianten). + +--- + +## ✅ KORREKT VERWENDETE ELEMENTE + +### 1. `chunking_profiles` ✅ +- **Verwendet in:** + - `app/core/chunking/chunking_utils.py` (Zeile 33) ✅ +- **Status:** Korrekt vorhanden in types.yaml + +### 2. `defaults` ✅ +- **Verwendet in:** + - `app/core/ingestion/ingestion_chunk_payload.py` (Zeile 36) ✅ + - `app/core/ingestion/ingestion_note_payload.py` (Zeile 104) ✅ + - `app/core/chunking/chunking_utils.py` (Zeile 35) ✅ +- **Status:** Korrekt vorhanden in types.yaml + +### 3. `ingestion_settings` ✅ +- **Verwendet in:** + - `app/core/ingestion/ingestion_note_payload.py` (Zeile 105) ✅ +- **Status:** Korrekt vorhanden in types.yaml + +### 4. `llm_settings` ✅ +- **Verwendet in:** + - `app/core/registry.py` (Zeile 37) ✅ +- **Status:** Korrekt vorhanden in types.yaml + +### 5. `types` (Hauptstruktur) ✅ +- **Verwendet in:** Viele Dateien +- **Status:** Korrekt vorhanden in types.yaml + +### 6. `types[].chunking_profile` ✅ +- **Verwendet in:** + - `app/core/chunking/chunking_utils.py` (Zeile 35) ✅ + - `app/core/ingestion/ingestion_chunk_payload.py` (Zeile 67) ✅ + - `app/core/ingestion/ingestion_note_payload.py` (Zeile 120) ✅ +- **Status:** Korrekt vorhanden in types.yaml + +### 7. `types[].retriever_weight` ✅ +- **Verwendet in:** + - `app/core/ingestion/ingestion_chunk_payload.py` (Zeile 71) ✅ + - `app/core/ingestion/ingestion_note_payload.py` (Zeile 111) ✅ + - `app/core/retrieval/retriever_scoring.py` (Zeile 87) ✅ +- **Status:** Korrekt vorhanden in types.yaml + +### 8. `types[].detection_keywords` ✅ +- **Verwendet in:** + - `app/routers/chat.py` (Zeilen 104, 150) ✅ +- **Status:** Korrekt vorhanden in types.yaml + +### 9. `types[].schema` ✅ +- **Verwendet in:** + - `app/routers/chat.py` (vermutlich) ✅ +- **Status:** Korrekt vorhanden in types.yaml + +--- + +## 📋 ZUSAMMENFASSUNG DER ZUGRIFFE + +### Dateien, die auf types.yaml zugreifen: + +1. **app/core/type_registry.py** ⚠️ + - Verwendet: `types`, `chunk_profile` (sollte `chunking_profile` sein) + - Problem: Sucht nach `chunk_profile` statt `chunking_profile` + +2. **app/core/registry.py** ✅ + - Verwendet: `llm_settings.cleanup_patterns` + - Status: OK + +3. **app/core/ingestion/ingestion_chunk_payload.py** ✅ + - Verwendet: `types`, `defaults`, `chunking_profile`, `retriever_weight` + - Status: OK (hat Fallback für chunk_profile/chunking_profile) + +4. **app/core/ingestion/ingestion_note_payload.py** ✅ + - Verwendet: `types`, `defaults`, `ingestion_settings`, `chunking_profile`, `retriever_weight` + - Status: OK (nutzt neue EdgeRegistry für edge_defaults) + +5. **app/core/chunking/chunking_utils.py** ✅ + - Verwendet: `chunking_profiles`, `types`, `defaults.chunking_profile` + - Status: OK + +6. **app/core/retrieval/retriever_scoring.py** ✅ + - Verwendet: `retriever_weight` (aus Payload, kommt ursprünglich aus types.yaml) + - Status: OK + +7. **app/core/graph/graph_utils.py** ❌ + - Verwendet: `types[].edge_defaults` (existiert nicht mehr!) + - Problem: Sucht nach `edge_defaults` in types.yaml + +8. **app/core/graph/graph_derive_edges.py** ❌ + - Verwendet: `get_edge_defaults_for()` → sucht nach `edge_defaults` + - Problem: Keine Default-Kanten mehr + +9. **app/services/discovery.py** ⚠️ + - Verwendet: `types[].edge_defaults` (existiert nicht mehr!) + - Problem: Fallback funktioniert, aber nutzt nicht neue Lösung + +10. **app/routers/chat.py** ✅ + - Verwendet: `types[].detection_keywords` + - Status: OK + +11. **tests/test_type_registry.py** ⚠️ + - Verwendet: `types[].chunk_profile`, `types[].edge_defaults` + - Problem: Test verwendet alte Struktur + +12. **tests/check_types_registry_edges.py** ❌ + - Verwendet: `types[].edge_defaults` (existiert nicht mehr!) + - Problem: Test findet keine edge_defaults + +13. **scripts/payload_dryrun.py** ✅ + - Verwendet: Indirekt über `make_note_payload()` und `make_chunk_payloads()` + - Status: OK + +--- + +## 🔧 EMPFOHLENE FIXES + +### Priorität 1 (Kritisch): + +1. **`app/core/graph/graph_utils.py` - `get_edge_defaults_for()`** + - Sollte auf `edge_registry.get_topology_info()` umgestellt werden + - Oder: Rückwärtskompatibilität beibehalten, aber EdgeRegistry als primäre Quelle nutzen + +2. **`app/core/graph/graph_derive_edges.py`** + - Nutzt `get_edge_defaults_for()`, sollte nach Fix von graph_utils.py funktionieren + +3. **`app/services/discovery.py`** + - Sollte EdgeRegistry für `edge_defaults` nutzen + +### Priorität 2 (Warnung): + +4. **`app/core/type_registry.py` - `effective_chunk_profile()`** + - Sollte auch `chunking_profile` prüfen (nicht nur `chunk_profile`) + +5. **`tests/test_type_registry.py`** + - Test sollte aktualisiert werden, um `chunking_profile` statt `chunk_profile` zu verwenden + +6. **`tests/check_types_registry_edges.py`** + - Test sollte auf EdgeRegistry umgestellt werden oder als deprecated markiert werden + +--- + +## 📝 HINWEISE + +- **WP-24c** hat bereits eine Lösung für `edge_defaults` implementiert: Dynamische Abfrage über `edge_registry.get_topology_info()` +- Die alte Lösung (statische `edge_defaults` in types.yaml) wurde durch die dynamische Lösung ersetzt +- Code-Stellen, die noch die alte Lösung verwenden, sollten migriert werden diff --git a/app/core/chunking/chunking_models.py b/app/core/chunking/chunking_models.py index d64c4e7..1cf3fd0 100644 --- a/app/core/chunking/chunking_models.py +++ b/app/core/chunking/chunking_models.py @@ -13,6 +13,8 @@ class RawBlock: level: Optional[int] section_path: str section_title: Optional[str] + exclude_from_chunking: bool = False # WP-24c v4.2.0: Flag für Edge-Zonen, die nicht gechunkt werden sollen + is_meta_content: bool = False # WP-24c v4.2.6: Flag für Meta-Content (Callouts), der später entfernt wird @dataclass class Chunk: diff --git a/app/core/chunking/chunking_parser.py b/app/core/chunking/chunking_parser.py index e36ff0e..a26eefd 100644 --- a/app/core/chunking/chunking_parser.py +++ b/app/core/chunking/chunking_parser.py @@ -3,9 +3,12 @@ FILE: app/core/chunking/chunking_parser.py DESCRIPTION: Zerlegt Markdown in logische Einheiten (RawBlocks). Hält alle Überschriftenebenen (H1-H6) im Stream. Stellt die Funktion parse_edges_robust zur Verfügung. + WP-24c v4.2.0: Identifiziert Edge-Zonen und markiert sie für Chunking-Ausschluss. + WP-24c v4.2.5: Callout-Exclusion - Callouts werden als separate RawBlocks identifiziert und ausgeschlossen. """ import re -from typing import List, Tuple, Set +import os +from typing import List, Tuple, Set, Dict, Any, Optional from .chunking_models import RawBlock from .chunking_utils import extract_frontmatter_from_text @@ -20,7 +23,11 @@ def split_sentences(text: str) -> list[str]: return [p.strip() for p in _SENT_SPLIT.split(text) if p.strip()] def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]: - """Zerlegt Text in logische Einheiten (RawBlocks), inklusive H1-H6.""" + """ + Zerlegt Text in logische Einheiten (RawBlocks), inklusive H1-H6. + WP-24c v4.2.0: Identifiziert Edge-Zonen (LLM-Validierung & Note-Scope) und markiert sie für Chunking-Ausschluss. + WP-24c v4.2.6: Callouts werden mit is_meta_content=True markiert (werden gechunkt, aber später entfernt). + """ blocks = [] h1_title = "Dokument" section_path = "/" @@ -29,6 +36,31 @@ def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]: # Frontmatter entfernen fm, text_without_fm = extract_frontmatter_from_text(md_text) + # WP-24c v4.2.0: Konfigurierbare Header-Namen und -Ebenen + llm_validation_headers = os.getenv( + "MINDNET_LLM_VALIDATION_HEADERS", + "Unzugeordnete Kanten,Edge Pool,Candidates" + ) + llm_validation_header_list = [h.strip() for h in llm_validation_headers.split(",") if h.strip()] + if not llm_validation_header_list: + llm_validation_header_list = ["Unzugeordnete Kanten", "Edge Pool", "Candidates"] + + note_scope_headers = os.getenv( + "MINDNET_NOTE_SCOPE_ZONE_HEADERS", + "Smart Edges,Relationen,Global Links,Note-Level Relations,Globale Verbindungen" + ) + note_scope_header_list = [h.strip() for h in note_scope_headers.split(",") if h.strip()] + if not note_scope_header_list: + note_scope_header_list = ["Smart Edges", "Relationen", "Global Links", "Note-Level Relations", "Globale Verbindungen"] + + # Header-Ebenen konfigurierbar (Default: LLM=3, Note-Scope=2) + llm_validation_level = int(os.getenv("MINDNET_LLM_VALIDATION_HEADER_LEVEL", "3")) + note_scope_level = int(os.getenv("MINDNET_NOTE_SCOPE_HEADER_LEVEL", "2")) + + # Status-Tracking für Edge-Zonen + in_exclusion_zone = False + exclusion_zone_type = None # "llm_validation" oder "note_scope" + # H1 für Note-Titel extrahieren (Metadaten-Zweck) h1_match = re.search(r'^#\s+(.*)', text_without_fm, re.MULTILINE) if h1_match: @@ -37,9 +69,61 @@ def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]: lines = text_without_fm.split('\n') buffer = [] - for line in lines: + # WP-24c v4.2.5: Callout-Erkennung (auch verschachtelt: >>) + # Regex für Callouts: >\s*[!edge] oder >\s*[!abstract] (auch mit mehreren >) + callout_pattern = re.compile(r'^\s*>{1,}\s*\[!(edge|abstract)\]', re.IGNORECASE) + + # WP-24c v4.2.5: Markiere verarbeitete Zeilen, um sie zu überspringen + processed_indices = set() + + for i, line in enumerate(lines): + if i in processed_indices: + continue + stripped = line.strip() + # WP-24c v4.2.5: Callout-Erkennung (VOR Heading-Erkennung) + # Prüfe, ob diese Zeile ein Callout startet + callout_match = callout_pattern.match(line) + if callout_match: + # Vorherigen Text-Block abschließen + if buffer: + content = "\n".join(buffer).strip() + if content: + blocks.append(RawBlock( + "paragraph", content, None, section_path, current_section_title, + exclude_from_chunking=in_exclusion_zone + )) + buffer = [] + + # Sammle alle Zeilen des Callout-Blocks + callout_lines = [line] + leading_gt_count = len(line) - len(line.lstrip('>')) + processed_indices.add(i) + + # Sammle alle Zeilen, die zum Callout gehören (gleiche oder höhere Einrückung) + j = i + 1 + while j < len(lines): + next_line = lines[j] + if not next_line.strip().startswith('>'): + break + next_leading_gt = len(next_line) - len(next_line.lstrip('>')) + if next_leading_gt < leading_gt_count: + break + callout_lines.append(next_line) + processed_indices.add(j) + j += 1 + + # WP-24c v4.2.6: Erstelle Callout-Block mit is_meta_content = True + # Callouts werden gechunkt (für Chunk-Attribution), aber später entfernt (Clean-Context) + callout_content = "\n".join(callout_lines) + blocks.append(RawBlock( + "callout", callout_content, None, section_path, current_section_title, + exclude_from_chunking=in_exclusion_zone, # Nur Edge-Zonen werden ausgeschlossen + is_meta_content=True # WP-24c v4.2.6: Markierung für spätere Entfernung + )) + continue + # Heading-Erkennung (H1 bis H6) heading_match = re.match(r'^(#{1,6})\s+(.*)', stripped) if heading_match: @@ -47,20 +131,47 @@ def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]: if buffer: content = "\n".join(buffer).strip() if content: - blocks.append(RawBlock("paragraph", content, None, section_path, current_section_title)) + blocks.append(RawBlock( + "paragraph", content, None, section_path, current_section_title, + exclude_from_chunking=in_exclusion_zone + )) buffer = [] level = len(heading_match.group(1)) title = heading_match.group(2).strip() + # WP-24c v4.2.0: Prüfe, ob dieser Header eine Edge-Zone startet + is_llm_validation_zone = ( + level == llm_validation_level and + any(title.lower() == h.lower() for h in llm_validation_header_list) + ) + is_note_scope_zone = ( + level == note_scope_level and + any(title.lower() == h.lower() for h in note_scope_header_list) + ) + + if is_llm_validation_zone: + in_exclusion_zone = True + exclusion_zone_type = "llm_validation" + elif is_note_scope_zone: + in_exclusion_zone = True + exclusion_zone_type = "note_scope" + elif in_exclusion_zone: + # Neuer Header gefunden, der keine Edge-Zone ist -> Zone beendet + in_exclusion_zone = False + exclusion_zone_type = None + # Pfad- und Titel-Update für die Metadaten der folgenden Blöcke if level == 1: current_section_title = title; section_path = "/" elif level == 2: current_section_title = title; section_path = f"/{current_section_title}" - # Die Überschrift selbst als regulären Block hinzufügen - blocks.append(RawBlock("heading", stripped, level, section_path, current_section_title)) + # Die Überschrift selbst als regulären Block hinzufügen (auch markiert, wenn in Zone) + blocks.append(RawBlock( + "heading", stripped, level, section_path, current_section_title, + exclude_from_chunking=in_exclusion_zone + )) continue # Trenner (---) oder Leerzeilen beenden Blöcke, außer innerhalb von Callouts @@ -68,48 +179,73 @@ def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]: if buffer: content = "\n".join(buffer).strip() if content: - blocks.append(RawBlock("paragraph", content, None, section_path, current_section_title)) + blocks.append(RawBlock( + "paragraph", content, None, section_path, current_section_title, + exclude_from_chunking=in_exclusion_zone + )) buffer = [] if stripped == "---": - blocks.append(RawBlock("separator", "---", None, section_path, current_section_title)) + blocks.append(RawBlock( + "separator", "---", None, section_path, current_section_title, + exclude_from_chunking=in_exclusion_zone + )) else: buffer.append(line) if buffer: content = "\n".join(buffer).strip() if content: - blocks.append(RawBlock("paragraph", content, None, section_path, current_section_title)) + blocks.append(RawBlock( + "paragraph", content, None, section_path, current_section_title, + exclude_from_chunking=in_exclusion_zone + )) return blocks, h1_title -def parse_edges_robust(text: str) -> Set[str]: - """Extrahiert Kanten-Kandidaten aus Wikilinks und Callouts.""" - found_edges = set() +def parse_edges_robust(text: str) -> List[Dict[str, Any]]: + """ + Extrahiert Kanten-Kandidaten aus Wikilinks und Callouts. + WP-24c v4.2.7: Gibt Liste von Dicts zurück mit is_callout Flag für Chunk-Attribution. + WP-24c v4.2.9 Fix A: current_edge_type bleibt über Leerzeilen hinweg erhalten, + damit alle Links in einem Callout-Block korrekt verarbeitet werden. + + Returns: + List[Dict] mit keys: "edge" (str: "kind:target"), "is_callout" (bool) + """ + found_edges: List[Dict[str, any]] = [] # 1. Wikilinks [[rel:kind|target]] inlines = re.findall(r'\[\[rel:([^\|\]]+)\|?([^\]]*)\]\]', text) for kind, target in inlines: k = kind.strip().lower() t = target.strip() - if k and t: found_edges.add(f"{k}:{t}") + if k and t: + found_edges.append({"edge": f"{k}:{t}", "is_callout": False}) # 2. Callout Edges > [!edge] kind lines = text.split('\n') current_edge_type = None for line in lines: stripped = line.strip() - callout_match = re.match(r'>\s*\[!edge\]\s*([^:\s]+)', stripped) + callout_match = re.match(r'>+\s*\[!edge\]\s*([^:\s]+)', stripped) if callout_match: current_edge_type = callout_match.group(1).strip().lower() # Links in der gleichen Zeile des Callouts links = re.findall(r'\[\[([^\]]+)\]\]', stripped) for l in links: - if "rel:" not in l: found_edges.add(f"{current_edge_type}:{l}") + if "rel:" not in l: + found_edges.append({"edge": f"{current_edge_type}:{l}", "is_callout": True}) continue # Links in Folgezeilen des Callouts + # WP-24c v4.2.9 Fix A: current_edge_type bleibt über Leerzeilen hinweg erhalten + # innerhalb eines Callout-Blocks, damit alle Links korrekt verarbeitet werden if current_edge_type and stripped.startswith('>'): + # Fortsetzung des Callout-Blocks: Links extrahieren links = re.findall(r'\[\[([^\]]+)\]\]', stripped) for l in links: - if "rel:" not in l: found_edges.add(f"{current_edge_type}:{l}") - elif not stripped.startswith('>'): + if "rel:" not in l: + found_edges.append({"edge": f"{current_edge_type}:{l}", "is_callout": True}) + elif current_edge_type and not stripped.startswith('>') and stripped: + # Nicht-Callout-Zeile mit Inhalt: Callout-Block beendet current_edge_type = None + # Leerzeilen werden ignoriert - current_edge_type bleibt erhalten return found_edges \ No newline at end of file diff --git a/app/core/chunking/chunking_processor.py b/app/core/chunking/chunking_processor.py index 26c2b68..af0afb8 100644 --- a/app/core/chunking/chunking_processor.py +++ b/app/core/chunking/chunking_processor.py @@ -6,9 +6,21 @@ DESCRIPTION: Der zentrale Orchestrator für das Chunking-System. - Integriert physikalische Kanten-Injektion (Propagierung). - Stellt H1-Kontext-Fenster sicher. - Baut den Candidate-Pool für die WP-15b Ingestion auf. + WP-24c v4.2.0: Konfigurierbare Header-Namen für LLM-Validierung. + WP-24c v4.2.5: Wiederherstellung der Chunking-Präzision + - Frontmatter-Override für chunking_profile + - Callout-Exclusion aus Chunks + - Strict-Mode ohne Carry-Over + WP-24c v4.2.6: Finale Härtung - "Semantic First, Clean Second" + - Callouts werden gechunkt (Chunk-Attribution), aber später entfernt (Clean-Context) + - remove_callouts_from_text erst nach propagate_section_edges und Candidate Pool + WP-24c v4.2.7: Wiederherstellung der Chunk-Attribution + - Callout-Kanten erhalten explicit:callout Provenance im candidate_pool + - graph_derive_edges.py erkennt diese und verhindert Note-Scope Duplikate """ import asyncio import re +import os import logging from typing import List, Dict, Optional from .chunking_models import Chunk @@ -23,64 +35,106 @@ async def assemble_chunks(note_id: str, md_text: str, note_type: str, config: Op """ Hauptfunktion zur Zerlegung einer Note. Verbindet Strategien mit physikalischer Kontext-Anreicherung. + WP-24c v4.2.5: Frontmatter-Override für chunking_profile wird berücksichtigt. """ - # 1. Konfiguration & Parsing - if config is None: - config = get_chunk_config(note_type) - + # 1. WP-24c v4.2.5: Frontmatter VOR Konfiguration extrahieren (für Override) fm, body_text = extract_frontmatter_from_text(md_text) + + # 2. Konfiguration mit Frontmatter-Override + if config is None: + config = get_chunk_config(note_type, frontmatter=fm) + blocks, doc_title = parse_blocks(md_text) + # WP-24c v4.2.6: Filtere NUR Edge-Zonen (LLM-Validierung & Note-Scope) + # Callouts (is_meta_content=True) müssen durch, damit Chunk-Attribution erhalten bleibt + blocks_for_chunking = [b for b in blocks if not getattr(b, 'exclude_from_chunking', False)] + # Vorbereitung des H1-Präfix für die Embedding-Fenster (Breadcrumbs) h1_prefix = f"# {doc_title}" if doc_title else "" # 2. Anwendung der Splitting-Strategie # Alle Strategien nutzen nun einheitlich context_prefix für die Window-Bildung. + # WP-24c v4.2.6: Callouts sind in blocks_for_chunking enthalten (für Chunk-Attribution) if config.get("strategy") == "by_heading": chunks = await asyncio.to_thread( - strategy_by_heading, blocks, config, note_id, context_prefix=h1_prefix + strategy_by_heading, blocks_for_chunking, config, note_id, context_prefix=h1_prefix ) else: chunks = await asyncio.to_thread( - strategy_sliding_window, blocks, config, note_id, context_prefix=h1_prefix + strategy_sliding_window, blocks_for_chunking, config, note_id, context_prefix=h1_prefix ) if not chunks: return [] # 3. Physikalische Kontext-Anreicherung (Der Qualitäts-Fix) + # WP-24c v4.2.6: Arbeite auf Original-Text inkl. Callouts (für korrekte Chunk-Attribution) # Schreibt Kanten aus Callouts/Inlines hart in den Text für Qdrant. chunks = propagate_section_edges(chunks) - # 4. WP-15b: Candidate Pool Aufbau (Metadaten für IngestionService) + # 5. WP-15b: Candidate Pool Aufbau (Metadaten für IngestionService) + # WP-24c v4.2.7: Markiere Callout-Kanten explizit für Chunk-Attribution # Zuerst die explizit im Text vorhandenen Kanten sammeln. - for ch in chunks: + # WP-24c v4.4.0-DEBUG: Schnittstelle 1 - Extraktion + for idx, ch in enumerate(chunks): # Wir extrahieren aus dem bereits (durch Propagation) angereicherten Text. # ch.candidate_pool wird im Modell-Konstruktor als leere Liste initialisiert. - for e_str in parse_edges_robust(ch.text): - parts = e_str.split(':', 1) + for edge_info in parse_edges_robust(ch.text): + edge_str = edge_info["edge"] + is_callout = edge_info.get("is_callout", False) + parts = edge_str.split(':', 1) if len(parts) == 2: k, t = parts - ch.candidate_pool.append({"kind": k, "to": t, "provenance": "explicit"}) + # WP-24c v4.2.7: Callout-Kanten erhalten explicit:callout Provenance + # WP-24c v4.4.1: Harmonisierung - Provenance muss exakt "explicit:callout" sein + provenance = "explicit:callout" if is_callout else "explicit" + # WP-24c v4.4.1: Verwende "to" für Kompatibilität (wird auch in graph_derive_edges.py erwartet) + # Zusätzlich "target_id" für maximale Kompatibilität mit ingestion_processor Validierung + pool_entry = {"kind": k, "to": t, "provenance": provenance} + if is_callout: + # WP-24c v4.4.1: Für Callouts auch "target_id" hinzufügen für Validierung + pool_entry["target_id"] = t + ch.candidate_pool.append(pool_entry) + + # WP-24c v4.4.0-DEBUG: Schnittstelle 1 - Logging + if is_callout: + logger.debug(f"DEBUG-TRACER [Extraction]: Chunk Index: {idx}, Chunk ID: {ch.id}, Kind: {k}, Target: {t}, Provenance: {provenance}, Is_Callout: {is_callout}, Raw_Edge_Str: {edge_str}") - # 5. Global Pool (Unzugeordnete Kanten aus dem Dokument-Ende) - # Sucht nach dem Edge-Pool Block im Original-Markdown. - pool_match = re.search( - r'###?\s*(?:Unzugeordnete Kanten|Edge Pool|Candidates)\s*\n(.*?)(?:\n#|$)', - body_text, - re.DOTALL | re.IGNORECASE + # 6. Global Pool (Unzugeordnete Kanten - kann mitten im Dokument oder am Ende stehen) + # WP-24c v4.2.0: Konfigurierbare Header-Namen und -Ebene via .env + # Sucht nach ALLEN Edge-Pool Blöcken im Original-Markdown (nicht nur am Ende). + llm_validation_headers = os.getenv( + "MINDNET_LLM_VALIDATION_HEADERS", + "Unzugeordnete Kanten,Edge Pool,Candidates" ) - if pool_match: + header_list = [h.strip() for h in llm_validation_headers.split(",") if h.strip()] + # Fallback auf Defaults, falls leer + if not header_list: + header_list = ["Unzugeordnete Kanten", "Edge Pool", "Candidates"] + + # Header-Ebene konfigurierbar (Default: 3 für ###) + llm_validation_level = int(os.getenv("MINDNET_LLM_VALIDATION_HEADER_LEVEL", "3")) + header_level_pattern = "#" * llm_validation_level + + # Regex-Pattern mit konfigurierbaren Headern und Ebene + # WP-24c v4.2.0: finditer statt search, um ALLE Zonen zu finden (auch mitten im Dokument) + # Zone endet bei einem neuen Header (jeder Ebene) oder am Dokument-Ende + header_pattern = "|".join(re.escape(h) for h in header_list) + zone_pattern = rf'^{re.escape(header_level_pattern)}\s*(?:{header_pattern})\s*\n(.*?)(?=\n#|$)' + + for pool_match in re.finditer(zone_pattern, body_text, re.DOTALL | re.IGNORECASE | re.MULTILINE): global_edges = parse_edges_robust(pool_match.group(1)) - for e_str in global_edges: - parts = e_str.split(':', 1) + for edge_info in global_edges: + edge_str = edge_info["edge"] + parts = edge_str.split(':', 1) if len(parts) == 2: k, t = parts # Diese Kanten werden als "global_pool" markiert für die spätere KI-Prüfung. for ch in chunks: ch.candidate_pool.append({"kind": k, "to": t, "provenance": "global_pool"}) - # 6. De-Duplikation des Pools & Linking + # 7. De-Duplikation des Pools & Linking for ch in chunks: seen = set() unique = [] @@ -92,6 +146,56 @@ async def assemble_chunks(note_id: str, md_text: str, note_type: str, config: Op unique.append(c) ch.candidate_pool = unique + # 8. WP-24c v4.2.6: Clean-Context - Entferne Callout-Syntax aus Chunk-Text + # WICHTIG: Dies geschieht NACH propagate_section_edges und Candidate Pool Aufbau, + # damit Chunk-Attribution erhalten bleibt und Kanten korrekt extrahiert werden. + # Hinweis: Callouts können mehrzeilig sein (auch verschachtelt: >>) + def remove_callouts_from_text(text: str) -> str: + """Entfernt alle Callout-Zeilen (> [!edge] oder > [!abstract]) aus dem Text.""" + if not text: + return text + + lines = text.split('\n') + cleaned_lines = [] + i = 0 + + # NEU (v4.2.8): + # WP-24c v4.2.8: Callout-Pattern für Edge und Abstract + callout_start_pattern = re.compile(r'^>\s*\[!(edge|abstract)[^\]]*\]', re.IGNORECASE) + + while i < len(lines): + line = lines[i] + callout_match = callout_start_pattern.match(line) + + if callout_match: + # Callout gefunden: Überspringe alle Zeilen des Callout-Blocks + leading_gt_count = len(line) - len(line.lstrip('>')) + i += 1 + + # Überspringe alle Zeilen, die zum Callout gehören + while i < len(lines): + next_line = lines[i] + if not next_line.strip().startswith('>'): + break + next_leading_gt = len(next_line) - len(next_line.lstrip('>')) + if next_leading_gt < leading_gt_count: + break + i += 1 + else: + # Normale Zeile: Behalte + cleaned_lines.append(line) + i += 1 + + # Normalisiere Leerzeilen (max. 2 aufeinanderfolgende) + result = '\n'.join(cleaned_lines) + result = re.sub(r'\n\s*\n\s*\n+', '\n\n', result) + return result + + for ch in chunks: + ch.text = remove_callouts_from_text(ch.text) + if ch.window: + ch.window = remove_callouts_from_text(ch.window) + # Verknüpfung der Nachbarschaften für Graph-Traversierung for i, ch in enumerate(chunks): ch.neighbors_prev = chunks[i-1].id if i > 0 else None diff --git a/app/core/chunking/chunking_propagation.py b/app/core/chunking/chunking_propagation.py index 890b89e..c7fc1e3 100644 --- a/app/core/chunking/chunking_propagation.py +++ b/app/core/chunking/chunking_propagation.py @@ -22,11 +22,13 @@ def propagate_section_edges(chunks: List[Chunk]) -> List[Chunk]: continue # Nutzt den robusten Parser aus dem Package - edges = parse_edges_robust(ch.text) - if edges: + # WP-24c v4.2.7: parse_edges_robust gibt jetzt Liste von Dicts zurück + edge_infos = parse_edges_robust(ch.text) + if edge_infos: if ch.section_path not in section_map: section_map[ch.section_path] = set() - section_map[ch.section_path].update(edges) + for edge_info in edge_infos: + section_map[ch.section_path].add(edge_info["edge"]) # 2. Injizieren: Kanten in jeden Chunk der Sektion zurückschreiben (Broadcasting) for ch in chunks: @@ -37,7 +39,9 @@ def propagate_section_edges(chunks: List[Chunk]) -> List[Chunk]: # Vorhandene Kanten (Typ:Ziel) in DIESEM Chunk ermitteln, # um Dopplungen (z.B. durch Callouts) zu vermeiden. - existing_edges = parse_edges_robust(ch.text) + # WP-24c v4.2.7: parse_edges_robust gibt jetzt Liste von Dicts zurück + existing_edge_infos = parse_edges_robust(ch.text) + existing_edges = {ei["edge"] for ei in existing_edge_infos} injections = [] # Sortierung für deterministische Ergebnisse diff --git a/app/core/chunking/chunking_strategies.py b/app/core/chunking/chunking_strategies.py index 5ca68fe..fefb343 100644 --- a/app/core/chunking/chunking_strategies.py +++ b/app/core/chunking/chunking_strategies.py @@ -5,6 +5,7 @@ DESCRIPTION: Strategien für atomares Sektions-Chunking v3.9.9. - Keine redundante Kanten-Injektion. - Strikte Einhaltung von Sektionsgrenzen via Look-Ahead. - Fix: Synchronisierung der Parameter mit dem Orchestrator (context_prefix). + WP-24c v4.2.5: Strict-Mode ohne Carry-Over - Bei strict_heading_split wird nach jeder Sektion geflasht. """ from typing import List, Dict, Any, Optional from .chunking_models import RawBlock, Chunk @@ -83,23 +84,46 @@ def strategy_by_heading(blocks: List[RawBlock], config: Dict[str, Any], note_id: current_meta["title"] = item["meta"].section_title current_meta["path"] = item["meta"].section_path - # FALL A: HARD SPLIT MODUS + # FALL A: HARD SPLIT MODUS (WP-24c v4.2.5: Strict-Mode ohne Carry-Over) if is_hard_split_mode: - # Leere Überschriften (z.B. H1 direkt vor H2) verbleiben am nächsten Chunk - if item.get("is_empty", False) and queue: - current_chunk_text = (current_chunk_text + "\n\n" + item_text).strip() - continue - - combined = (current_chunk_text + "\n\n" + item_text).strip() - # Wenn durch Verschmelzung das Limit gesprengt würde, vorher flashen - if estimate_tokens(combined) > max_tokens and current_chunk_text: + # WP-24c v4.2.5: Bei strict_heading_split: true wird nach JEDER Sektion geflasht + # Kein Carry-Over erlaubt, auch nicht für leere Überschriften + if current_chunk_text: + # Flashe vorherigen Chunk _emit(current_chunk_text, current_meta["title"], current_meta["path"]) - current_chunk_text = item_text - else: - current_chunk_text = combined + current_chunk_text = "" + + # Neue Sektion: Initialisiere Meta + current_meta["title"] = item["meta"].section_title + current_meta["path"] = item["meta"].section_path + + # WP-24c v4.2.5: Auch leere Sektionen werden als separater Chunk erstellt + # (nur Überschrift, kein Inhalt) + if item.get("is_empty", False): + # Leere Sektion: Nur Überschrift als Chunk + _emit(item_text, current_meta["title"], current_meta["path"]) + else: + # Normale Sektion: Prüfe auf Token-Limit + if estimate_tokens(item_text) > max_tokens: + # Sektion zu groß: Smart Zerlegung (aber trotzdem in separaten Chunks) + sents = split_sentences(item_text) + header_prefix = item["meta"].text if item["meta"].kind == "heading" else "" + + take_sents = []; take_len = 0 + while sents: + s = sents.pop(0); slen = estimate_tokens(s) + if take_len + slen > target and take_sents: + _emit(" ".join(take_sents), current_meta["title"], current_meta["path"]) + take_sents = [s]; take_len = slen + else: + take_sents.append(s); take_len += slen + + if take_sents: + _emit(" ".join(take_sents), current_meta["title"], current_meta["path"]) + else: + # Sektion passt: Direkt als Chunk + _emit(item_text, current_meta["title"], current_meta["path"]) - # Im Hard-Split wird nach jeder Sektion geflasht - _emit(current_chunk_text, current_meta["title"], current_meta["path"]) current_chunk_text = "" continue diff --git a/app/core/chunking/chunking_utils.py b/app/core/chunking/chunking_utils.py index da812aa..fe7456b 100644 --- a/app/core/chunking/chunking_utils.py +++ b/app/core/chunking/chunking_utils.py @@ -6,7 +6,7 @@ import math import yaml import logging from pathlib import Path -from typing import Dict, Any, Tuple +from typing import Dict, Any, Tuple, Optional logger = logging.getLogger(__name__) @@ -27,12 +27,31 @@ def load_yaml_config() -> Dict[str, Any]: return data except Exception: return {} -def get_chunk_config(note_type: str) -> Dict[str, Any]: - """Lädt die Chunking-Strategie basierend auf dem Note-Type.""" +def get_chunk_config(note_type: str, frontmatter: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + Lädt die Chunking-Strategie basierend auf dem Note-Type. + WP-24c v4.2.5: Frontmatter-Override für chunking_profile hat höchste Priorität. + + Args: + note_type: Der Typ der Note (z.B. "decision", "experience") + frontmatter: Optionales Frontmatter-Dict mit chunking_profile Override + + Returns: + Dict mit Chunking-Konfiguration + """ full_config = load_yaml_config() profiles = full_config.get("chunking_profiles", {}) type_def = full_config.get("types", {}).get(note_type.lower(), {}) - profile_name = type_def.get("chunking_profile") or full_config.get("defaults", {}).get("chunking_profile", "sliding_standard") + + # WP-24c v4.2.5: Priorität: Frontmatter > Type-Def > Defaults + profile_name = None + if frontmatter and "chunking_profile" in frontmatter: + profile_name = frontmatter.get("chunking_profile") or frontmatter.get("chunk_profile") + if not profile_name: + profile_name = type_def.get("chunking_profile") + if not profile_name: + profile_name = full_config.get("defaults", {}).get("chunking_profile", "sliding_standard") + config = profiles.get(profile_name, DEFAULT_PROFILE).copy() if "overlap" in config and isinstance(config["overlap"], list): config["overlap"] = tuple(config["overlap"]) diff --git a/app/core/database/qdrant_points.py b/app/core/database/qdrant_points.py index 7c36a52..f5b7716 100644 --- a/app/core/database/qdrant_points.py +++ b/app/core/database/qdrant_points.py @@ -1,10 +1,11 @@ """ FILE: app/core/database/qdrant_points.py -DESCRIPTION: Object-Mapper für Qdrant. Konvertiert JSON-Payloads (Notes, Chunks, Edges) in PointStructs und generiert deterministische UUIDs. -VERSION: 1.5.1 (WP-Fix: Explicit Target Section Support) +DESCRIPTION: Object-Mapper für Qdrant. Konvertiert JSON-Payloads (Notes, Chunks, Edges) + in PointStructs und generiert deterministische UUIDs. +VERSION: 4.1.0 (WP-24c: Gold-Standard Identity v4.1.0 - target_section Support) STATUS: Active -DEPENDENCIES: qdrant_client, uuid, os -LAST_ANALYSIS: 2025-12-29 +DEPENDENCIES: qdrant_client, uuid, os, app.core.graph.graph_utils +LAST_ANALYSIS: 2026-01-10 """ from __future__ import annotations import os @@ -14,25 +15,44 @@ from typing import List, Tuple, Iterable, Optional, Dict, Any from qdrant_client.http import models as rest from qdrant_client import QdrantClient +# WP-24c: Import der zentralen Identitäts-Logik zur Vermeidung von ID-Drift +from app.core.graph.graph_utils import _mk_edge_id + # --------------------- ID helpers --------------------- def _to_uuid(stable_key: str) -> str: - return str(uuid.uuid5(uuid.NAMESPACE_URL, stable_key)) + """ + Erzeugt eine deterministische UUIDv5 basierend auf einem stabilen Schlüssel. + Härtung v1.5.2: Guard gegen leere Schlüssel zur Vermeidung von Pydantic-Fehlern. + """ + if not stable_key: + raise ValueError("UUID generation failed: stable_key is empty or None") + return str(uuid.uuid5(uuid.NAMESPACE_URL, str(stable_key))) def _names(prefix: str) -> Tuple[str, str, str]: + """Interne Auflösung der Collection-Namen basierend auf dem Präfix.""" return f"{prefix}_notes", f"{prefix}_chunks", f"{prefix}_edges" # --------------------- Points builders --------------------- def points_for_note(prefix: str, note_payload: dict, note_vec: List[float] | None, dim: int) -> Tuple[str, List[rest.PointStruct]]: + """Konvertiert Note-Metadaten in Qdrant Points.""" notes_col, _, _ = _names(prefix) + # Nutzt Null-Vektor als Fallback, falls kein Embedding vorhanden ist vector = note_vec if note_vec is not None else [0.0] * int(dim) + raw_note_id = note_payload.get("note_id") or note_payload.get("id") or "missing-note-id" point_id = _to_uuid(raw_note_id) - pt = rest.PointStruct(id=point_id, vector=vector, payload=note_payload) + + pt = rest.PointStruct( + id=point_id, + vector=vector, + payload=note_payload + ) return notes_col, [pt] def points_for_chunks(prefix: str, chunk_payloads: List[dict], vectors: List[List[float]]) -> Tuple[str, List[rest.PointStruct]]: + """Konvertiert Chunks und deren Vektoren in Qdrant Points.""" _, chunks_col, _ = _names(prefix) points: List[rest.PointStruct] = [] for i, (pl, vec) in enumerate(zip(chunk_payloads, vectors), start=1): @@ -41,8 +61,13 @@ def points_for_chunks(prefix: str, chunk_payloads: List[dict], vectors: List[Lis note_id = pl.get("note_id") or pl.get("parent_note_id") or "missing-note" chunk_id = f"{note_id}#{i}" pl["chunk_id"] = chunk_id + point_id = _to_uuid(chunk_id) - points.append(rest.PointStruct(id=point_id, vector=vec, payload=pl)) + points.append(rest.PointStruct( + id=point_id, + vector=vec, + payload=pl + )) return chunks_col, points def _normalize_edge_payload(pl: dict) -> dict: @@ -68,25 +93,61 @@ def _normalize_edge_payload(pl: dict) -> dict: return pl def points_for_edges(prefix: str, edge_payloads: List[dict]) -> Tuple[str, List[rest.PointStruct]]: + """ + Konvertiert Kanten-Payloads in PointStructs. + WP-24c v4.1.0: Nutzt die zentrale _mk_edge_id Funktion aus graph_utils. + Dies eliminiert den ID-Drift zwischen manuellen und virtuellen Kanten. + + GOLD-STANDARD v4.1.0: Die ID-Generierung verwendet 4 Parameter + optional target_section + (kind, source_id, target_id, scope, target_section). + rule_id und variant werden ignoriert, target_section fließt ein (Multigraph-Support). + """ _, _, edges_col = _names(prefix) points: List[rest.PointStruct] = [] + for raw in edge_payloads: pl = _normalize_edge_payload(raw) - edge_id = pl.get("edge_id") - if not edge_id: - kind = pl.get("kind", "edge") - s = pl.get("source_id", "unknown-src") - t = pl.get("target_id", "unknown-tgt") - seq = pl.get("seq") or "" - edge_id = f"{kind}:{s}->{t}#{seq}" - pl["edge_id"] = edge_id - point_id = _to_uuid(edge_id) - points.append(rest.PointStruct(id=point_id, vector=[0.0], payload=pl)) + + # Extraktion der Identitäts-Parameter (GOLD-STANDARD v4.1.0) + kind = pl.get("kind", "edge") + s = pl.get("source_id", "unknown-src") + t = pl.get("target_id", "unknown-tgt") + scope = pl.get("scope", "note") + target_section = pl.get("target_section") # WP-24c v4.1.0: target_section für Section-Links + + # Hinweis: rule_id und variant werden im Payload gespeichert, + # fließen aber NICHT in die ID-Generierung ein (v4.0.0 Standard) + # target_section fließt in die ID ein (v4.1.0: Multigraph-Support für Section-Links) + + try: + # Aufruf der Single-Source-of-Truth für IDs + # GOLD-STANDARD v4.1.0: 4 Parameter + optional target_section + point_id = _mk_edge_id( + kind=kind, + s=s, + t=t, + scope=scope, + target_section=target_section + ) + + # Synchronisierung des Payloads mit der berechneten ID + pl["edge_id"] = point_id + + points.append(rest.PointStruct( + id=point_id, + vector=[0.0], + payload=pl + )) + except ValueError as e: + # Fehlerhaft definierte Kanten werden übersprungen, um Pydantic-Crashes zu vermeiden + continue + return edges_col, points # --------------------- Vector schema & overrides --------------------- def _preferred_name(candidates: List[str]) -> str: + """Ermittelt den primären Vektor-Namen aus einer Liste von Kandidaten.""" for k in ("text", "default", "embedding", "content"): if k in candidates: return k @@ -94,10 +155,11 @@ def _preferred_name(candidates: List[str]) -> str: def _env_override_for_collection(collection: str) -> Optional[str]: """ + Prüft auf Umgebungsvariablen-Overrides für Vektor-Namen. Returns: - - "__single__" to force single-vector - - concrete name (str) to force named-vector with that name - - None to auto-detect + - "__single__" für erzwungenen Single-Vector Modus + - Name (str) für spezifischen Named-Vector + - None für automatische Erkennung """ base = os.getenv("MINDNET_VECTOR_NAME") if collection.endswith("_notes"): @@ -112,19 +174,17 @@ def _env_override_for_collection(collection: str) -> Optional[str]: val = base.strip() if val.lower() in ("__single__", "single"): return "__single__" - return val # concrete name + return val def _get_vector_schema(client: QdrantClient, collection_name: str) -> dict: - """ - Return {"kind": "single", "size": int} or {"kind": "named", "names": [...], "primary": str}. - """ + """Ermittelt das Vektor-Schema einer existierenden Collection via API.""" try: info = client.get_collection(collection_name=collection_name) vecs = getattr(info, "vectors", None) - # Single-vector config + # Prüfung auf Single-Vector Konfiguration if hasattr(vecs, "size") and isinstance(vecs.size, int): return {"kind": "single", "size": vecs.size} - # Named-vectors config (dict-like in .config) + # Prüfung auf Named-Vectors Konfiguration cfg = getattr(vecs, "config", None) if isinstance(cfg, dict) and cfg: names = list(cfg.keys()) @@ -135,6 +195,7 @@ def _get_vector_schema(client: QdrantClient, collection_name: str) -> dict: return {"kind": "single", "size": None} def _as_named(points: List[rest.PointStruct], name: str) -> List[rest.PointStruct]: + """Transformiert PointStructs in das Named-Vector Format.""" out: List[rest.PointStruct] = [] for pt in points: vec = getattr(pt, "vector", None) @@ -142,7 +203,6 @@ def _as_named(points: List[rest.PointStruct], name: str) -> List[rest.PointStruc if name in vec: out.append(pt) else: - # take any existing entry; if empty dict fallback to [0.0] fallback_vec = None try: fallback_vec = list(next(iter(vec.values()))) @@ -157,35 +217,42 @@ def _as_named(points: List[rest.PointStruct], name: str) -> List[rest.PointStruc # --------------------- Qdrant ops --------------------- -def upsert_batch(client: QdrantClient, collection: str, points: List[rest.PointStruct]) -> None: +def upsert_batch(client: QdrantClient, collection: str, points: List[rest.PointStruct], wait: bool = True) -> None: + """ + Schreibt Points hocheffizient in eine Collection. + Unterstützt automatische Schema-Erkennung und Named-Vector Transformation. + WP-Fix: 'wait=True' ist Default für Datenkonsistenz zwischen den Ingest-Phasen. + """ if not points: return - # 1) ENV overrides come first + # 1) ENV overrides prüfen override = _env_override_for_collection(collection) if override == "__single__": - client.upsert(collection_name=collection, points=points, wait=True) + client.upsert(collection_name=collection, points=points, wait=wait) return elif isinstance(override, str): - client.upsert(collection_name=collection, points=_as_named(points, override), wait=True) + client.upsert(collection_name=collection, points=_as_named(points, override), wait=wait) return - # 2) Auto-detect schema + # 2) Automatische Schema-Erkennung (Live-Check) schema = _get_vector_schema(client, collection) if schema.get("kind") == "named": name = schema.get("primary") or _preferred_name(schema.get("names") or []) - client.upsert(collection_name=collection, points=_as_named(points, name), wait=True) + client.upsert(collection_name=collection, points=_as_named(points, name), wait=wait) return - # 3) Fallback single-vector - client.upsert(collection_name=collection, points=points, wait=True) + # 3) Fallback: Single-Vector Upsert + client.upsert(collection_name=collection, points=points, wait=wait) # --- Optional search helpers --- def _filter_any(field: str, values: Iterable[str]) -> rest.Filter: + """Hilfsfunktion für händische Filter-Konstruktion (Logical OR).""" return rest.Filter(should=[rest.FieldCondition(key=field, match=rest.MatchValue(value=v)) for v in values]) def _merge_filters(*filters: Optional[rest.Filter]) -> Optional[rest.Filter]: + """Führt mehrere Filter-Objekte zu einem konsolidierten Filter zusammen.""" fs = [f for f in filters if f is not None] if not fs: return None @@ -200,6 +267,7 @@ def _merge_filters(*filters: Optional[rest.Filter]) -> Optional[rest.Filter]: return rest.Filter(must=must) def _filter_from_dict(filters: Optional[Dict[str, Any]]) -> Optional[rest.Filter]: + """Konvertiert ein Python-Dict in ein Qdrant-Filter Objekt.""" if not filters: return None parts = [] @@ -211,9 +279,17 @@ def _filter_from_dict(filters: Optional[Dict[str, Any]]) -> Optional[rest.Filter return _merge_filters(*parts) def search_chunks_by_vector(client: QdrantClient, prefix: str, vector: List[float], top: int = 10, filters: Optional[Dict[str, Any]] = None) -> List[Tuple[str, float, dict]]: + """Sucht semantisch ähnliche Chunks in der Vektordatenbank.""" _, chunks_col, _ = _names(prefix) flt = _filter_from_dict(filters) - res = client.search(collection_name=chunks_col, query_vector=vector, limit=top, with_payload=True, with_vectors=False, query_filter=flt) + res = client.search( + collection_name=chunks_col, + query_vector=vector, + limit=top, + with_payload=True, + with_vectors=False, + query_filter=flt + ) out: List[Tuple[str, float, dict]] = [] for r in res: out.append((str(r.id), float(r.score), dict(r.payload or {}))) @@ -229,41 +305,18 @@ def get_edges_for_sources( edge_types: Optional[Iterable[str]] = None, limit: int = 2048, ) -> List[Dict[str, Any]]: - """Retrieve edge payloads from the _edges collection. - - Args: - client: QdrantClient instance. - prefix: Mindnet collection prefix (e.g. "mindnet"). - source_ids: Iterable of source_id values (typically chunk_ids or note_ids). - edge_types: Optional iterable of edge kinds (e.g. ["references", "depends_on"]). If None, - all kinds are returned. - limit: Maximum number of edge payloads to return. - - Returns: - A list of edge payload dicts, e.g.: - { - "note_id": "...", - "chunk_id": "...", - "kind": "references" | "depends_on" | ..., - "scope": "chunk", - "source_id": "...", - "target_id": "...", - "rule_id": "...", - "confidence": 0.7, - ... - } - """ + """Ruft alle Kanten ab, die von einer Menge von Quell-Notizen ausgehen.""" source_ids = list(source_ids) if not source_ids or limit <= 0: return [] - # Resolve collection name + # Namen der Edges-Collection auflösen _, _, edges_col = _names(prefix) - # Build filter: source_id IN source_ids + # Filter-Bau: source_id IN source_ids src_filter = _filter_any("source_id", [str(s) for s in source_ids]) - # Optional: kind IN edge_types + # Optionaler Filter auf den Kanten-Typ kind_filter = None if edge_types: kind_filter = _filter_any("kind", [str(k) for k in edge_types]) @@ -274,7 +327,7 @@ def get_edges_for_sources( next_page = None remaining = int(limit) - # Use paginated scroll API; we don't need vectors, only payloads. + # Paginated Scroll API (NUR Payload, keine Vektoren) while remaining > 0: batch_limit = min(256, remaining) res, next_page = client.scroll( @@ -286,10 +339,6 @@ def get_edges_for_sources( offset=next_page, ) - # Recovery: In der originalen Codebasis v1.5.0 fehlt hier der Abschluss des Loops. - # Um 100% Konformität zu wahren, habe ich ihn genau so gelassen. - # ACHTUNG: Der Code unten stellt die logische Fortsetzung aus deiner Datei dar. - if not res: break 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 faa18b1..195926f 100644 --- a/app/core/graph/graph_derive_edges.py +++ b/app/core/graph/graph_derive_edges.py @@ -3,10 +3,33 @@ FILE: app/core/graph/graph_derive_edges.py DESCRIPTION: Hauptlogik zur Kanten-Aggregation und De-Duplizierung. WP-15b/c Audit: - Präzises Sektions-Splitting via parse_link_target. - - Eindeutige ID-Generierung pro Sektions-Variante (Multigraph). + - 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 + 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 + WP-24c v4.2.2: Semantische De-Duplizierung + - Gruppierung nach (kind, source, target, section) unabhängig vom Scope + - Scope-Entscheidung: explicit:note_zone > chunk-Scope + - ID-Berechnung erst nach Scope-Entscheidung + WP-24c v4.3.0: Lokalisierung des Datenverlusts + - Debug-Logik für Audit des Datentransfers + - Verifizierung der candidate_pool Übertragung + WP-24c v4.3.1: Präzisions-Priorität für Chunk-Scope + - Chunk-Scope gewinnt zwingend über Note-Scope (außer explicit:note_zone) + - Confidence-Werte: candidate_pool explicit:callout = 1.0, globaler Scan = 0.7 + - Key-Generierung gehärtet für konsistente Deduplizierung +VERSION: 4.3.1 (WP-24c: Präzisions-Priorität) +STATUS: Active """ -from typing import List, Optional, Dict, Tuple +import re +import logging +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 @@ -15,19 +38,582 @@ from .graph_extractors import ( extract_typed_relations, extract_callout_relations, extract_wikilinks ) +# WP-24c v4.4.0-DEBUG: Logger am Modul-Level für alle Funktionen verfügbar +logger = logging.getLogger(__name__) + +# WP-24c v4.2.0: Header-basierte Identifikation von Note-Scope Zonen +# Konfigurierbar via MINDNET_NOTE_SCOPE_ZONE_HEADERS (komma-separiert) +def get_note_scope_zone_headers() -> List[str]: + """ + Lädt die konfigurierten Header-Namen für Note-Scope Zonen. + Fallback auf Defaults, falls nicht konfiguriert. + """ + import os + headers_env = os.getenv( + "MINDNET_NOTE_SCOPE_ZONE_HEADERS", + "Smart Edges,Relationen,Global Links,Note-Level Relations,Globale Verbindungen" + ) + header_list = [h.strip() for h in headers_env.split(",") if h.strip()] + # Fallback auf Defaults, falls leer + if not header_list: + header_list = [ + "Smart Edges", + "Relationen", + "Global Links", + "Note-Level Relations", + "Globale Verbindungen" + ] + return header_list + +# WP-24c v4.5.6: Header-basierte Identifikation von LLM-Validierungs-Zonen +# Konfigurierbar via MINDNET_LLM_VALIDATION_HEADERS (komma-separiert) +def get_llm_validation_zone_headers() -> List[str]: + """ + Lädt die konfigurierten Header-Namen für LLM-Validierungs-Zonen. + Fallback auf Defaults, falls nicht konfiguriert. + """ + import os + headers_env = os.getenv( + "MINDNET_LLM_VALIDATION_HEADERS", + "Unzugeordnete Kanten,Edge Pool,Candidates" + ) + header_list = [h.strip() for h in headers_env.split(",") if h.strip()] + # Fallback auf Defaults, falls leer + if not header_list: + header_list = [ + "Unzugeordnete Kanten", + "Edge Pool", + "Candidates" + ] + return header_list + +def extract_note_scope_zones(markdown_body: str) -> List[Tuple[str, str]]: + """ + WP-24c v4.2.0: Extrahiert Note-Scope Zonen aus Markdown. + WP-24c v4.5.6: Unterscheidet zwischen Note-Scope-Zonen und LLM-Validierungs-Zonen. + + Identifiziert Sektionen mit spezifischen Headern (konfigurierbar via .env) + 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]] = [] + + # WP-24c v4.2.0: Konfigurierbare Header-Ebene + import os + import re + note_scope_level = int(os.getenv("MINDNET_NOTE_SCOPE_HEADER_LEVEL", "2")) + header_level_pattern = "#" * note_scope_level + + # Regex für Header-Erkennung (konfigurierbare Ebene) + header_pattern = rf'^{re.escape(header_level_pattern)}\s+(.+?)$' + + lines = markdown_body.split('\n') + in_zone = False + zone_content = [] + + # WP-24c v4.5.6: Lade beide Header-Listen für Unterscheidung + zone_headers = get_note_scope_zone_headers() + llm_validation_headers = get_llm_validation_zone_headers() + + 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() + + # WP-24c v4.5.6: Prüfe, ob dieser Header eine Note-Scope Zone ist + # (NICHT eine LLM-Validierungs-Zone - diese werden separat behandelt) + is_zone_header = any( + header_text.lower() == zone_header.lower() + for zone_header in zone_headers + ) + + # WP-24c v4.5.6: Ignoriere LLM-Validierungs-Zonen hier (werden separat verarbeitet) + is_llm_validation = any( + header_text.lower() == llm_header.lower() + for llm_header in llm_validation_headers + ) + + if is_zone_header and not is_llm_validation: + 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)) + # WP-24c v4.2.1: Callouts werden NICHT hier extrahiert, da sie global abgedeckt werden + 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)) + # WP-24c v4.2.1: Callouts werden NICHT hier extrahiert, da sie global abgedeckt werden + + return edges + +def extract_llm_validation_zones(markdown_body: str) -> List[Tuple[str, str]]: + """ + WP-24c v4.5.6: Extrahiert LLM-Validierungs-Zonen aus Markdown. + + Identifiziert Sektionen mit LLM-Validierungs-Headern (konfigurierbar via .env) + und extrahiert alle darin enthaltenen Links (Wikilinks, Typed Relations, Callouts). + Diese Kanten erhalten das Präfix "candidate:" in der rule_id. + + Returns: + List[Tuple[str, str]]: Liste von (kind, target) Tupeln + """ + if not markdown_body: + return [] + + edges: List[Tuple[str, str]] = [] + + # WP-24c v4.5.6: Konfigurierbare Header-Ebene für LLM-Validierung + import os + import re + llm_validation_level = int(os.getenv("MINDNET_LLM_VALIDATION_HEADER_LEVEL", "3")) + header_level_pattern = "#" * llm_validation_level + + # Regex für Header-Erkennung (konfigurierbare Ebene) + header_pattern = rf'^{re.escape(header_level_pattern)}\s+(.+?)$' + + lines = markdown_body.split('\n') + in_zone = False + zone_content = [] + + # WP-24c v4.5.6: Lade LLM-Validierungs-Header + llm_validation_headers = get_llm_validation_zone_headers() + + for i, line in enumerate(lines): + # Prüfe auf Header (konfiguriertes Level aus MINDNET_LLM_VALIDATION_HEADER_LEVEL) + header_match = re.match(header_pattern, line.strip()) + + if header_match: + header_text = header_match.group(1).strip() + + # WP-24c v4.5.6: Prüfe, ob dieser Header eine LLM-Validierungs-Zone ist + is_llm_validation = any( + header_text.lower() == llm_header.lower() + for llm_header in llm_validation_headers + ) + + if is_llm_validation: + 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)) + # WP-24c v4.5.6: Extrahiere auch Callouts aus LLM-Validierungs-Zonen + callout_pairs, _ = extract_callout_relations(zone_text) + edges.extend(callout_pairs) + 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)) + # WP-24c v4.5.6: Extrahiere auch Callouts aus LLM-Validierungs-Zonen + callout_pairs, _ = extract_callout_relations(zone_text) + edges.extend(callout_pairs) + + 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. + WP-24c v4.5.6: Header-Status-Maschine für korrekte Zonen-Erkennung. + + 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. + + WP-24c v4.5.6: Prüft für jeden Callout, ob er in einer LLM-Validierungs-Zone liegt. + - In LLM-Validierungs-Zone: rule_id = "candidate:explicit:callout" + - In Standard-Zone: rule_id = "explicit:callout" (ohne candidate:) + + 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] = [] + + # WP-24c v4.5.6: Header-Status-Maschine - Baue Mapping von Zeilen zu Zonen-Status + import os + import re + + llm_validation_headers = get_llm_validation_zone_headers() + llm_validation_level = int(os.getenv("MINDNET_LLM_VALIDATION_HEADER_LEVEL", "3")) + # WP-24c v4.5.6: Konfigurierbare Header-Ebene (vollständig über .env steuerbar) + header_level_pattern = "#" * llm_validation_level + header_pattern = rf'^{re.escape(header_level_pattern)}\s+(.+?)$' + + lines = markdown_body.split('\n') + current_zone_is_llm_validation = False + + # WP-24c v4.5.6: Zeile-für-Zeile Verarbeitung mit Zonen-Tracking + # Extrahiere Callouts direkt während des Durchlaufs, um Zonen-Kontext zu behalten + current_kind = None + in_callout_block = False + callout_block_lines = [] # Sammle Zeilen eines Callout-Blocks + + for i, line in enumerate(lines): + stripped = line.strip() + + # WP-24c v4.5.6: Prüfe auf Header (Zonen-Wechsel) + # Verwendet das konfigurierte Level aus MINDNET_LLM_VALIDATION_HEADER_LEVEL + header_match = re.match(header_pattern, stripped) + + if header_match: + header_text = header_match.group(1).strip() + # WP-24c v4.5.7: Speichere Zonen-Status VOR der Aktualisierung + # (für Callout-Blöcke, die vor diesem Header enden) + zone_before_header = current_zone_is_llm_validation + + # Prüfe, ob dieser Header eine LLM-Validierungs-Zone startet + # WP-24c v4.5.6: Header-Status-Maschine - korrekte Zonen-Erkennung + current_zone_is_llm_validation = any( + header_text.lower() == llm_header.lower() + for llm_header in llm_validation_headers + ) + logger.debug(f"DEBUG-TRACER [Zone-Change]: Header '{header_text}' (Level {llm_validation_level}) -> LLM-Validierung: {current_zone_is_llm_validation} (vorher: {zone_before_header})") + # Beende aktuellen Callout-Block bei Header-Wechsel + if in_callout_block: + # Verarbeite gesammelten Callout-Block VOR dem Zonen-Wechsel + if callout_block_lines: + block_text = '\n'.join([lines[j] for j in callout_block_lines]) + block_call_pairs, _ = extract_callout_relations(block_text) + + # Verarbeite jeden Callout mit Zonen-Kontext + # WICHTIG: Verwende den Zonen-Status VOR dem Header-Wechsel + + for k, raw_t in block_call_pairs: + t, sec = parse_link_target(raw_t, note_id) + if not t: + continue + + callout_key = (k, t, sec) + is_blocked = callout_key in existing_chunk_callouts + + if is_blocked: + continue + + # WP-24c v4.5.6: Bestimme rule_id basierend auf Zonen-Status VOR Header + if zone_before_header: + rule_id = "candidate:explicit:callout" + provenance = "explicit:callout" + else: + rule_id = "explicit:callout" # KEIN candidate: für Standard-Zonen + provenance = "explicit:callout" + + payload = { + "edge_id": _mk_edge_id(k, note_id, t, "note", target_section=sec), + "provenance": provenance, + "rule_id": rule_id, + "confidence": 0.7 + } + if sec: + payload["target_section"] = sec + + logger.debug(f"DEBUG-TRACER [Zone-Check]: Callout in {'LLM-Validierungs' if zone_before_header else 'Standard'}-Zone (Zeile {callout_block_lines[0]}) -> rule_id: {rule_id}") + + edges.append(_edge( + kind=k, + scope="note", + source_id=note_id, + target_id=t, + note_id=note_id, + extra=payload + )) + + # Reset für nächsten Block + in_callout_block = False + current_kind = None + callout_block_lines = [] + continue + + # WP-24c v4.5.6: Prüfe auf Callout-Start + callout_start_match = re.match(r'^\s*>{1,}\s*\[!edge\]\s*(.*)$', stripped, re.IGNORECASE) + if callout_start_match: + in_callout_block = True + callout_block_lines = [i] # Start-Zeile + header_content = callout_start_match.group(1).strip() + # Prüfe, ob Header einen Typ enthält + if header_content and re.match(r'^[a-z_]+$', header_content, re.IGNORECASE): + current_kind = header_content.lower() + continue + + # WP-24c v4.5.6: Sammle Callout-Block-Zeilen + if in_callout_block: + if stripped.startswith('>'): + callout_block_lines.append(i) + else: + # Callout-Block beendet - verarbeite gesammelte Zeilen + if callout_block_lines: + # Extrahiere Callouts aus diesem Block + block_text = '\n'.join([lines[j] for j in callout_block_lines]) + block_call_pairs, _ = extract_callout_relations(block_text) + + # Verarbeite jeden Callout mit Zonen-Kontext + for k, raw_t in block_call_pairs: + t, sec = parse_link_target(raw_t, note_id) + if not t: + continue + + callout_key = (k, t, sec) + is_blocked = callout_key in existing_chunk_callouts + + if is_blocked: + continue + + # WP-24c v4.5.6: Bestimme rule_id basierend auf Zonen-Status + if current_zone_is_llm_validation: + rule_id = "candidate:explicit:callout" + provenance = "explicit:callout" + else: + rule_id = "explicit:callout" # KEIN candidate: für Standard-Zonen + provenance = "explicit:callout" + + payload = { + "edge_id": _mk_edge_id(k, note_id, t, "note", target_section=sec), + "provenance": provenance, + "rule_id": rule_id, + "confidence": 0.7 + } + if sec: + payload["target_section"] = sec + + logger.debug(f"DEBUG-TRACER [Zone-Check]: Callout in {'LLM-Validierungs' if current_zone_is_llm_validation else 'Standard'}-Zone (Zeile {callout_block_lines[0]}) -> rule_id: {rule_id}") + + edges.append(_edge( + kind=k, + scope="note", + source_id=note_id, + target_id=t, + note_id=note_id, + extra=payload + )) + + # Reset für nächsten Block + in_callout_block = False + current_kind = None + callout_block_lines = [] + + # WP-24c v4.5.6: Verarbeite letzten Callout-Block (falls am Ende) + if in_callout_block and callout_block_lines: + block_text = '\n'.join([lines[j] for j in callout_block_lines]) + block_call_pairs, _ = extract_callout_relations(block_text) + + for k, raw_t in block_call_pairs: + t, sec = parse_link_target(raw_t, note_id) + if not t: + continue + + callout_key = (k, t, sec) + is_blocked = callout_key in existing_chunk_callouts + + if is_blocked: + continue + + # WP-24c v4.5.6: Bestimme rule_id basierend auf Zonen-Status + if current_zone_is_llm_validation: + rule_id = "candidate:explicit:callout" + provenance = "explicit:callout" + else: + rule_id = "explicit:callout" + provenance = "explicit:callout" + + payload = { + "edge_id": _mk_edge_id(k, note_id, t, "note", target_section=sec), + "provenance": provenance, + "rule_id": rule_id, + "confidence": 0.7 + } + if sec: + payload["target_section"] = sec + + logger.debug(f"DEBUG-TRACER [Zone-Check]: Callout in {'LLM-Validierungs' if current_zone_is_llm_validation else 'Standard'}-Zone (Zeile {callout_block_lines[0]}) -> rule_id: {rule_id}") + + edges.append(_edge( + kind=k, + scope="note", + source_id=note_id, + target_id=t, + note_id=note_id, + extra=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]: """ 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. + WP-24c v4.2.7: Chunk-Attribution für Callouts über candidate_pool mit explicit:callout Provenance. + WP-24c v4.2.9: Finalisierung der Chunk-Attribution - Synchronisation mit "Semantic First" Signal. + Callout-Keys werden VOR dem globalen Scan aus candidate_pool gesammelt. + WP-24c v4.2.9 Fix B: Zwei-Phasen-Synchronisation für Chunk-Autorität. + Phase 1: Sammle alle explicit:callout Keys VOR Text-Scan. + Phase 2: Globaler Scan respektiert all_chunk_callout_keys als Ausschlusskriterium. + + 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.5.7: Initialisiere all_chunk_callout_keys VOR jeder Verwendung + # Dies verhindert UnboundLocalError, wenn LLM-Validierungs-Zonen vor Phase 1 verarbeitet werden + all_chunk_callout_keys: Set[Tuple[str, str, Optional[str]]] = set() + + # WP-24c v4.2.0: Note-Scope Zonen Extraktion (VOR Chunk-Verarbeitung) + # WP-24c v4.5.6: Separate Behandlung von LLM-Validierungs-Zonen + note_scope_edges: List[dict] = [] + llm_validation_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) + 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 + )) + + # WP-24c v4.5.6: LLM-Validierungs-Zonen (mit candidate: Präfix) + llm_validation_links = extract_llm_validation_zones(markdown_body) + for kind, raw_target in llm_validation_links: + target, sec = parse_link_target(raw_target, note_id) + if not target: + continue + + # WP-24c v4.5.6: LLM-Validierungs-Kanten mit scope: "note" und rule_id: "candidate:..." + # Diese werden gegen alle Chunks der Note geprüft + # Bestimme Provenance basierend auf Link-Typ + if kind == "related_to": + # Wikilink in LLM-Validierungs-Zone + provenance = "explicit:wikilink" + else: + # Typed Relation oder Callout in LLM-Validierungs-Zone + provenance = "explicit" + + payload = { + "edge_id": _mk_edge_id(kind, note_id, target, "note", target_section=sec), + "provenance": provenance, + "rule_id": f"candidate:{provenance}", # WP-24c v4.5.6: Zonen-Priorität - candidate: Präfix + "confidence": PROVENANCE_PRIORITY.get(provenance, 0.90) + } + if sec: + payload["target_section"] = sec + + llm_validation_edges.append(_edge( + kind=kind, + scope="note", + source_id=note_id, # WP-24c v4.5.6: source_id = note_id (Note-Scope für LLM-Validierung) + target_id=target, + note_id=note_id, + extra=payload + )) + + # WP-24c v4.5.6: Füge Callouts aus LLM-Validierungs-Zonen zu all_chunk_callout_keys hinzu + # damit sie nicht im globalen Scan doppelt verarbeitet werden + # (Nur für Callouts, nicht für Wikilinks oder Typed Relations) + # Callouts werden in extract_llm_validation_zones bereits extrahiert + # und müssen daher aus dem globalen Scan ausgeschlossen werden + # Hinweis: extract_llm_validation_zones gibt auch Callouts zurück (als (kind, target) Tupel) + # Daher müssen wir prüfen, ob es sich um einen Callout handelt + # (Callouts haben typischerweise spezifische kinds wie "depends_on", "related_to", etc.) + # Für jetzt nehmen wir an, dass alle Links aus LLM-Validierungs-Zonen als "bereits verarbeitet" markiert werden + # Dies verhindert Duplikate im globalen Scan + callout_key = (kind, target, sec) + all_chunk_callout_keys.add(callout_key) + logger.debug(f"Note [{note_id}]: LLM-Validierungs-Zone Callout-Key hinzugefügt: ({kind}, {target}, {sec})") # 1) Struktur-Kanten (Internal: belongs_to, next/prev) # Diese erhalten die Provenienz 'structure' und sind in der Registry geschützt. @@ -36,9 +622,10 @@ def build_edges_for_note( if not cid: continue # Verbindung Chunk -> Note + # WP-24c v4.0.0: rule_id wird nur im Payload gespeichert, fließt nicht in die ID ein edges.append(_edge("belongs_to", "chunk", cid, note_id, note_id, { "chunk_id": cid, - "edge_id": _mk_edge_id("belongs_to", cid, note_id, "chunk", "structure:belongs_to"), + "edge_id": _mk_edge_id("belongs_to", cid, note_id, "chunk"), "provenance": "structure", "rule_id": "structure:belongs_to", "confidence": PROVENANCE_PRIORITY["structure:belongs_to"] @@ -48,14 +635,14 @@ def build_edges_for_note( if idx < len(chunks) - 1: next_id = _get(chunks[idx+1], "chunk_id", "id") if next_id: + # WP-24c v4.0.0: rule_id wird nur im Payload gespeichert, fließt nicht in die ID ein edges.append(_edge("next", "chunk", cid, next_id, note_id, { "chunk_id": cid, - "edge_id": _mk_edge_id("next", cid, next_id, "chunk", "structure:order"), + "edge_id": _mk_edge_id("next", cid, next_id, "chunk"), "provenance": "structure", "rule_id": "structure:order", "confidence": PROVENANCE_PRIORITY["structure:order"] })) edges.append(_edge("prev", "chunk", next_id, cid, note_id, { - "chunk_id": next_id, - "edge_id": _mk_edge_id("prev", next_id, cid, "chunk", "structure:order"), + "edge_id": _mk_edge_id("prev", next_id, cid, "chunk"), "provenance": "structure", "rule_id": "structure:order", "confidence": PROVENANCE_PRIORITY["structure:order"] })) @@ -63,7 +650,58 @@ def build_edges_for_note( reg = load_types_registry() defaults = get_edge_defaults_for(note_type, reg) refs_all: List[str] = [] + + # WP-24c v4.5.7: all_chunk_callout_keys wurde bereits oben initialisiert + # (Zeile 530) - keine erneute Initialisierung nötig + # PHASE 1 (Sicherung der Chunk-Autorität): Sammle alle Callout-Keys aus candidate_pool + # BEVOR der globale Markdown-Scan oder der Loop über die Chunks beginnt + # Dies stellt sicher, dass bereits geerntete Callouts nicht dupliziert werden + # WP-24c v4.3.0: Debug-Logik für Audit des Datentransfers + # WP-24c v4.4.0-DEBUG: Logger ist am Modul-Level definiert + + for ch in chunks: + cid = _get(ch, "chunk_id", "id") + if not cid: continue + + # Iteriere durch candidate_pool und sammle explicit:callout Kanten + pool = ch.get("candidate_pool") or ch.get("candidate_edges") or [] + + # WP-24c v4.3.0: Debug-Logik - Ausgabe der Pool-Größe + pool_size = len(pool) + explicit_callout_count = sum(1 for cand in pool if cand.get("provenance") == "explicit:callout") + if pool_size > 0: + logger.debug(f"Note [{note_id}]: Chunk [{ch.get('index', '?')}] hat {pool_size} Kanten im Candidate-Pool ({explicit_callout_count} explicit:callout)") + + for cand in pool: + # WP-24c v4.4.1: Harmonisierung - akzeptiere sowohl "to" als auch "target_id" + raw_t = cand.get("to") or cand.get("target_id") + k = cand.get("kind", "related_to") + p = cand.get("provenance", "semantic_ai") + + # WP-24c v4.4.1: String-Check - Provenance muss exakt "explicit:callout" sein (case-sensitive) + # WP-24c v4.2.9 Fix B: Wenn Provenance explicit:callout, extrahiere Key + # WP-24c v4.3.1: Key-Generierung gehärtet - Format (kind, target_id, target_section) + # Exakt konsistent mit dem globalen Scan für zuverlässige Deduplizierung + if p == "explicit:callout" and raw_t: + t, sec = parse_link_target(raw_t, note_id) + if t: + # Key-Format: (kind, target_id, target_section) - exakt wie im globalen Scan + # Dies verhindert, dass der globale Scan diese Kante als Note-Scope neu anlegt + callout_key = (k, t, sec) # WP-24c v4.3.1: Explizite Key-Generierung + all_chunk_callout_keys.add(callout_key) + logger.debug(f"Note [{note_id}]: Callout-Key gesammelt: ({k}, {t}, {sec})") + + # WP-24c v4.4.0-DEBUG: Schnittstelle 3 - Synchronisation Phase 1 + logger.debug(f"DEBUG-TRACER [Phase 1 Sync]: Gefundener Key im Pool: ({k}, {t}, {sec}), Raw_Target: {raw_t}, Zugeordnet zu: {cid}, Chunk_Index: {ch.get('index', '?')}, Provenance: {p}") + + # WP-24c v4.3.0: Debug-Logik - Ausgabe der gesammelten Keys + if all_chunk_callout_keys: + logger.debug(f"Note [{note_id}]: Gesammelt {len(all_chunk_callout_keys)} Callout-Keys aus candidate_pools") + else: + logger.warning(f"Note [{note_id}]: KEINE Callout-Keys in candidate_pools gefunden - möglicher Datenverlust!") + + # WP-24c v4.2.9: PHASE 2: Verarbeite Chunks und erstelle Kanten for ch in chunks: cid = _get(ch, "chunk_id", "id") if not cid: continue @@ -77,36 +715,67 @@ def build_edges_for_note( payload = { "chunk_id": cid, - # WP-Fix: Variant=sec sorgt für eindeutige ID pro Sektion - "edge_id": _mk_edge_id(k, cid, t, "chunk", "inline:rel", variant=sec), + # WP-24c v4.1.0: target_section fließt nun fest in die ID-Generierung ein + "edge_id": _mk_edge_id(k, cid, t, "chunk", target_section=sec), "provenance": "explicit", "rule_id": "inline:rel", "confidence": PROVENANCE_PRIORITY["inline:rel"] } if sec: payload["target_section"] = sec edges.append(_edge(k, "chunk", cid, t, note_id, payload)) # B. Candidate Pool (WP-15b Validierte KI-Kanten) + # WP-24c v4.2.9: Erstelle Kanten aus candidate_pool (Keys bereits in Phase 1 gesammelt) pool = ch.get("candidate_pool") or ch.get("candidate_edges") or [] for cand in pool: - raw_t, k, p = cand.get("to"), cand.get("kind", "related_to"), cand.get("provenance", "semantic_ai") + # WP-24c v4.4.1: Harmonisierung - akzeptiere sowohl "to" als auch "target_id" + raw_t = cand.get("to") or cand.get("target_id") + k = cand.get("kind", "related_to") + p = cand.get("provenance", "semantic_ai") t, sec = parse_link_target(raw_t, note_id) if t: + # WP-24c v4.1.0: target_section fließt nun fest in die ID-Generierung ein + # WP-24c v4.3.1: explicit:callout erhält Confidence 1.0 für Präzisions-Priorität + # WP-24c v4.5.6: candidate: Präfix NUR für global_pool (aus LLM-Validierungs-Zonen) + # Normale Callouts im Fließtext erhalten KEIN candidate: Präfix + confidence = 1.0 if p == "explicit:callout" else PROVENANCE_PRIORITY.get(p, 0.90) + + # WP-24c v4.5.6: rule_id nur mit candidate: für global_pool (LLM-Validierungs-Zonen) + # explicit:callout (normale Callouts im Fließtext) erhalten KEIN candidate: Präfix + if p == "global_pool": + rule_id = f"candidate:{p}" + elif p == "explicit:callout": + rule_id = "explicit:callout" # WP-24c v4.5.6: Kein candidate: für Fließtext-Callouts + else: + rule_id = p # Andere Provenances ohne candidate: + payload = { "chunk_id": cid, - "edge_id": _mk_edge_id(k, cid, t, "chunk", f"candidate:{p}", variant=sec), - "provenance": p, "rule_id": f"candidate:{p}", "confidence": PROVENANCE_PRIORITY.get(p, 0.90) + "edge_id": _mk_edge_id(k, cid, t, "chunk", target_section=sec), + "provenance": p, "rule_id": rule_id, "confidence": confidence } 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.9: Fallback für Callouts im gereinigten Text + # HINWEIS: Da der Text bereits gereinigt wurde (Clean-Context), werden hier typischerweise + # keine Callouts mehr gefunden. Falls doch, prüfe gegen all_chunk_callout_keys. 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.9: Prüfe, ob dieser Callout bereits im candidate_pool erfasst wurde + callout_key = (k, t, sec) + if callout_key in all_chunk_callout_keys: + # Bereits im candidate_pool erfasst -> überspringe (wird mit chunk-Scope angelegt) + continue + + # WP-24c v4.2.1: Tracke Callout für spätere Deduplizierung (global sammeln) + all_chunk_callout_keys.add(callout_key) + + # WP-24c v4.1.0: target_section fließt nun fest in die ID-Generierung ein payload = { "chunk_id": cid, - "edge_id": _mk_edge_id(k, cid, t, "chunk", "callout:edge", variant=sec), + "edge_id": _mk_edge_id(k, cid, t, "chunk", target_section=sec), "provenance": "explicit", "rule_id": "callout:edge", "confidence": PROVENANCE_PRIORITY["callout:edge"] } if sec: payload["target_section"] = sec @@ -118,9 +787,10 @@ def build_edges_for_note( r, sec = parse_link_target(raw_r, note_id) if not r: continue + # WP-24c v4.1.0: target_section fließt nun fest in die ID-Generierung ein payload = { "chunk_id": cid, "ref_text": raw_r, - "edge_id": _mk_edge_id("references", cid, r, "chunk", "explicit:wikilink", variant=sec), + "edge_id": _mk_edge_id("references", cid, r, "chunk", target_section=sec), "provenance": "explicit", "rule_id": "explicit:wikilink", "confidence": PROVENANCE_PRIORITY["explicit:wikilink"] } if sec: payload["target_section"] = sec @@ -129,9 +799,10 @@ def build_edges_for_note( # Automatische Kanten-Vererbung aus types.yaml for rel in defaults: if rel != "references": + # WP-24c v4.1.0: target_section fließt nun fest in die ID-Generierung ein def_payload = { "chunk_id": cid, - "edge_id": _mk_edge_id(rel, cid, r, "chunk", f"edge_defaults:{rel}", variant=sec), + "edge_id": _mk_edge_id(rel, cid, r, "chunk", target_section=sec), "provenance": "rule", "rule_id": f"edge_defaults:{rel}", "confidence": PROVENANCE_PRIORITY["edge_defaults"] } if sec: def_payload["target_section"] = sec @@ -146,24 +817,181 @@ def build_edges_for_note( for r in refs_note: if not r: continue + # WP-24c v4.0.0: rule_id wird nur im Payload gespeichert, fließt nicht in die ID ein edges.append(_edge("references", "note", note_id, r, note_id, { - "edge_id": _mk_edge_id("references", note_id, r, "note", "explicit:note_scope"), - "provenance": "explicit", "confidence": PROVENANCE_PRIORITY["explicit:note_scope"] + "edge_id": _mk_edge_id("references", note_id, r, "note"), + "provenance": "explicit", "rule_id": "explicit:note_scope", "confidence": PROVENANCE_PRIORITY["explicit:note_scope"] })) # Backlinks zur Stärkung der Bidirektionalität edges.append(_edge("backlink", "note", r, note_id, note_id, { - "edge_id": _mk_edge_id("backlink", r, note_id, "note", "derived:backlink"), - "provenance": "rule", "confidence": PROVENANCE_PRIORITY["derived:backlink"] + "edge_id": _mk_edge_id("backlink", r, note_id, "note"), + "provenance": "rule", "rule_id": "derived:backlink", "confidence": PROVENANCE_PRIORITY["derived:backlink"] })) - # 4) De-Duplizierung (In-Place) - # Da die EDGE-ID nun die Sektion (variant) enthält, bleiben Links auf - # unterschiedliche Abschnitte derselben Note erhalten. - unique_map: Dict[str, dict] = {} + # 4) WP-24c v4.2.0: Note-Scope Edges hinzufügen (VOR De-Duplizierung) + # WP-24c v4.2.0: Note-Scope Edges hinzufügen + edges.extend(note_scope_edges) + # WP-24c v4.5.6: LLM-Validierungs-Edges hinzufügen (mit candidate: Präfix) + edges.extend(llm_validation_edges) + + # 5) WP-24c v4.2.9 Fix B PHASE 2 (Deduplizierung): Callout-Extraktion aus Markdown + # Der globale Scan des markdown_body nutzt all_chunk_callout_keys als Ausschlusskriterium. + # Callouts, die bereits in Phase 1 als Chunk-Kanten identifiziert wurden, + # dürfen nicht erneut als Note-Scope Kanten angelegt werden. + callout_edges_from_markdown: List[dict] = [] + if markdown_body: + # WP-24c v4.3.0: Debug-Logik - Ausgabe vor globalem Scan + logger.debug(f"Note [{note_id}]: Starte globalen Markdown-Scan mit {len(all_chunk_callout_keys)} ausgeschlossenen Callout-Keys") + + # WP-24c v4.4.0-DEBUG: Schnittstelle 3 - Global Scan Start + block_list = list(all_chunk_callout_keys) + logger.debug(f"DEBUG-TRACER [Global Scan Start]: Block-Liste (all_chunk_callout_keys): {block_list}, Anzahl: {len(block_list)}") + for key in block_list: + logger.debug(f"DEBUG-TRACER [Global Scan Start]: Block-Key Detail - Kind: {key[0]}, Target: {key[1]}, Section: {key[2]}") + + callout_edges_from_markdown = extract_callouts_from_markdown( + markdown_body, + note_id, + existing_chunk_callouts=all_chunk_callout_keys # WP-24c v4.2.9 Fix B: Strikte Respektierung + ) + + # WP-24c v4.3.0: Debug-Logik - Ausgabe nach globalem Scan + if callout_edges_from_markdown: + logger.debug(f"Note [{note_id}]: Globaler Scan erzeugte {len(callout_edges_from_markdown)} Note-Scope Callout-Kanten") + else: + logger.debug(f"Note [{note_id}]: Globaler Scan erzeugte KEINE Note-Scope Callout-Kanten (alle bereits in Chunks)") + + edges.extend(callout_edges_from_markdown) + + # 6) WP-24c v4.2.2: Semantische De-Duplizierung mit Scope-Entscheidung + # Problem: edge_id enthält Scope, daher werden semantisch identische Kanten + # (gleiches kind, source, target, section) mit unterschiedlichem Scope nicht erkannt. + # Lösung: Zuerst semantische Gruppierung, dann Scope-Entscheidung, dann ID-Berechnung. + + # Schritt 1: Semantische Gruppierung (unabhängig vom Scope) + # Schlüssel: (kind, source_id, target_id, target_section) + # Hinweis: source_id ist bei chunk-Scope die chunk_id, bei note-Scope die note_id + # Für semantische Gleichheit müssen wir prüfen: Ist die Quelle die gleiche Note? + semantic_groups: Dict[Tuple[str, str, str, Optional[str]], List[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): - unique_map[eid] = e - - return list(unique_map.values()) \ No newline at end of file + kind = e.get("kind", "related_to") + source_id = e.get("source_id", "") + target_id = e.get("target_id", "") + target_section = e.get("target_section") + scope = e.get("scope", "chunk") + note_id_from_edge = e.get("note_id", "") + + # WP-24c v4.2.2: Normalisiere source_id für semantische Gruppierung + # Bei chunk-Scope: source_id ist chunk_id, aber wir wollen nach note_id gruppieren + # Bei note-Scope: source_id ist bereits note_id + # Für semantische Gleichheit: Beide Kanten müssen von derselben Note ausgehen + if scope == "chunk": + # Bei chunk-Scope: source_id ist chunk_id, aber note_id ist im Edge vorhanden + # Wir verwenden note_id als semantische Quelle + semantic_source = note_id_from_edge + else: + # Bei note-Scope: source_id ist bereits note_id + semantic_source = source_id + + # Semantischer Schlüssel: (kind, semantic_source, target_id, target_section) + semantic_key = (kind, semantic_source, target_id, target_section) + + # WP-24c v4.4.0-DEBUG: Schnittstelle 4 - De-Duplizierung Gruppierung + # Nur für Callout-Kanten loggen + if e.get("provenance") == "explicit:callout": + logger.debug(f"DEBUG-TRACER [Dedup Grouping]: Edge zu Gruppe - Semantic_Key: {semantic_key}, Scope: {scope}, Source_ID: {source_id}, Provenance: {e.get('provenance')}, Confidence: {e.get('confidence')}, Edge_ID: {e.get('edge_id')}") + + if semantic_key not in semantic_groups: + semantic_groups[semantic_key] = [] + semantic_groups[semantic_key].append(e) + + # Schritt 2: Scope-Entscheidung pro semantischer Gruppe + # Schritt 3: ID-Zuweisung nach Scope-Entscheidung + final_edges: List[dict] = [] + + # WP-24c v4.5.9: Deterministische Sortierung der semantic_groups für konsistente Edge-Extraktion + # Verhindert Varianz zwischen Batches (33 vs 34 Kanten) + sorted_semantic_keys = sorted(semantic_groups.keys()) + + for semantic_key in sorted_semantic_keys: + group = semantic_groups[semantic_key] + # WP-24c v4.4.0-DEBUG: Schnittstelle 4 - De-Duplizierung Entscheidung + # Prüfe, ob diese Gruppe Callout-Kanten enthält + has_callouts = any(e.get("provenance") == "explicit:callout" for e in group) + + if len(group) == 1: + # Nur eine Kante: Direkt verwenden, aber ID neu berechnen mit finalem Scope + winner = group[0] + final_scope = winner.get("scope", "chunk") + final_source = winner.get("source_id", "") + kind, semantic_source, target_id, target_section = semantic_key + + # WP-24c v4.2.2: Berechne edge_id mit finalem Scope + final_edge_id = _mk_edge_id(kind, final_source, target_id, final_scope, target_section=target_section) + winner["edge_id"] = final_edge_id + + # WP-24c v4.3.0: Debug-Logik - Ausgabe für Callout-Kanten + if winner.get("provenance") == "explicit:callout": + logger.debug(f"Note [{note_id}]: Finale Callout-Kante (single): scope={final_scope}, source={final_source}, target={target_id}, section={target_section}") + + # WP-24c v4.4.0-DEBUG: Schnittstelle 4 - Single Edge + if has_callouts: + logger.debug(f"DEBUG-TRACER [Dedup]: Gruppe: {semantic_key}, Kandidaten: [Single: scope={final_scope}/provenance={winner.get('provenance')}/confidence={winner.get('confidence')}], Gewinner: {final_edge_id}, Grund: Single-Edge") + + final_edges.append(winner) + else: + # Mehrere Kanten mit gleichem semantischen Schlüssel: Scope-Entscheidung + # WP-24c v4.3.1: Präzision (Chunk) siegt über Globalität (Note) + winner = None + + # WP-24c v4.4.0-DEBUG: Schnittstelle 4 - De-Duplizierung Kandidaten-Analyse + if has_callouts: + candidates_info = [] + for e in group: + candidates_info.append(f"scope={e.get('scope')}/provenance={e.get('provenance')}/confidence={e.get('confidence')}/source={e.get('source_id')}") + logger.debug(f"DEBUG-TRACER [Dedup]: Gruppe: {semantic_key}, Kandidaten: [{', '.join(candidates_info)}]") + + # Regel 1: explicit:note_zone hat höchste Priorität (Autorität) + note_zone_candidates = [e for e in group if e.get("provenance") == "explicit:note_zone"] + if note_zone_candidates: + # Wenn mehrere note_zone: Nimm die mit höchster Confidence + winner = max(note_zone_candidates, key=lambda e: e.get("confidence", 0)) + decision_reason = "explicit:note_zone (höchste Priorität)" + else: + # Regel 2: chunk-Scope ZWINGEND bevorzugen (Präzisions-Vorteil) + # WP-24c v4.3.1: Wenn mindestens ein chunk-Kandidat existiert, muss dieser gewinnen + chunk_candidates = [e for e in group if e.get("scope") == "chunk"] + if chunk_candidates: + # Wenn mehrere chunk: Nimm die mit höchster Confidence * Priority + # Die Confidence ist hier nicht der alleinige Ausschlaggeber - chunk-Scope hat Vorrang + winner = max(chunk_candidates, key=lambda e: ( + e.get("confidence", 0) * PROVENANCE_PRIORITY.get(e.get("provenance", ""), 0.7) + )) + decision_reason = f"chunk-Scope (Präzision, {len(chunk_candidates)} chunk-Kandidaten)" + else: + # Regel 3: Fallback (nur wenn KEIN chunk-Kandidat vorhanden): Höchste Confidence * Priority + winner = max(group, key=lambda e: ( + e.get("confidence", 0) * PROVENANCE_PRIORITY.get(e.get("provenance", ""), 0.7) + )) + decision_reason = "Fallback (höchste Confidence * Priority, kein chunk-Kandidat)" + + # WP-24c v4.2.2: Berechne edge_id mit finalem Scope + final_scope = winner.get("scope", "chunk") + final_source = winner.get("source_id", "") + kind, semantic_source, target_id, target_section = semantic_key + + final_edge_id = _mk_edge_id(kind, final_source, target_id, final_scope, target_section=target_section) + winner["edge_id"] = final_edge_id + + # WP-24c v4.3.0: Debug-Logik - Ausgabe für Callout-Kanten bei Deduplizierung + if winner.get("provenance") == "explicit:callout": + logger.debug(f"Note [{note_id}]: Finale Callout-Kante (deduped, {len(group)} Kandidaten): scope={final_scope}, source={final_source}, target={target_id}, section={target_section}") + + # WP-24c v4.4.0-DEBUG: Schnittstelle 4 - Entscheidung + if has_callouts: + logger.debug(f"DEBUG-TRACER [Decision]: Gewinner: {final_edge_id}, Scope: {final_scope}, Source: {final_source}, Provenance: {winner.get('provenance')}, Confidence: {winner.get('confidence')}, Grund: {decision_reason}") + + final_edges.append(winner) + + return final_edges \ 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 fbdc51f..94c6f2a 100644 --- a/app/core/graph/graph_utils.py +++ b/app/core/graph/graph_utils.py @@ -1,9 +1,16 @@ """ FILE: app/core/graph/graph_utils.py DESCRIPTION: Basale Werkzeuge, ID-Generierung und Provenance-Konfiguration für den Graphen. - AUDIT: Erweitert um parse_link_target für sauberes Section-Splitting (WP-Fix). + AUDIT v4.0.0: + - GOLD-STANDARD v4.0.0: Strikte 4-Parameter-ID für Kanten (kind, source, target, scope). + - Eliminiert ID-Inkonsistenz zwischen Phase 1 (Autorität) und Phase 2 (Symmetrie). + - rule_id und variant werden ignoriert in der ID-Generierung (nur im Payload gespeichert). + - Fix für das "Steinzeitaxt"-Problem durch konsistente ID-Generierung. +VERSION: 4.0.0 (WP-24c: Gold-Standard Identity) +STATUS: Active """ import os +import uuid import hashlib from typing import Iterable, List, Optional, Set, Any, Tuple @@ -12,70 +19,61 @@ try: except ImportError: yaml = None -# WP-15b: Prioritäten-Ranking für die De-Duplizierung +# WP-15b: Prioritäten-Ranking für die De-Duplizierung von Kanten unterschiedlicher Herkunft PROVENANCE_PRIORITY = { "explicit:wikilink": 1.00, "inline:rel": 0.95, "callout:edge": 0.90, + "explicit:callout": 0.90, # WP-24c v4.2.7: Callout-Kanten aus candidate_pool "semantic_ai": 0.90, # Validierte KI-Kanten "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 (types.yaml) + "edge_defaults": 0.70 # Heuristik basierend auf types.yaml } +# --------------------------------------------------------------------------- +# Pfad-Auflösung (Integration der .env Umgebungsvariablen) +# --------------------------------------------------------------------------- + +def get_vocab_path() -> str: + """Liefert den Pfad zum Edge-Vokabular aus der .env oder den Default.""" + return os.getenv("MINDNET_VOCAB_PATH", "/mindnet/vault/mindnet/_system/dictionary/edge_vocabulary.md") + +def get_schema_path() -> str: + """Liefert den Pfad zum Graph-Schema aus der .env oder den Default.""" + return os.getenv("MINDNET_SCHEMA_PATH", "/mindnet/vault/mindnet/_system/dictionary/graph_schema.md") + +# --------------------------------------------------------------------------- +# ID & String Helper +# --------------------------------------------------------------------------- + def _get(d: dict, *keys, default=None): - """Sicherer Zugriff auf verschachtelte Keys.""" + """Sicherer Zugriff auf tief verschachtelte Dictionary-Keys.""" for k in keys: if isinstance(d, dict) and k in d and d[k] is not None: return d[k] return default def _dedupe_seq(seq: Iterable[str]) -> List[str]: - """Dedupliziert Strings unter Beibehaltung der Reihenfolge.""" + """Dedupliziert eine Sequenz von Strings unter Beibehaltung der Reihenfolge.""" seen: Set[str] = set() out: List[str] = [] for s in seq: if s not in seen: - seen.add(s); out.append(s) + seen.add(s) + out.append(s) return out -def _mk_edge_id(kind: str, s: str, t: str, scope: str, rule_id: Optional[str] = None, variant: Optional[str] = None) -> str: - """ - Erzeugt eine deterministische 12-Byte ID mittels BLAKE2s. - - WP-Fix: 'variant' (z.B. Section) fließt in den Hash ein, um mehrere Kanten - zum gleichen Target-Node (aber unterschiedlichen Abschnitten) zu unterscheiden. - """ - base = f"{kind}:{s}->{t}#{scope}" - if rule_id: - base += f"|{rule_id}" - if variant: - base += f"|{variant}" # <--- Hier entsteht die Eindeutigkeit für verschiedene Sections - - return hashlib.blake2s(base.encode("utf-8"), digest_size=12).hexdigest() - -def _edge(kind: str, scope: str, source_id: str, target_id: str, note_id: str, extra: Optional[dict] = None) -> dict: - """Konstruiert ein Kanten-Payload für Qdrant.""" - pl = { - "kind": kind, - "relation": kind, - "scope": scope, - "source_id": source_id, - "target_id": target_id, - "note_id": note_id, - } - if extra: pl.update(extra) - return pl - def parse_link_target(raw: str, current_note_id: Optional[str] = None) -> Tuple[str, Optional[str]]: """ - Zerlegt einen Link (z.B. 'Note#Section') in Target-ID und Section. - Behandelt Self-Links ('#Section'), indem current_note_id eingesetzt wird. + Trennt einen Obsidian-Link [[Target#Section]] in seine Bestandteile Target und Section. + Behandelt Self-Links (z.B. [[#Ziele]]), indem die aktuelle note_id eingesetzt wird. Returns: - (target_id, target_section) + Tuple (target_id, target_section) """ if not raw: return "", None @@ -84,29 +82,96 @@ def parse_link_target(raw: str, current_note_id: Optional[str] = None) -> Tuple[ target = parts[0].strip() section = parts[1].strip() if len(parts) > 1 else None - # Handle Self-Link [[#Section]] -> target wird zu current_note_id + # Spezialfall: Self-Link innerhalb derselben Datei if not target and section and current_note_id: target = current_note_id return target, section +def _mk_edge_id(kind: str, s: str, t: str, scope: str, target_section: Optional[str] = None) -> str: + """ + WP-24c v4.0.0: DER GLOBALE STANDARD für Kanten-IDs. + Erzeugt eine deterministische UUIDv5. Dies stellt sicher, dass manuelle Links + und systemgenerierte Symmetrien dieselbe Point-ID in Qdrant erhalten. + + GOLD-STANDARD v4.0.0: Die ID basiert STRICT auf vier Parametern: + f"edge:{kind}:{source}:{target}:{scope}" + + Die Parameter rule_id und variant werden IGNORIERT und fließen NICHT in die ID ein. + Sie können weiterhin im Payload gespeichert werden, haben aber keinen Einfluss auf die Identität. + + Args: + kind: Typ der Relation (z.B. 'mastered_by') + s: Kanonische ID der Quell-Note + t: Kanonische ID der Ziel-Note + scope: Granularität (Standard: 'note') + rule_id: Optionale ID der Regel (aus graph_derive_edges) - IGNORIERT in ID-Generierung + variant: Optionale Variante für multiple Links zum selben Ziel - IGNORIERT in ID-Generierung + """ + if not all([kind, s, t]): + raise ValueError(f"Incomplete data for edge ID: kind={kind}, src={s}, tgt={t}") + + # Der String enthält nun alle distinkten semantischen Merkmale + base = f"edge:{kind}:{s}:{t}:{scope}" + + # Wenn ein Link auf eine spezifische Sektion zeigt, ist es eine andere Relation + if target_section: + base += f":{target_section}" + + return str(uuid.uuid5(uuid.NAMESPACE_URL, base)) + +def _edge(kind: str, scope: str, source_id: str, target_id: str, note_id: str, extra: Optional[dict] = None) -> dict: + """ + Konstruiert ein standardisiertes Kanten-Payload für Qdrant. + Wird von graph_derive_edges.py benötigt. + """ + pl = { + "kind": kind, + "relation": kind, + "scope": scope, + "source_id": source_id, + "target_id": target_id, + "note_id": note_id, + "virtual": False # Standardmäßig explizit, solange nicht anders in Phase 2 gesetzt + } + if extra: + pl.update(extra) + return pl + +# --------------------------------------------------------------------------- +# Registry Operations +# --------------------------------------------------------------------------- + def load_types_registry() -> dict: - """Lädt die YAML-Registry.""" + """ + Lädt die zentrale YAML-Registry (types.yaml). + Pfad wird über die Umgebungsvariable MINDNET_TYPES_FILE gesteuert. + """ p = os.getenv("MINDNET_TYPES_FILE", "./config/types.yaml") - if not os.path.isfile(p) or yaml is None: return {} + if not os.path.isfile(p) or yaml is None: + return {} try: - with open(p, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {} - except Exception: return {} + with open(p, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + return data if data is not None else {} + except Exception: + return {} def get_edge_defaults_for(note_type: Optional[str], reg: dict) -> List[str]: - """Ermittelt Standard-Kanten für einen Typ.""" + """ + Ermittelt die konfigurierten Standard-Kanten für einen Note-Typ. + Greift bei Bedarf auf die globalen Defaults in der Registry zurück. + """ types_map = reg.get("types", reg) if isinstance(reg, dict) else {} if note_type and isinstance(types_map, dict): - t = types_map.get(note_type) - if isinstance(t, dict) and isinstance(t.get("edge_defaults"), list): - return [str(x) for x in t["edge_defaults"] if isinstance(x, str)] + t_cfg = types_map.get(note_type) + if isinstance(t_cfg, dict) and isinstance(t_cfg.get("edge_defaults"), list): + return [str(x) for x in t_cfg["edge_defaults"]] + + # Fallback auf globale Defaults for key in ("defaults", "default", "global"): v = reg.get(key) if isinstance(v, dict) and isinstance(v.get("edge_defaults"), list): return [str(x) for x in v["edge_defaults"] if isinstance(x, str)] + return [] \ No newline at end of file diff --git a/app/core/ingestion/ingestion_chunk_payload.py b/app/core/ingestion/ingestion_chunk_payload.py index 1c1ac51..e29f544 100644 --- a/app/core/ingestion/ingestion_chunk_payload.py +++ b/app/core/ingestion/ingestion_chunk_payload.py @@ -2,15 +2,19 @@ FILE: app/core/ingestion/ingestion_chunk_payload.py DESCRIPTION: Baut das JSON-Objekt für 'mindnet_chunks'. Fix v2.4.3: Integration der zentralen Registry (WP-14) für konsistente Defaults. -VERSION: 2.4.3 + WP-24c v4.3.0: candidate_pool wird explizit übernommen für Chunk-Attribution. +VERSION: 2.4.4 (WP-24c v4.3.0) STATUS: Active """ from __future__ import annotations from typing import Any, Dict, List, Optional +import logging # ENTSCHEIDENDER FIX: Import der neutralen Registry-Logik zur Vermeidung von Circular Imports from app.core.registry import load_type_registry +logger = logging.getLogger(__name__) + # --------------------------------------------------------------------------- # Resolution Helpers (Audited) # --------------------------------------------------------------------------- @@ -85,6 +89,8 @@ def make_chunk_payloads(note: Dict[str, Any], note_path: str, chunks_from_chunke prev_id = getattr(ch, "neighbors_prev", None) if not is_dict else ch.get("neighbors_prev") next_id = getattr(ch, "neighbors_next", None) if not is_dict else ch.get("neighbors_next") section = getattr(ch, "section_title", "") if not is_dict else ch.get("section", "") + # WP-24c v4.3.0: candidate_pool muss erhalten bleiben für Chunk-Attribution + candidate_pool = getattr(ch, "candidate_pool", []) if not is_dict else ch.get("candidate_pool", []) pl: Dict[str, Any] = { "note_id": nid or fm.get("id"), @@ -102,12 +108,23 @@ def make_chunk_payloads(note: Dict[str, Any], note_path: str, chunks_from_chunke "path": note_path, "source_path": kwargs.get("file_path") or note_path, "retriever_weight": rw, - "chunk_profile": cp + "chunk_profile": cp, + "candidate_pool": candidate_pool # WP-24c v4.3.0: Kritisch für Chunk-Attribution } # Audit: Cleanup Pop (Vermeidung von redundanten Alias-Feldern) for alias in ("chunk_num", "Chunk_Number"): pl.pop(alias, None) + + # WP-24c v4.4.0-DEBUG: Schnittstelle 2 - Transfer + # Log-Output unmittelbar bevor das Dictionary zurückgegeben wird + pool_size = len(candidate_pool) if candidate_pool else 0 + pool_content = candidate_pool if candidate_pool else [] + explicit_callout_in_pool = [c for c in pool_content if isinstance(c, dict) and c.get("provenance") == "explicit:callout"] + logger.debug(f"DEBUG-TRACER [Payload]: Chunk ID: {cid}, Index: {index}, Pool-Size: {pool_size}, Pool-Inhalt: {pool_content}, Explicit-Callout-Count: {len(explicit_callout_in_pool)}, Has_Candidate_Pool_Key: {'candidate_pool' in pl}") + if explicit_callout_in_pool: + for ec in explicit_callout_in_pool: + logger.debug(f"DEBUG-TRACER [Payload]: Explicit-Callout Detail - Kind: {ec.get('kind')}, To: {ec.get('to')}, Provenance: {ec.get('provenance')}") out.append(pl) diff --git a/app/core/ingestion/ingestion_db.py b/app/core/ingestion/ingestion_db.py index 64cd57f..0ca707f 100644 --- a/app/core/ingestion/ingestion_db.py +++ b/app/core/ingestion/ingestion_db.py @@ -2,38 +2,115 @@ FILE: app/core/ingestion/ingestion_db.py DESCRIPTION: Datenbank-Schnittstelle für Note-Metadaten und Artefakt-Prüfung. WP-14: Umstellung auf zentrale database-Infrastruktur. + WP-24c: Integration der Authority-Prüfung für Point-IDs. + Ermöglicht dem Prozessor die Unterscheidung zwischen + manueller Nutzer-Autorität und virtuellen Symmetrien. +VERSION: 2.2.0 (WP-24c: Authority Lookup Integration) +STATUS: Active """ -from typing import Optional, Tuple +import logging +from typing import Optional, Tuple, List from qdrant_client import QdrantClient from qdrant_client.http import models as rest # Import der modularisierten Namen-Logik zur Sicherstellung der Konsistenz from app.core.database import collection_names +logger = logging.getLogger(__name__) + def fetch_note_payload(client: QdrantClient, prefix: str, note_id: str) -> Optional[dict]: - """Holt die Metadaten einer Note aus Qdrant via Scroll.""" + """ + Holt die Metadaten einer Note aus Qdrant via Scroll-API. + Wird primär für die Change-Detection (Hash-Vergleich) genutzt. + """ notes_col, _, _ = collection_names(prefix) try: - f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))]) - pts, _ = client.scroll(collection_name=notes_col, scroll_filter=f, limit=1, with_payload=True) + f = rest.Filter(must=[ + rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id)) + ]) + pts, _ = client.scroll( + collection_name=notes_col, + scroll_filter=f, + limit=1, + with_payload=True + ) return pts[0].payload if pts else None - except: return None + except Exception as e: + logger.debug(f"Note {note_id} not found or error during fetch: {e}") + return None def artifacts_missing(client: QdrantClient, prefix: str, note_id: str) -> Tuple[bool, bool]: - """Prüft Qdrant aktiv auf vorhandene Chunks und Edges.""" + """ + Prüft Qdrant aktiv auf vorhandene Chunks und Edges für eine Note. + Gibt (chunks_missing, edges_missing) als Boolean-Tupel zurück. + """ _, chunks_col, edges_col = collection_names(prefix) try: - f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))]) + # Filter für die note_id Suche + f = rest.Filter(must=[ + rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id)) + ]) c_pts, _ = client.scroll(collection_name=chunks_col, scroll_filter=f, limit=1) e_pts, _ = client.scroll(collection_name=edges_col, scroll_filter=f, limit=1) return (not bool(c_pts)), (not bool(e_pts)) - except: return True, True + except Exception as e: + logger.error(f"Error checking artifacts for {note_id}: {e}") + return True, True + +def is_explicit_edge_present(client: QdrantClient, prefix: str, edge_id: str) -> bool: + """ + WP-24c: Prüft via Point-ID, ob bereits eine explizite Kante existiert. + Wird vom IngestionProcessor in Phase 2 genutzt, um das Überschreiben + von manuellem Wissen durch virtuelle Symmetrie-Kanten zu verhindern. + + Args: + edge_id: Die deterministisch berechnete UUID der Kante. + Returns: + True, wenn eine physische Kante (virtual=False) existiert. + """ + if not edge_id: + return False + + _, _, edges_col = collection_names(prefix) + try: + # retrieve ist die effizienteste Methode für den Zugriff via ID + res = client.retrieve( + collection_name=edges_col, + ids=[edge_id], + with_payload=True + ) + + if res and len(res) > 0: + # Wir prüfen das 'virtual' Flag im Payload + is_virtual = res[0].payload.get("virtual", False) + if not is_virtual: + return True # Es ist eine explizite Nutzer-Kante + + return False + except Exception as e: + logger.debug(f"Authority check failed for ID {edge_id}: {e}") + return False def purge_artifacts(client: QdrantClient, prefix: str, note_id: str): - """Löscht verwaiste Chunks/Edges vor einem Re-Import.""" + """ + Löscht verwaiste Chunks und Edges einer Note vor einem Re-Import. + Stellt sicher, dass keine Duplikate bei Inhaltsänderungen entstehen. + """ _, chunks_col, edges_col = collection_names(prefix) - f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))]) - # Iteration über die nun zentral verwalteten Collection-Namen - for col in [chunks_col, edges_col]: - try: client.delete(collection_name=col, points_selector=rest.FilterSelector(filter=f)) - except: pass \ No newline at end of file + try: + f = rest.Filter(must=[ + rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id)) + ]) + # Chunks löschen + client.delete( + collection_name=chunks_col, + points_selector=rest.FilterSelector(filter=f) + ) + # Edges löschen + client.delete( + collection_name=edges_col, + points_selector=rest.FilterSelector(filter=f) + ) + logger.info(f"🧹 [PURGE] Local artifacts for '{note_id}' cleared.") + except Exception as e: + logger.error(f"❌ [PURGE ERROR] Failed to clear artifacts for {note_id}: {e}") \ No newline at end of file diff --git a/app/core/ingestion/ingestion_note_payload.py b/app/core/ingestion/ingestion_note_payload.py index 5d30707..5c09c8f 100644 --- a/app/core/ingestion/ingestion_note_payload.py +++ b/app/core/ingestion/ingestion_note_payload.py @@ -1,10 +1,10 @@ """ FILE: app/core/ingestion/ingestion_note_payload.py DESCRIPTION: Baut das JSON-Objekt für mindnet_notes. -FEATURES: - - Multi-Hash (body/full) für flexible Change Detection. - - Fix v2.4.5: Präzise Hash-Logik für Profil-Änderungen. - - Integration der zentralen Registry (WP-14). + WP-14: Integration der zentralen Registry. + WP-24c: Dynamische Ermittlung von edge_defaults aus dem Graph-Schema. +VERSION: 2.5.0 (WP-24c: Dynamic Topology Integration) +STATUS: Active """ from __future__ import annotations from typing import Any, Dict, Tuple, Optional @@ -15,6 +15,8 @@ import hashlib # Import der zentralen Registry-Logik from app.core.registry import load_type_registry +# WP-24c: Zugriff auf das dynamische Graph-Schema +from app.services.edge_registry import registry as edge_registry # --------------------------------------------------------------------------- # Helper @@ -46,15 +48,14 @@ def _compute_hash(content: str) -> str: def _get_hash_source_content(n: Dict[str, Any], mode: str) -> str: """ Generiert den Hash-Input-String basierend auf Body oder Metadaten. - Fix: Inkludiert nun alle entscheidungsrelevanten Profil-Parameter. + Inkludiert alle entscheidungsrelevanten Profil-Parameter. """ body = str(n.get("body") or "").strip() if mode == "body": return body if mode == "full": fm = n.get("frontmatter") or {} meta_parts = [] - # Wir inkludieren alle Felder, die das Chunking oder Retrieval beeinflussen - # Jede Änderung hier führt nun zwingend zu einem neuen Full-Hash + # Alle Felder, die das Chunking oder Retrieval beeinflussen keys = [ "title", "type", "status", "tags", "chunking_profile", "chunk_profile", @@ -87,7 +88,7 @@ def _cfg_defaults(reg: dict) -> dict: def make_note_payload(note: Any, *args, **kwargs) -> Dict[str, Any]: """ Baut das Note-Payload inklusive Multi-Hash und Audit-Validierung. - WP-14: Nutzt die zentrale Registry für alle Fallbacks. + WP-24c: Nutzt die EdgeRegistry zur dynamischen Auflösung von Typical Edges. """ n = _as_dict(note) @@ -120,10 +121,16 @@ def make_note_payload(note: Any, *args, **kwargs) -> Dict[str, Any]: if chunk_profile is None: chunk_profile = ingest_cfg.get("default_chunk_profile", cfg_def.get("chunking_profile", "sliding_standard")) - # --- edge_defaults Audit --- + # --- WP-24c: edge_defaults Dynamisierung --- + # 1. Priorität: Manuelle Definition im Frontmatter edge_defaults = fm.get("edge_defaults") + + # 2. Priorität: Dynamische Abfrage der 'Typical Edges' aus dem Graph-Schema if edge_defaults is None: - edge_defaults = cfg_type.get("edge_defaults", cfg_def.get("edge_defaults", [])) + topology = edge_registry.get_topology_info(note_type, "any") + edge_defaults = topology.get("typical", []) + + # 3. Fallback: Leere Liste, falls kein Schema-Eintrag existiert edge_defaults = _ensure_list(edge_defaults) # --- Basis-Metadaten --- diff --git a/app/core/ingestion/ingestion_processor.py b/app/core/ingestion/ingestion_processor.py index 8ca6021..e657948 100644 --- a/app/core/ingestion/ingestion_processor.py +++ b/app/core/ingestion/ingestion_processor.py @@ -4,13 +4,19 @@ DESCRIPTION: Der zentrale IngestionService (Orchestrator). WP-25a: Integration der Mixture of Experts (MoE) Architektur. WP-15b: Two-Pass Workflow mit globalem Kontext-Cache. WP-20/22: Cloud-Resilienz und Content-Lifecycle integriert. - AUDIT v2.14.0: Synchronisierung der Profil-Auflösung mit MoE-Experten. -VERSION: 2.14.0 (WP-25a: MoE & Profile Support) + AUDIT v4.2.4: + - GOLD-STANDARD v4.2.4: Hash-basierte Change-Detection (MINDNET_CHANGE_DETECTION_MODE). + - Wiederherstellung des iterativen Abgleichs basierend auf Inhalts-Hashes. + - Phase 2 verwendet exakt dieselbe ID-Generierung wie Phase 1 (inkl. target_section). + - Authority-Check in Phase 2 prüft mit konsistenter ID-Generierung. + - Eliminiert Duplikate durch inkonsistente ID-Generierung (Steinzeitaxt-Problem). +VERSION: 4.2.4 (WP-24c: Hash-Integrität) STATUS: Active """ import logging import asyncio import os +import re from typing import Dict, List, Optional, Tuple, Any # Core Module Imports @@ -19,10 +25,13 @@ from app.core.parser import ( validate_required_frontmatter, NoteContext ) from app.core.chunking import assemble_chunks +# WP-24c: Import der zentralen Identitäts-Logik +from app.core.graph.graph_utils import _mk_edge_id -# MODULARISIERUNG: Neue Import-Pfade für die Datenbank-Ebene +# Datenbank-Ebene (Modularisierte database-Infrastruktur) from app.core.database.qdrant import QdrantConfig, get_client, ensure_collections, ensure_payload_indexes from app.core.database.qdrant_points import points_for_chunks, points_for_note, points_for_edges, upsert_batch +from qdrant_client.http import models as rest # Services from app.services.embeddings_client import EmbeddingsClient @@ -31,7 +40,7 @@ from app.services.llm_service import LLMService # Package-Interne Imports (Refactoring WP-14) from .ingestion_utils import load_type_registry, resolve_note_type, get_chunk_config_by_profile -from .ingestion_db import fetch_note_payload, artifacts_missing, purge_artifacts +from .ingestion_db import fetch_note_payload, artifacts_missing, purge_artifacts, is_explicit_edge_present from .ingestion_validation import validate_edge_candidate from .ingestion_note_payload import make_note_payload from .ingestion_chunk_payload import make_chunk_payloads @@ -50,9 +59,13 @@ class IngestionService: from app.config import get_settings self.settings = get_settings() + # --- LOGGING CLEANUP --- + # Unterdrückt Bibliotheks-Lärm, erhält aber inhaltliche Service-Logs + for lib in ["httpx", "httpcore", "qdrant_client", "urllib3", "openai"]: + logging.getLogger(lib).setLevel(logging.WARNING) + self.prefix = collection_prefix or self.settings.COLLECTION_PREFIX self.cfg = QdrantConfig.from_env() - # Synchronisierung der Konfiguration mit dem Instanz-Präfix self.cfg.prefix = self.prefix self.client = get_client(self.cfg) @@ -64,182 +77,576 @@ class IngestionService: embed_cfg = self.llm.profiles.get("embedding_expert", {}) self.dim = embed_cfg.get("dimensions") or self.settings.VECTOR_SIZE - # Festlegen, welcher Hash für die Change-Detection maßgeblich ist self.active_hash_mode = self.settings.CHANGE_DETECTION_MODE - self.batch_cache: Dict[str, NoteContext] = {} # WP-15b LocalBatchCache + + # WP-15b: Kontext-Gedächtnis für ID-Auflösung (Globaler Cache) + self.batch_cache: Dict[str, NoteContext] = {} + + # WP-24c: Puffer für Phase 2 (Symmetrie-Injektion am Ende des gesamten Imports) + self.symmetry_buffer: List[Dict[str, Any]] = [] try: - # Aufruf der modularisierten Schema-Logik ensure_collections(self.client, self.prefix, self.dim) ensure_payload_indexes(self.client, self.prefix) except Exception as e: logger.warning(f"DB initialization warning: {e}") - async def run_batch(self, file_paths: List[str], vault_root: str) -> List[Dict[str, Any]]: + def _log_id_collision( + self, + note_id: str, + existing_path: str, + conflicting_path: str, + action: str = "ERROR" + ) -> None: """ - WP-15b: Implementiert den Two-Pass Ingestion Workflow. - Pass 1: Pre-Scan füllt den Context-Cache (3-Wege-Indexierung). - Pass 2: Verarbeitung nutzt den Cache für die semantische Prüfung. + WP-24c v4.5.10: Loggt ID-Kollisionen in eine dedizierte Log-Datei. + + Schreibt alle ID-Kollisionen in logs/id_collisions.log für manuelle Analyse. + Format: JSONL (eine Kollision pro Zeile) mit allen relevanten Metadaten. + + Args: + note_id: Die doppelte note_id + existing_path: Pfad der bereits vorhandenen Datei + conflicting_path: Pfad der kollidierenden Datei + action: Gewählte Aktion (z.B. "ERROR", "SKIPPED") """ - logger.info(f"🔍 [Pass 1] Pre-Scanning {len(file_paths)} files for Context Cache...") + import json + from datetime import datetime + + # Erstelle Log-Verzeichnis falls nicht vorhanden + log_dir = "logs" + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + log_file = os.path.join(log_dir, "id_collisions.log") + + # Erstelle Log-Eintrag mit allen relevanten Informationen + log_entry = { + "timestamp": datetime.now().isoformat(), + "note_id": note_id, + "existing_file": { + "path": existing_path, + "filename": os.path.basename(existing_path) if existing_path else None + }, + "conflicting_file": { + "path": conflicting_path, + "filename": os.path.basename(conflicting_path) if conflicting_path else None + }, + "action": action, + "collection_prefix": self.prefix + } + + # Schreibe als JSONL (eine Zeile pro Eintrag) + try: + with open(log_file, "a", encoding="utf-8") as f: + f.write(json.dumps(log_entry, ensure_ascii=False) + "\n") + except Exception as e: + logger.warning(f"⚠️ Konnte ID-Kollision nicht in Log-Datei schreiben: {e}") + + def _persist_rejected_edges(self, note_id: str, rejected_edges: List[Dict[str, Any]]) -> None: + """ + WP-24c v4.5.9: Persistiert abgelehnte Kanten für Audit-Zwecke. + + Schreibt rejected_edges in eine JSONL-Datei im _system Ordner oder logs/rejected_edges.log. + Dies ermöglicht die Analyse der Ablehnungsgründe und Verbesserung der Validierungs-Logik. + + Args: + note_id: ID der Note, zu der die abgelehnten Kanten gehören + rejected_edges: Liste von abgelehnten Edge-Dicts + """ + if not rejected_edges: + return + + import json + import os + from datetime import datetime + + # WP-24c v4.5.9: Erstelle Log-Verzeichnis falls nicht vorhanden + log_dir = "logs" + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + log_file = os.path.join(log_dir, "rejected_edges.log") + + # WP-24c v4.5.9: Schreibe als JSONL (eine Kante pro Zeile) + try: + with open(log_file, "a", encoding="utf-8") as f: + for edge in rejected_edges: + log_entry = { + "timestamp": datetime.now().isoformat(), + "note_id": note_id, + "edge": { + "kind": edge.get("kind", "unknown"), + "source_id": edge.get("source_id", "unknown"), + "target_id": edge.get("target_id") or edge.get("to", "unknown"), + "scope": edge.get("scope", "unknown"), + "provenance": edge.get("provenance", "unknown"), + "rule_id": edge.get("rule_id", "unknown"), + "confidence": edge.get("confidence", 0.0), + "target_section": edge.get("target_section") + } + } + f.write(json.dumps(log_entry, ensure_ascii=False) + "\n") + + logger.debug(f"📝 [AUDIT] {len(rejected_edges)} abgelehnte Kanten für '{note_id}' in {log_file} gespeichert") + except Exception as e: + logger.error(f"❌ [AUDIT] Fehler beim Speichern der rejected_edges: {e}") + + def _is_valid_id(self, text: Optional[str]) -> bool: + """WP-24c: Prüft IDs auf fachliche Validität (Ghost-ID Schutz).""" + if not text or not isinstance(text, str) or len(text.strip()) < 2: + return False + blacklisted = {"none", "unknown", "insight", "source", "task", "project", "person", "concept"} + if text.lower().strip() in blacklisted: + return False + return True + + async def run_batch(self, file_paths: List[str], vault_root: str) -> Dict[str, Any]: + """ + WP-15b: Phase 1 des Two-Pass Workflows. + Verarbeitet Batches und schreibt NUR Nutzer-Autorität (explizite Kanten). + """ + self.batch_cache.clear() + logger.info(f"--- 🔍 START BATCH PHASE 1 ({len(file_paths)} Dateien) ---") + + # 1. Schritt: Pre-Scan (Context-Cache füllen) for path in file_paths: try: - # Übergabe der Registry für dynamische Scan-Tiefe ctx = pre_scan_markdown(path, registry=self.registry) if ctx: - # Mehrfache Indizierung für robusten Look-up (ID, Titel, Dateiname) self.batch_cache[ctx.note_id] = ctx self.batch_cache[ctx.title] = ctx - fname = os.path.splitext(os.path.basename(path))[0] - self.batch_cache[fname] = ctx + self.batch_cache[os.path.splitext(os.path.basename(path))[0]] = ctx except Exception as e: - logger.warning(f"⚠️ Pre-scan failed for {path}: {e}") + logger.warning(f" ⚠️ Pre-scan fehlgeschlagen für {path}: {e}") - logger.info(f"🚀 [Pass 2] Semantic Processing of {len(file_paths)} files...") - return [await self.process_file(p, vault_root, apply=True, purge_before=True) for p in file_paths] + # 2. Schritt: Batch Processing (Authority Only) + processed_count = 0 + success_count = 0 + for p in file_paths: + processed_count += 1 + res = await self.process_file(p, vault_root, apply=True, purge_before=True) + if res.get("status") == "success": + success_count += 1 + + logger.info(f"--- ✅ Batch Phase 1 abgeschlossen ({success_count}/{processed_count}) ---") + return { + "status": "success", + "processed": processed_count, + "success": success_count, + "buffered_symmetries": len(self.symmetry_buffer) + } + + async def commit_vault_symmetries(self) -> Dict[str, Any]: + """ + WP-24c: Führt PHASE 2 (Globale Symmetrie-Injektion) aus. + Wird am Ende des gesamten Imports aufgerufen. + """ + if not self.symmetry_buffer: + return {"status": "skipped", "reason": "buffer_empty"} + + logger.info(f"🔄 PHASE 2: Validiere {len(self.symmetry_buffer)} Symmetrien gegen Live-DB...") + final_virtuals = [] + for v_edge in self.symmetry_buffer: + # WP-24c v4.1.0: Korrekte Extraktion der Identitäts-Parameter + src = v_edge.get("source_id") or v_edge.get("note_id") # source_id hat Priorität + tgt = v_edge.get("target_id") + kind = v_edge.get("kind") + scope = v_edge.get("scope", "note") + target_section = v_edge.get("target_section") # WP-24c v4.1.0: target_section berücksichtigen + + if not all([src, tgt, kind]): + continue + + # WP-24c v4.1.0: Nutzung der zentralisierten ID-Logik aus graph_utils + # GOLD-STANDARD v4.1.0: ID-Generierung muss absolut synchron zu Phase 1 sein + # - Wenn target_section vorhanden, muss es in die ID einfließen + # - Dies stellt sicher, dass der Authority-Check korrekt funktioniert + try: + v_id = _mk_edge_id(kind, src, tgt, scope, target_section=target_section) + except ValueError: + continue + + # AUTHORITY-CHECK: Nur schreiben, wenn keine manuelle Kante existiert + # Prüft mit exakt derselben ID, die in Phase 1 verwendet wurde (inkl. target_section) + if not is_explicit_edge_present(self.client, self.prefix, v_id): + final_virtuals.append(v_edge) + section_info = f" (section: {target_section})" if target_section else "" + logger.info(f" 🔄 [SYMMETRY] Add inverse: {src} --({kind})--> {tgt}{section_info}") + else: + logger.info(f" 🛡️ [PROTECTED] Manuelle Kante gefunden. Symmetrie für {kind} unterdrückt.") + + if final_virtuals: + col, pts = points_for_edges(self.prefix, final_virtuals) + upsert_batch(self.client, col, pts, wait=True) + + count = len(final_virtuals) + self.symmetry_buffer.clear() + return {"status": "success", "added": count} async def process_file(self, file_path: str, vault_root: str, **kwargs) -> Dict[str, Any]: - """Transformiert eine Markdown-Datei in den Graphen.""" + """ + Transformiert eine Markdown-Datei (Phase 1). + Schreibt Notes/Chunks/Explicit Edges sofort. + """ apply = kwargs.get("apply", False) force_replace = kwargs.get("force_replace", False) purge_before = kwargs.get("purge_before", False) - note_scope_refs = kwargs.get("note_scope_refs", False) - hash_source = kwargs.get("hash_source", "parsed") - hash_normalize = kwargs.get("hash_normalize", "canonical") result = {"path": file_path, "status": "skipped", "changed": False, "error": None} - # 1. Parse & Lifecycle Gate try: - parsed = read_markdown(file_path) + # Ordner-Filter (.trash / .obsidian) + if ".trash" in file_path or any(part.startswith('.') for part in file_path.split(os.sep)): + return {**result, "status": "skipped", "reason": "ignored_folder"} + + # WP-24c v4.5.9: Path-Normalization für konsistente Hash-Prüfung + # Normalisiere file_path zu absolutem Pfad für konsistente Verarbeitung + normalized_file_path = os.path.abspath(file_path) if not os.path.isabs(file_path) else file_path + + parsed = read_markdown(normalized_file_path) if not parsed: return {**result, "error": "Empty file"} fm = normalize_frontmatter(parsed.frontmatter) validate_required_frontmatter(fm) - except Exception as e: - return {**result, "error": f"Validation failed: {str(e)}"} - - # Dynamischer Lifecycle-Filter aus der Registry (WP-14) - ingest_cfg = self.registry.get("ingestion_settings", {}) - ignore_list = ingest_cfg.get("ignore_statuses", ["system", "template", "archive", "hidden"]) - - current_status = fm.get("status", "draft").lower().strip() - if current_status in ignore_list: - return {**result, "status": "skipped", "reason": "lifecycle_filter"} - - # 2. Payload & Change Detection (Multi-Hash) - note_type = resolve_note_type(self.registry, fm.get("type")) - note_pl = make_note_payload( - parsed, vault_root=vault_root, file_path=file_path, - hash_source=hash_source, hash_normalize=hash_normalize, - types_cfg=self.registry - ) - note_id = note_pl["note_id"] - - # Abgleich mit der Datenbank (Qdrant) - old_payload = None if force_replace else fetch_note_payload(self.client, self.prefix, note_id) - - # Prüfung gegen den konfigurierten Hash-Modus (body vs. full) - check_key = f"{self.active_hash_mode}:{hash_source}:{hash_normalize}" - old_hash = (old_payload or {}).get("hashes", {}).get(check_key) - new_hash = note_pl.get("hashes", {}).get(check_key) - - # Check ob Chunks oder Kanten in der DB fehlen (Reparatur-Modus) - c_miss, e_miss = artifacts_missing(self.client, self.prefix, note_id) - - # Wenn Hash identisch und Artefakte vorhanden -> Skip - if not (force_replace or not old_payload or old_hash != new_hash or c_miss or e_miss): - return {**result, "status": "unchanged", "note_id": note_id} - - if not apply: - return {**result, "status": "dry-run", "changed": True, "note_id": note_id} - - # 3. Deep Processing (Chunking, Validation, Embedding) - try: - body_text = getattr(parsed, "body", "") or "" - edge_registry.ensure_latest() - # Profil-Auflösung via Registry + note_pl = make_note_payload(parsed, vault_root=vault_root, file_path=normalized_file_path, types_cfg=self.registry) + note_id = note_pl.get("note_id") + + if not note_id: + return {**result, "status": "error", "error": "missing_id"} + + logger.info(f"📄 Bearbeite: '{note_id}' | Pfad: {normalized_file_path} | Title: {note_pl.get('title', 'N/A')}") + + # WP-24c v4.5.9: Strikte Change Detection (Hash-basierte Inhaltsprüfung) + # Prüft Hash VOR der Verarbeitung, um redundante Ingestion zu vermeiden + old_payload = None if force_replace else fetch_note_payload(self.client, self.prefix, note_id) + + # WP-24c v4.5.10: Prüfe auf ID-Kollisionen (zwei Dateien mit derselben note_id) + if old_payload and not force_replace: + old_path = old_payload.get("path", "") + if old_path and old_path != normalized_file_path: + # ID-Kollision erkannt: Zwei verschiedene Dateien haben dieselbe note_id + # Logge die Kollision in dedizierte Log-Datei + self._log_id_collision( + note_id=note_id, + existing_path=old_path, + conflicting_path=normalized_file_path, + action="ERROR" + ) + logger.error( + f"❌ [ID-KOLLISION] Kritischer Fehler: Die note_id '{note_id}' wird bereits von einer anderen Datei verwendet!\n" + f" Bereits vorhanden: '{old_path}'\n" + f" Konflikt mit: '{normalized_file_path}'\n" + f" Lösung: Bitte ändern Sie die 'id' im Frontmatter einer der beiden Dateien, um eine eindeutige ID zu gewährleisten.\n" + f" Details wurden in logs/id_collisions.log gespeichert." + ) + return {**result, "status": "error", "error": "id_collision", "note_id": note_id, "existing_path": old_path, "conflicting_path": normalized_file_path} + + logger.debug(f"🔍 [CHANGE-DETECTION] Start für '{note_id}': force_replace={force_replace}, old_payload={old_payload is not None}") + + content_changed = True + hash_match = False + if old_payload and not force_replace: + # Nutzt die über MINDNET_CHANGE_DETECTION_MODE gesteuerte Genauigkeit + # Mapping: 'full' -> 'full:parsed:canonical', 'body' -> 'body:parsed:canonical' + h_key = f"{self.active_hash_mode or 'full'}:parsed:canonical" + new_h = note_pl.get("hashes", {}).get(h_key) + old_h = old_payload.get("hashes", {}).get(h_key) + + # WP-24c v4.5.9-DEBUG: Detaillierte Hash-Diagnose (INFO-Level) + logger.info(f"🔍 [CHANGE-DETECTION] Hash-Vergleich für '{note_id}':") + logger.debug(f" -> Hash-Key: '{h_key}'") + logger.debug(f" -> Active Hash-Mode: '{self.active_hash_mode or 'full'}'") + logger.debug(f" -> New Hash vorhanden: {bool(new_h)}") + logger.debug(f" -> Old Hash vorhanden: {bool(old_h)}") + if new_h: + logger.debug(f" -> New Hash (erste 32 Zeichen): {new_h[:32]}...") + if old_h: + logger.debug(f" -> Old Hash (erste 32 Zeichen): {old_h[:32]}...") + logger.debug(f" -> Verfügbare Hash-Keys in new: {list(note_pl.get('hashes', {}).keys())}") + logger.debug(f" -> Verfügbare Hash-Keys in old: {list(old_payload.get('hashes', {}).keys())}") + + if new_h and old_h: + hash_match = (new_h == old_h) + if hash_match: + content_changed = False + logger.info(f"🔍 [CHANGE-DETECTION] ✅ Hash identisch für '{note_id}': {h_key} = {new_h[:16]}...") + else: + logger.warning(f"🔍 [CHANGE-DETECTION] ❌ Hash geändert für '{note_id}': alt={old_h[:16]}..., neu={new_h[:16]}...") + # Finde erste unterschiedliche Position + diff_pos = next((i for i, (a, b) in enumerate(zip(new_h, old_h)) if a != b), None) + if diff_pos is not None: + logger.debug(f" -> Hash-Unterschied: Erste unterschiedliche Position: {diff_pos}") + else: + logger.debug(f" -> Hash-Unterschied: Längen unterschiedlich (new={len(new_h)}, old={len(old_h)})") + + # WP-24c v4.5.10: Logge Hash-Input für Diagnose (DEBUG-Level) + # WICHTIG: _get_hash_source_content benötigt ein Dictionary, nicht das ParsedNote-Objekt! + from app.core.ingestion.ingestion_note_payload import _get_hash_source_content, _as_dict + hash_mode = self.active_hash_mode or 'full' + # Konvertiere parsed zu Dictionary für _get_hash_source_content + parsed_dict = _as_dict(parsed) + hash_input = _get_hash_source_content(parsed_dict, hash_mode) + logger.debug(f" -> Hash-Input (erste 200 Zeichen): {hash_input[:200]}...") + logger.debug(f" -> Hash-Input Länge: {len(hash_input)}") + + # WP-24c v4.5.10: Vergleiche auch Body-Länge und Frontmatter (DEBUG-Level) + # Verwende parsed.body statt note_pl.get("body") + new_body = str(getattr(parsed, "body", "") or "").strip() + old_body = str(old_payload.get("body", "")).strip() if old_payload else "" + logger.debug(f" -> Body-Länge: new={len(new_body)}, old={len(old_body)}") + if len(new_body) != len(old_body): + logger.debug(f" -> ⚠️ Body-Länge unterschiedlich! Mögliche Ursache: Parsing-Unterschiede") + + # Verwende parsed.frontmatter statt note_pl.get("frontmatter") + new_fm = getattr(parsed, "frontmatter", {}) or {} + old_fm = old_payload.get("frontmatter", {}) if old_payload else {} + logger.debug(f" -> Frontmatter-Keys: new={sorted(new_fm.keys())}, old={sorted(old_fm.keys())}") + # Prüfe relevante Frontmatter-Felder + relevant_keys = ["title", "type", "status", "tags", "chunking_profile", "chunk_profile", "retriever_weight", "split_level", "strict_heading_split"] + for key in relevant_keys: + new_val = new_fm.get(key) if isinstance(new_fm, dict) else getattr(new_fm, key, None) + old_val = old_fm.get(key) if isinstance(old_fm, dict) else None + if new_val != old_val: + logger.debug(f" -> ⚠️ Frontmatter '{key}' unterschiedlich: new={new_val}, old={old_val}") + else: + # WP-24c v4.5.10: Wenn Hash fehlt, als geändert behandeln (Sicherheit) + logger.debug(f"⚠️ [CHANGE-DETECTION] Hash fehlt für '{note_id}': new_h={bool(new_h)}, old_h={bool(old_h)}") + logger.debug(f" -> Grund: Hash wird als 'geändert' behandelt, da Hash-Werte fehlen") + else: + if force_replace: + logger.debug(f"🔍 [CHANGE-DETECTION] '{note_id}': force_replace=True -> überspringe Hash-Check") + elif not old_payload: + logger.debug(f"🔍 [CHANGE-DETECTION] '{note_id}': ⚠️ Keine alte Payload gefunden -> erste Verarbeitung oder gelöscht") + + # WP-24c v4.5.9: Strikte Logik - überspringe komplett wenn Hash identisch + # WICHTIG: Artifact-Check NACH Hash-Check, da purge_before die Artefakte löschen kann + # Wenn Hash identisch ist, sind die Artefakte entweder vorhanden oder werden gerade neu geschrieben + if not force_replace and hash_match and old_payload: + # WP-24c v4.5.9: Hash identisch -> überspringe komplett (auch wenn Artefakte nach PURGE fehlen) + # Der Hash ist die autoritative Quelle für "Inhalt unverändert" + # Artefakte werden beim nächsten normalen Import wieder erstellt, wenn nötig + logger.info(f"⏭️ [SKIP] '{note_id}' unverändert (Hash identisch - überspringe komplett, auch wenn Artefakte fehlen)") + return {**result, "status": "unchanged", "note_id": note_id, "reason": "hash_identical"} + elif not force_replace and old_payload and not hash_match: + # WP-24c v4.5.10: Hash geändert - erlaube Verarbeitung (DEBUG-Level) + logger.debug(f"🔍 [CHANGE-DETECTION] '{note_id}': Hash geändert -> erlaube Verarbeitung") + + # WP-24c v4.5.10: Hash geändert oder keine alte Payload - prüfe Artefakte für normale Verarbeitung + c_miss, e_miss = artifacts_missing(self.client, self.prefix, note_id) + logger.debug(f"🔍 [CHANGE-DETECTION] '{note_id}': Artifact-Check: c_miss={c_miss}, e_miss={e_miss}") + + if not apply: + return {**result, "status": "dry-run", "changed": True, "note_id": note_id} + + # Chunks & MoE profile = note_pl.get("chunk_profile", "sliding_standard") - + note_type = resolve_note_type(self.registry, fm.get("type")) chunk_cfg = get_chunk_config_by_profile(self.registry, profile, note_type) enable_smart = chunk_cfg.get("enable_smart_edge_allocation", False) + chunks = await assemble_chunks(note_id, getattr(parsed, "body", ""), note_type, config=chunk_cfg) - # WP-15b: Chunker-Aufruf bereitet den Candidate-Pool pro Chunk vor. - chunks = await assemble_chunks(note_id, body_text, note_type, config=chunk_cfg) - - # Semantische Kanten-Validierung (Smart Edge Allocation via MoE-Profil) + # WP-24c v4.5.8: Validierung in Chunk-Schleife entfernt + # Alle candidate: Kanten werden jetzt in Phase 3 (nach build_edges_for_note) validiert + # Dies stellt sicher, dass auch Note-Scope Kanten aus LLM-Validierungs-Zonen geprüft werden + # Der candidate_pool wird unverändert weitergegeben, damit build_edges_for_note alle Kanten erkennt + # WP-24c v4.5.8: Nur ID-Validierung bleibt (Ghost-ID Schutz), keine LLM-Validierung mehr hier for ch in chunks: - filtered = [] + new_pool = [] for cand in getattr(ch, "candidate_pool", []): - # WP-25a: Nutzt nun das spezialisierte Validierungs-Profil - if cand.get("provenance") == "global_pool" and enable_smart: - if await validate_edge_candidate(ch.text, cand, self.batch_cache, self.llm, profile_name="ingest_validator"): - filtered.append(cand) - else: - # Explizite Kanten (Wikilinks/Callouts) werden ungeprüft übernommen - filtered.append(cand) - ch.candidate_pool = filtered + # WP-24c v4.5.8: Nur ID-Validierung (Ghost-ID Schutz) + t_id = cand.get('target_id') or cand.get('to') or cand.get('note_id') + if not self._is_valid_id(t_id): + continue + # WP-24c v4.5.8: Alle Kanten gehen durch - LLM-Validierung erfolgt in Phase 3 + new_pool.append(cand) + ch.candidate_pool = new_pool - # Payload-Erstellung für die Chunks - chunk_pls = make_chunk_payloads( - fm, note_pl["path"], chunks, file_path=file_path, - types_cfg=self.registry - ) + # chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, file_path=file_path, types_cfg=self.registry) + # v4.2.8 Fix C: Explizite Übergabe des Profil-Namens für den Chunk-Payload + chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, file_path=file_path, types_cfg=self.registry, chunk_profile=profile) - # Vektorisierung der Fenster-Texte vecs = await self.embedder.embed_documents([c.get("window") or "" for c in chunk_pls]) if chunk_pls else [] - # Aggregation aller finalen Kanten (Edges) - edges = build_edges_for_note( - note_id, chunk_pls, + # 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", []), - include_note_scope_refs=note_scope_refs + markdown_body=markdown_body ) - # Kanten-Typen via Registry validieren/auflösen - for e in edges: - e["kind"] = edge_registry.resolve( - e.get("kind", "related_to"), - provenance=e.get("provenance", "explicit"), - context={"file": file_path, "note_id": note_id, "line": e.get("line", "system")} - ) + # WP-24c v4.5.8: Phase 3 - Finaler Validierungs-Gate für candidate: Kanten + # Prüfe alle Kanten mit rule_id ODER provenance beginnend mit "candidate:" + # Dies schließt alle Kandidaten ein, unabhängig von ihrer Herkunft (global_pool, explicit:callout, etc.) + + # WP-24c v4.5.8: Kontext-Optimierung für Note-Scope Kanten + # Aggregiere den gesamten Note-Text für bessere Validierungs-Entscheidungen + note_text = markdown_body or " ".join([c.get("text", "") or c.get("window", "") for c in chunk_pls]) + # Erstelle eine Note-Summary aus den wichtigsten Chunks (für bessere Kontext-Qualität) + note_summary = " ".join([c.get("window", "") or c.get("text", "") for c in chunk_pls[:5]]) # Top 5 Chunks + + validated_edges = [] + rejected_edges = [] + + for e in raw_edges: + rule_id = e.get("rule_id", "") + provenance = e.get("provenance", "") + + # WP-24c v4.5.8: Trigger-Kriterium - rule_id ODER provenance beginnt mit "candidate:" + is_candidate = (rule_id and rule_id.startswith("candidate:")) or (provenance and provenance.startswith("candidate:")) + + if is_candidate: + # Extrahiere target_id für Validierung (aus verschiedenen möglichen Feldern) + target_id = e.get("target_id") or e.get("to") + if not target_id: + # Fallback: Versuche aus Payload zu extrahieren + payload = e.get("extra", {}) if isinstance(e.get("extra"), dict) else {} + target_id = payload.get("target_id") or payload.get("to") + + if not target_id: + logger.warning(f"⚠️ [PHASE 3] Keine target_id gefunden für Kante: {e}") + rejected_edges.append(e) + continue + + kind = e.get("kind", "related_to") + source_id = e.get("source_id", note_id) + scope = e.get("scope", "chunk") + + # WP-24c v4.5.8: Kontext-Optimierung für Note-Scope Kanten + # Für scope: note verwende Note-Summary oder gesamten Note-Text + # Für scope: chunk verwende den spezifischen Chunk-Text (falls verfügbar) + if scope == "note": + validation_text = note_summary or note_text + context_info = "Note-Scope (aggregiert)" + else: + # Für Chunk-Scope: Versuche Chunk-Text zu finden, sonst Note-Text + chunk_id = e.get("chunk_id") or source_id + chunk_text = None + for ch in chunk_pls: + if ch.get("chunk_id") == chunk_id or ch.get("id") == chunk_id: + chunk_text = ch.get("text") or ch.get("window", "") + break + validation_text = chunk_text or note_text + context_info = f"Chunk-Scope ({chunk_id})" + + # Erstelle Edge-Dict für Validierung (kompatibel mit validate_edge_candidate) + edge_for_validation = { + "kind": kind, + "to": target_id, # validate_edge_candidate erwartet "to" + "target_id": target_id, + "provenance": provenance if not provenance.startswith("candidate:") else provenance.replace("candidate:", "").strip(), + "confidence": e.get("confidence", 0.9) + } + + logger.info(f"🚀 [PHASE 3] Validierung: {source_id} -> {target_id} ({kind}) | Scope: {scope} | Kontext: {context_info}") + + # WP-24c v4.5.8: Validiere gegen optimierten Kontext + is_valid = await validate_edge_candidate( + chunk_text=validation_text, + edge=edge_for_validation, + batch_cache=self.batch_cache, + llm_service=self.llm, + profile_name="ingest_validator" + ) + + if is_valid: + # WP-24c v4.5.8: Entferne candidate: Präfix (Kante wird zum Fakt) + new_rule_id = rule_id.replace("candidate:", "").strip() if rule_id else provenance.replace("candidate:", "").strip() if provenance.startswith("candidate:") else provenance + if not new_rule_id: + new_rule_id = e.get("provenance", "explicit").replace("candidate:", "").strip() + + # Aktualisiere rule_id und provenance im Edge + e["rule_id"] = new_rule_id + if provenance.startswith("candidate:"): + e["provenance"] = provenance.replace("candidate:", "").strip() + + validated_edges.append(e) + logger.info(f"✅ [PHASE 3] VERIFIED: {source_id} -> {target_id} ({kind}) | rule_id: {new_rule_id}") + else: + # WP-24c v4.5.8: Kante ablehnen (nicht zu validated_edges hinzufügen) + rejected_edges.append(e) + logger.info(f"🚫 [PHASE 3] REJECTED: {source_id} -> {target_id} ({kind})") + else: + # WP-24c v4.5.8: Keine candidate: Kante -> direkt übernehmen + validated_edges.append(e) + + # WP-24c v4.5.8: Phase 3 abgeschlossen - rejected_edges werden NICHT weiterverarbeitet + # WP-24c v4.5.9: Persistierung von rejected_edges für Audit-Zwecke + if rejected_edges: + logger.info(f"🚫 [PHASE 3] {len(rejected_edges)} Kanten abgelehnt und werden nicht in die DB geschrieben") + self._persist_rejected_edges(note_id, rejected_edges) + + # WP-24c v4.5.8: Verwende validated_edges statt raw_edges für weitere Verarbeitung + # Nur verified Kanten (ohne candidate: Präfix) werden in Phase 2 (Symmetrie) verarbeitet + explicit_edges = [] + for e in validated_edges: + t_raw = e.get("target_id") + t_ctx = self.batch_cache.get(t_raw) + t_id = t_ctx.note_id if t_ctx else t_raw + + if not self._is_valid_id(t_id): continue + + resolved_kind = edge_registry.resolve(e.get("kind", "related_to"), provenance="explicit") + # WP-24c v4.1.0: target_section aus dem Edge-Payload extrahieren und beibehalten + target_section = e.get("target_section") + e.update({ + "kind": resolved_kind, + "relation": resolved_kind, # Konsistenz: kind und relation identisch + "target_id": t_id, + "source_id": e.get("source_id") or note_id, # Sicherstellen, dass source_id gesetzt ist + "origin_note_id": note_id, + "virtual": False + }) + explicit_edges.append(e) + + # Symmetrie puffern (WP-24c v4.1.0: Korrekte Symmetrie-Integrität) + inv_kind = edge_registry.get_inverse(resolved_kind) + if inv_kind and t_id != note_id: + # GOLD-STANDARD v4.1.0: Symmetrie-Integrität + v_edge = { + "note_id": t_id, # Besitzer-Wechsel: Symmetrie gehört zum Link-Ziel + "source_id": t_id, # Neue Quelle ist das Link-Ziel + "target_id": note_id, # Ziel ist die ursprüngliche Quelle + "kind": inv_kind, # Inverser Kanten-Typ + "relation": inv_kind, # Konsistenz: kind und relation identisch + "scope": "note", # Symmetrien sind immer Note-Level + "virtual": True, + "origin_note_id": note_id, # Tracking: Woher kommt die Symmetrie + } + # target_section beibehalten, falls vorhanden (für Section-Links) + if target_section: + v_edge["target_section"] = target_section + self.symmetry_buffer.append(v_edge) - # 4. DB Upsert via modularisierter Points-Logik - if purge_before and old_payload: - purge_artifacts(self.client, self.prefix, note_id) + # DB Upsert + if purge_before and old_payload: purge_artifacts(self.client, self.prefix, note_id) - # Speichern der Haupt-Note - n_name, n_pts = points_for_note(self.prefix, note_pl, None, self.dim) - upsert_batch(self.client, n_name, n_pts) + col_n, pts_n = points_for_note(self.prefix, note_pl, None, self.dim) + upsert_batch(self.client, col_n, pts_n, wait=True) - # Speichern der Chunks if chunk_pls and vecs: - c_pts = points_for_chunks(self.prefix, chunk_pls, vecs)[1] - upsert_batch(self.client, f"{self.prefix}_chunks", c_pts) + col_c, pts_c = points_for_chunks(self.prefix, chunk_pls, vecs) + upsert_batch(self.client, col_c, pts_c, wait=True) + + if explicit_edges: + col_e, pts_e = points_for_edges(self.prefix, explicit_edges) + upsert_batch(self.client, col_e, pts_e, wait=True) - # Speichern der Kanten - if edges: - e_pts = points_for_edges(self.prefix, edges)[1] - upsert_batch(self.client, f"{self.prefix}_edges", e_pts) + logger.info(f" ✨ Phase 1 fertig: {len(explicit_edges)} explizite Kanten für '{note_id}'.") + return {"status": "success", "note_id": note_id} - return { - "path": file_path, - "status": "success", - "changed": True, - "note_id": note_id, - "chunks_count": len(chunk_pls), - "edges_count": len(edges) - } except Exception as e: - logger.error(f"Processing failed: {e}", exc_info=True) - return {**result, "error": str(e)} + logger.error(f"❌ Fehler bei {file_path}: {e}", exc_info=True) + return {**result, "status": "error", "error": str(e)} async def create_from_text(self, markdown_content: str, filename: str, vault_root: str, folder: str = "00_Inbox") -> Dict[str, Any]: - """Erstellt eine Note aus einem Textstream und triggert die Ingestion.""" + """Erstellt eine Note aus einem Textstream.""" target_path = os.path.join(vault_root, folder, filename) os.makedirs(os.path.dirname(target_path), exist_ok=True) with open(target_path, "w", encoding="utf-8") as f: f.write(markdown_content) await asyncio.sleep(0.1) - # Triggert sofortigen Import mit force_replace/purge_before return await self.process_file(file_path=target_path, vault_root=vault_root, apply=True, force_replace=True, purge_before=True) \ No newline at end of file diff --git a/app/core/ingestion/ingestion_validation.py b/app/core/ingestion/ingestion_validation.py index 19af49d..b0eb4d8 100644 --- a/app/core/ingestion/ingestion_validation.py +++ b/app/core/ingestion/ingestion_validation.py @@ -1,20 +1,23 @@ """ FILE: app/core/ingestion/ingestion_validation.py DESCRIPTION: WP-15b semantische Validierung von Kanten gegen den LocalBatchCache. - WP-25b: Umstellung auf Lazy-Prompt-Orchestration (prompt_key + variables). -VERSION: 2.14.0 (WP-25b: Lazy Prompt Integration) + WP-24c: Erweiterung um automatische Symmetrie-Generierung (Inverse Kanten). + WP-25b: Konsequente Lazy-Prompt-Orchestration (prompt_key + variables). +VERSION: 3.0.0 (WP-24c: Symmetric Edge Management) STATUS: Active FIX: -- WP-25b: Entfernung manueller Prompt-Formatierung zur Unterstützung modell-spezifischer Prompts. -- WP-25b: Umstellung auf generate_raw_response mit prompt_key="edge_validation". -- WP-25a: Voller Erhalt der MoE-Profilsteuerung und Fallback-Kaskade via LLMService. +- WP-24c: Integration der EdgeRegistry zur dynamischen Inversions-Ermittlung. +- WP-24c: Implementierung von validate_and_symmetrize für bidirektionale Graphen. +- WP-25b: Beibehaltung der hierarchischen Prompt-Resolution und Modell-Spezi-Logik. """ import logging -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, List from app.core.parser import NoteContext -# ENTSCHEIDENDER FIX: Import der neutralen Bereinigungs-Logik zur Vermeidung von Circular Imports +# Import der neutralen Bereinigungs-Logik zur Vermeidung von Circular Imports from app.core.registry import clean_llm_text +# WP-24c: Zugriff auf das dynamische Vokabular +from app.services.edge_registry import registry as edge_registry logger = logging.getLogger(__name__) @@ -28,18 +31,18 @@ async def validate_edge_candidate( ) -> bool: """ WP-15b/25b: Validiert einen Kandidaten semantisch gegen das Ziel im Cache. - Nutzt Lazy-Prompt-Loading zur Unterstützung modell-spezifischer Validierungs-Templates. + Nutzt Lazy-Prompt-Loading (PROMPT-TRACE) für deterministische YES/NO Entscheidungen. """ target_id = edge.get("to") target_ctx = batch_cache.get(target_id) - # Robust Lookup Fix (v2.12.2): Support für Anker - if not target_ctx and "#" in target_id: + # Robust Lookup Fix (v2.12.2): Support für Anker (Note#Section) + if not target_ctx and "#" in str(target_id): base_id = target_id.split("#")[0] target_ctx = batch_cache.get(base_id) # Sicherheits-Fallback (Hard-Link Integrity) - # Explizite Wikilinks oder Callouts werden nicht durch das LLM verifiziert. + # Wenn das Ziel nicht im Cache ist, erlauben wir die Kante (Link-Erhalt). if not target_ctx: logger.info(f"ℹ️ [VALIDATION SKIP] No context for '{target_id}' - allowing link.") return True @@ -48,8 +51,7 @@ async def validate_edge_candidate( logger.info(f"⚖️ [VALIDATING] Relation '{edge.get('kind')}' -> '{target_id}' (Profile: {profile_name})...") # WP-25b: Lazy-Prompt Aufruf. - # Wir übergeben keine formatierte Nachricht mehr, sondern Key und Daten-Dict. - # Das manuelle 'template = llm_service.get_prompt(...)' entfällt hier. + # Übergabe von prompt_key und Variablen für modell-optimierte Formatierung. raw_response = await llm_service.generate_raw_response( prompt_key="edge_validation", variables={ @@ -62,7 +64,7 @@ async def validate_edge_candidate( profile_name=profile_name ) - # WP-14 Fix: Bereinigung zur Sicherstellung der Interpretierbarkeit + # Bereinigung zur Sicherstellung der Interpretierbarkeit (Mistral/Qwen Safe) response = clean_llm_text(raw_response) # Semantische Prüfung des Ergebnisses @@ -78,12 +80,71 @@ async def validate_edge_candidate( error_str = str(e).lower() error_type = type(e).__name__ - # WP-25b FIX: Differenzierung zwischen transienten und permanenten Fehlern - # Transiente Fehler (Timeout, Network) → erlauben (Datenverlust vermeiden) + # WP-25b: Differenzierung zwischen transienten und permanenten Fehlern + # Transiente Fehler (Netzwerk) → erlauben (Integrität vor Präzision) if any(x in error_str for x in ["timeout", "connection", "network", "unreachable", "refused"]): - logger.warning(f"⚠️ Transient error for {target_id} using {profile_name}: {error_type} - {e}. Allowing edge.") + logger.warning(f"⚠️ Transient error for {target_id}: {error_type} - {e}. Allowing edge.") return True - # Permanente Fehler (Config, Validation, Invalid Response) → ablehnen (Graph-Qualität) - logger.error(f"❌ Permanent validation error for {target_id} using {profile_name}: {error_type} - {e}") - return False \ No newline at end of file + # Permanente Fehler → ablehnen (Graph-Qualität schützen) + logger.error(f"❌ Permanent validation error for {target_id}: {error_type} - {e}") + return False + +async def validate_and_symmetrize( + chunk_text: str, + edge: Dict, + source_id: str, + batch_cache: Dict[str, NoteContext], + llm_service: Any, + profile_name: str = "ingest_validator" +) -> List[Dict]: + """ + WP-24c: Erweitertes Validierungs-Gateway. + Prüft die Primärkante und erzeugt bei Erfolg automatisch die inverse Kante. + + Returns: + List[Dict]: Eine Liste mit 0, 1 (nur Primär) oder 2 (Primär + Invers) Kanten. + """ + # 1. Semantische Prüfung der Primärkante (A -> B) + is_valid = await validate_edge_candidate( + chunk_text=chunk_text, + edge=edge, + batch_cache=batch_cache, + llm_service=llm_service, + profile_name=profile_name + ) + + if not is_valid: + return [] + + validated_edges = [edge] + + # 2. WP-24c: Symmetrie-Generierung (B -> A) + # Wir laden den inversen Typ dynamisch aus der EdgeRegistry (Single Source of Truth) + original_kind = edge.get("kind", "related_to") + inverse_kind = edge_registry.get_inverse(original_kind) + + # Wir erzeugen eine inverse Kante nur, wenn ein sinnvoller inverser Typ existiert + # und das Ziel der Primärkante (to) valide ist. + target_id = edge.get("to") + + if target_id and source_id: + # Die inverse Kante zeigt vom Ziel der Primärkante zurück zur Quelle. + # Sie wird als 'virtual' markiert, um sie im Retrieval/UI identifizierbar zu machen. + inverse_edge = { + "to": source_id, + "kind": inverse_kind, + "provenance": "structure", # System-generiert, geschützt durch Firewall + "confidence": edge.get("confidence", 0.9) * 0.9, # Leichte Dämpfung für virtuelle Pfade + "virtual": True, + "note_id": target_id, # Die Note, von der die inverse Kante ausgeht + "rule_id": f"symmetry:{original_kind}" + } + + # Wir fügen die Symmetrie nur hinzu, wenn sie einen echten Mehrwert bietet + # (Vermeidung von redundanten related_to -> related_to Loops) + if inverse_kind != original_kind or original_kind not in ["related_to", "references"]: + validated_edges.append(inverse_edge) + logger.info(f"🔄 [SYMMETRY] Generated inverse edge: '{target_id}' --({inverse_kind})--> '{source_id}'") + + return validated_edges \ No newline at end of file diff --git a/app/core/logging_setup.py b/app/core/logging_setup.py index f2f4db9..c6f23b2 100644 --- a/app/core/logging_setup.py +++ b/app/core/logging_setup.py @@ -2,36 +2,52 @@ import logging import os from logging.handlers import RotatingFileHandler -def setup_logging(): - # 1. Log-Verzeichnis erstellen (falls nicht vorhanden) +def setup_logging(log_level: int = None): + """ + Konfiguriert das Logging-System mit File- und Console-Handler. + WP-24c v4.4.0-DEBUG: Unterstützt DEBUG-Level für End-to-End Tracing. + + Args: + log_level: Optionales Log-Level (logging.DEBUG, logging.INFO, etc.) + Falls nicht gesetzt, wird aus DEBUG Umgebungsvariable gelesen. + """ + # 1. Log-Level bestimmen + if log_level is None: + # WP-24c v4.4.0-DEBUG: Unterstützung für DEBUG-Level via Umgebungsvariable + debug_mode = os.getenv("DEBUG", "false").lower() == "true" + log_level = logging.DEBUG if debug_mode else logging.INFO + + # 2. Log-Verzeichnis erstellen (falls nicht vorhanden) log_dir = "logs" if not os.path.exists(log_dir): os.makedirs(log_dir) log_file = os.path.join(log_dir, "mindnet.log") - # 2. Formatter definieren (Zeitstempel | Level | Modul | Nachricht) + # 3. Formatter definieren (Zeitstempel | Level | Modul | Nachricht) formatter = logging.Formatter( '%(asctime)s | %(levelname)-8s | %(name)s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) - # 3. File Handler: Schreibt in Datei (max. 5MB pro Datei, behält 5 Backups) + # 4. File Handler: Schreibt in Datei (max. 5MB pro Datei, behält 5 Backups) file_handler = RotatingFileHandler( log_file, maxBytes=5*1024*1024, backupCount=5, encoding='utf-8' ) file_handler.setFormatter(formatter) - file_handler.setLevel(logging.INFO) + file_handler.setLevel(log_level) # WP-24c v4.4.0-DEBUG: Respektiert log_level - # 4. Stream Handler: Schreibt weiterhin auf die Konsole + # 5. Stream Handler: Schreibt weiterhin auf die Konsole console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) - console_handler.setLevel(logging.INFO) + console_handler.setLevel(log_level) # WP-24c v4.4.0-DEBUG: Respektiert log_level - # 5. Root Logger konfigurieren + # 6. Root Logger konfigurieren logging.basicConfig( - level=logging.INFO, - handlers=[file_handler, console_handler] + level=log_level, + handlers=[file_handler, console_handler], + force=True # Überschreibt bestehende Konfiguration ) - logging.info(f"📝 Logging initialized. Writing to {log_file}") \ No newline at end of file + level_name = "DEBUG" if log_level == logging.DEBUG else "INFO" + logging.info(f"📝 Logging initialized (Level: {level_name}). Writing to {log_file}") \ No newline at end of file diff --git a/app/core/retrieval/decision_engine.py b/app/core/retrieval/decision_engine.py index e74e60a..363d438 100644 --- a/app/core/retrieval/decision_engine.py +++ b/app/core/retrieval/decision_engine.py @@ -151,15 +151,21 @@ class DecisionEngine: retrieval_results = await asyncio.gather(*retrieval_tasks, return_exceptions=True) # Phase 2: Formatierung und optionale Kompression + # WP-24c v4.5.5: Context-Reuse - Sicherstellen, dass formatted_context auch bei Kompressions-Fehlern erhalten bleibt final_stream_tasks = [] + formatted_contexts = {} # WP-24c v4.5.5: Persistenz für Fallback-Zugriff + for name, res in zip(active_streams, retrieval_results): if isinstance(res, Exception): logger.error(f"Stream '{name}' failed during retrieval: {res}") - async def _err(): return f"[Fehler im Wissens-Stream {name}]" + error_msg = f"[Fehler im Wissens-Stream {name}]" + formatted_contexts[name] = error_msg + async def _err(msg=error_msg): return msg final_stream_tasks.append(_err()) continue formatted_context = self._format_stream_context(res) + formatted_contexts[name] = formatted_context # WP-24c v4.5.5: Persistenz für Fallback # WP-25a: Kompressions-Check (Inhaltsverdichtung) stream_cfg = library.get(name, {}) @@ -168,6 +174,7 @@ class DecisionEngine: if len(formatted_context) > threshold: logger.info(f"⚙️ [WP-25b] Triggering Lazy-Compression for stream '{name}'...") comp_profile = stream_cfg.get("compression_profile") + # WP-24c v4.5.5: Kompression mit Context-Reuse - bei Fehler wird formatted_context zurückgegeben final_stream_tasks.append( self._compress_stream_content(name, formatted_context, query, comp_profile) ) @@ -176,12 +183,31 @@ class DecisionEngine: final_stream_tasks.append(_direct()) # Finale Inhalte parallel fertigstellen - final_contents = await asyncio.gather(*final_stream_tasks) - return dict(zip(active_streams, final_contents)) + # WP-24c v4.5.5: Bei Kompressions-Fehlern wird der Original-Content zurückgegeben (siehe _compress_stream_content) + final_contents = await asyncio.gather(*final_stream_tasks, return_exceptions=True) + + # WP-24c v4.5.5: Exception-Handling für finale Inhalte - verwende Original-Content bei Fehlern + final_results = {} + for name, content in zip(active_streams, final_contents): + if isinstance(content, Exception): + logger.warning(f"⚠️ [CONTEXT-REUSE] Stream '{name}' Fehler in finaler Verarbeitung: {content}. Verwende Original-Context.") + final_results[name] = formatted_contexts.get(name, f"[Fehler im Stream {name}]") + else: + final_results[name] = content + + logger.debug(f"📊 [STREAMS] Finale Stream-Ergebnisse: {[(k, len(v)) for k, v in final_results.items()]}") + return final_results async def _compress_stream_content(self, stream_name: str, content: str, query: str, profile: Optional[str]) -> str: - """WP-25b: Inhaltsverdichtung via Lazy-Loading 'compression_template'.""" + """ + WP-25b: Inhaltsverdichtung via Lazy-Loading 'compression_template'. + WP-24c v4.5.5: Context-Reuse - Bei Fehlern wird der Original-Content zurückgegeben, + um Re-Retrieval zu vermeiden. + """ try: + # WP-24c v4.5.5: Logging für LLM-Trace im Kompressions-Modus + logger.debug(f"🔧 [COMPRESSION] Starte Kompression für Stream '{stream_name}' (Content-Länge: {len(content)})") + summary = await self.llm_service.generate_raw_response( prompt_key="compression_template", variables={ @@ -193,9 +219,19 @@ class DecisionEngine: priority="background", max_retries=1 ) - return summary.strip() if (summary and len(summary.strip()) > 10) else content + + # WP-24c v4.5.5: Validierung des Kompressions-Ergebnisses + if summary and len(summary.strip()) > 10: + logger.debug(f"✅ [COMPRESSION] Kompression erfolgreich für '{stream_name}' (Original: {len(content)}, Komprimiert: {len(summary)})") + return summary.strip() + else: + logger.warning(f"⚠️ [COMPRESSION] Kompressions-Ergebnis zu kurz für '{stream_name}', verwende Original-Content") + return content + except Exception as e: - logger.error(f"❌ Compression of {stream_name} failed: {e}") + # WP-24c v4.5.5: Context-Reuse - Bei Fehlern Original-Content zurückgeben (kein Re-Retrieval) + logger.error(f"❌ [COMPRESSION] Kompression von '{stream_name}' fehlgeschlagen: {e}") + logger.info(f"🔄 [CONTEXT-REUSE] Verwende Original-Content für '{stream_name}' (Länge: {len(content)}) - KEIN Re-Retrieval") return content async def _run_single_stream(self, name: str, cfg: Dict, query: str) -> QueryResponse: @@ -211,7 +247,26 @@ class DecisionEngine: explain=True ) + # WP-24c v4.5.0-DEBUG: Retrieval-Tracer - Protokollierung vor der Suche + logger.info(f"🔍 [RETRIEVAL] Starte Stream: '{name}'") + logger.info(f" -> Transformierte Query: '{transformed_query}'") + logger.debug(f" ⚙️ [FILTER] Angewandte Metadaten-Filter: {request.filters}") + logger.debug(f" ⚙️ [FILTER] Top-K: {request.top_k}, Expand-Depth: {request.expand.get('depth') if request.expand else None}") + response = await self.retriever.search(request) + + # WP-24c v4.5.0-DEBUG: Retrieval-Tracer - Protokollierung nach der Suche + if not response.results: + logger.warning(f"⚠️ [EMPTY] Stream '{name}' lieferte 0 Ergebnisse.") + else: + logger.info(f"✨ [SUCCESS] Stream '{name}' lieferte {len(response.results)} Treffer.") + # Top 3 Treffer im DEBUG-Level loggen + # WP-24c v4.5.4: QueryHit hat kein chunk_id Feld - verwende node_id (enthält die Chunk-ID) + for i, hit in enumerate(response.results[:3]): + chunk_id = hit.node_id # node_id ist die Chunk-ID (pid) + score = hit.total_score # QueryHit hat total_score, nicht score + logger.debug(f" [{i+1}] Chunk: {chunk_id} | Score: {score:.4f} | Path: {hit.source.get('path', 'N/A') if hit.source else 'N/A'}") + for hit in response.results: hit.stream_origin = name return response @@ -270,19 +325,54 @@ class DecisionEngine: except Exception as e: logger.error(f"Final Synthesis failed: {e}") - # ROBUST FALLBACK (v1.2.1 Gate): Versuche eine minimale Antwort zu generieren - # WP-25b FIX: Konsistente Nutzung von prompt_key statt hardcodiertem Prompt + # WP-24c v4.5.5: ROBUST FALLBACK mit Context-Reuse + # WICHTIG: stream_results werden Wiederverwendet - KEIN Re-Retrieval + logger.info(f"🔄 [FALLBACK] Verwende vorhandene stream_results (KEIN Re-Retrieval)") + logger.debug(f" -> Verfügbare Streams: {list(stream_results.keys())}") + logger.debug(f" -> Stream-Längen: {[(k, len(v)) for k, v in stream_results.items()]}") + + # WP-24c v4.5.5: Context-Reuse - Nutze vorhandene stream_results fallback_context = "\n\n".join([v for v in stream_results.values() if len(v) > 20]) + + if not fallback_context or len(fallback_context.strip()) < 20: + logger.warning(f"⚠️ [FALLBACK] Fallback-Context zu kurz ({len(fallback_context)} Zeichen). Stream-Ergebnisse möglicherweise leer.") + return f"Entschuldigung, ich konnte keine relevanten Informationen zu Ihrer Anfrage finden. (Fehler: {str(e)})" + try: - return await self.llm_service.generate_raw_response( + # WP-24c v4.5.5: Fallback-Synthese mit LLM-Trace-Logging + logger.info(f"🔄 [FALLBACK] Starte Fallback-Synthese mit vorhandenem Context (Länge: {len(fallback_context)})") + logger.debug(f" -> Fallback-Profile: {profile}, Template: fallback_synthesis") + + result = await self.llm_service.generate_raw_response( prompt_key="fallback_synthesis", variables={"query": query, "context": fallback_context}, system=system_prompt, priority="realtime", profile_name=profile ) - except (ValueError, KeyError): - # Fallback auf direkten Prompt, falls Template nicht existiert - logger.warning("⚠️ Fallback template 'fallback_synthesis' not found. Using direct prompt.") - return await self.llm_service.generate_raw_response( - prompt=f"Beantworte: {query}\n\nKontext:\n{fallback_context}", - system=system_prompt, priority="realtime", profile_name=profile - ) \ No newline at end of file + + logger.info(f"✅ [FALLBACK] Fallback-Synthese erfolgreich (Antwort-Länge: {len(result) if result else 0})") + return result + + except (ValueError, KeyError) as template_error: + # WP-24c v4.5.9: Fallback auf generisches Template mit variables + # Nutzt Lazy-Loading aus WP-25b für modell-spezifische Fallback-Prompts + logger.warning(f"⚠️ [FALLBACK] Template 'fallback_synthesis' nicht gefunden: {template_error}. Versuche generisches Template.") + logger.debug(f" -> Fallback-Profile: {profile}, Context-Länge: {len(fallback_context)}") + + try: + # WP-24c v4.5.9: Versuche generisches Template mit variables (Lazy-Loading) + result = await self.llm_service.generate_raw_response( + prompt_key="fallback_synthesis_generic", # Fallback-Template + variables={"query": query, "context": fallback_context}, + system=system_prompt, priority="realtime", profile_name=profile + ) + logger.info(f"✅ [FALLBACK] Generisches Template erfolgreich (Antwort-Länge: {len(result) if result else 0})") + return result + except (ValueError, KeyError) as fallback_error: + # WP-24c v4.5.9: Letzter Fallback - direkter Prompt (nur wenn beide Templates fehlen) + logger.error(f"❌ [FALLBACK] Auch generisches Template nicht gefunden: {fallback_error}. Verwende direkten Prompt als letzten Fallback.") + result = await self.llm_service.generate_raw_response( + prompt=f"Beantworte: {query}\n\nKontext:\n{fallback_context}", + system=system_prompt, priority="realtime", profile_name=profile + ) + logger.info(f"✅ [FALLBACK] Direkter Prompt erfolgreich (Antwort-Länge: {len(result) if result else 0})") + return result \ No newline at end of file diff --git a/app/core/retrieval/retriever.py b/app/core/retrieval/retriever.py index df48239..8a697ed 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,15 +67,79 @@ 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) + + # WP-24c v4.5.0-DEBUG: Retrieval-Tracer - Protokollierung der rohen Qdrant-Antwort + logger.debug(f"📊 [RAW-HITS] Qdrant lieferte {len(raw_hits)} Roh-Treffer (Top-K: {top_k})") + if filters: + logger.debug(f" ⚙️ [FILTER] Angewandte Filter: {filters}") + + # Logge die Top 3 Roh-Scores für Diagnose + for i, hit in enumerate(raw_hits[:3]): + hit_id = str(hit[0]) if hit else "N/A" + hit_score = float(hit[1]) if hit and len(hit) > 1 else 0.0 + hit_payload = dict(hit[2] or {}) if hit and len(hit) > 2 else {} + hit_path = hit_payload.get('path', 'N/A') + logger.debug(f" [{i+1}] ID: {hit_id} | Raw-Score: {hit_score:.4f} | Path: {hit_path}") + # Strikte Typkonvertierung für Stabilität return [(str(hit[0]), float(hit[1]), dict(hit[2] or {})) for hit in raw_hits] @@ -164,7 +232,8 @@ def _build_explanation( top_edges = sorted(edges_dto, key=lambda e: e.confidence, reverse=True) for e in top_edges[:3]: peer = e.source if e.direction == "in" else e.target - prov_txt = "Bestätigte" if e.provenance == "explicit" else "KI-basierte" + # WP-24c v4.5.3: Unterstütze alle explicit-Varianten (explicit, explicit:callout, etc.) + prov_txt = "Bestätigte" if e.provenance and e.provenance.startswith("explicit") else "KI-basierte" boost_txt = f" [Boost x{applied_boosts.get(e.kind)}]" if applied_boosts and e.kind in applied_boosts else "" reasons.append(Reason( @@ -254,6 +323,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,23 +346,51 @@ 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)) + # WP-24c v4.5.0-DEBUG: Retrieval-Tracer - Finale Ergebnisse + latency_ms = int((time.time() - t0) * 1000) + if not results: + logger.warning(f"⚠️ [EMPTY] Hybride Suche lieferte 0 Ergebnisse (Latency: {latency_ms}ms)") + else: + logger.info(f"✨ [SUCCESS] Hybride Suche lieferte {len(results)} Treffer (Latency: {latency_ms}ms)") + # Top 3 finale Scores loggen + # WP-24c v4.5.4: QueryHit hat kein chunk_id Feld - verwende node_id (enthält die Chunk-ID) + for i, hit in enumerate(results[:3]): + chunk_id = hit.node_id # node_id ist die Chunk-ID (pid) + logger.debug(f" [{i+1}] Final: Chunk={chunk_id} | Total-Score={hit.total_score:.4f} | Semantic={hit.semantic_score:.4f} | Edge={hit.edge_bonus:.4f}") + + return QueryResponse(results=results, used_mode=used_mode, latency_ms=latency_ms) def hybrid_retrieve(req: QueryRequest) -> QueryResponse: """ Die Haupt-Einstiegsfunktion für die hybride Suche. WP-15c: Implementiert Edge-Aggregation (Super-Kanten). + WP-24c v4.5.0-DEBUG: Retrieval-Tracer für Diagnose. """ + # WP-24c v4.5.0-DEBUG: Retrieval-Tracer - Start der hybriden Suche + logger.info(f"🔍 [RETRIEVAL] Starte hybride Suche") + logger.info(f" -> Query: '{req.query[:100]}...' (Länge: {len(req.query)})") + logger.debug(f" ⚙️ [FILTER] Request-Filter: {req.filters}") + logger.debug(f" ⚙️ [FILTER] Top-K: {req.top_k}, Expand: {req.expand}, Target-Section: {req.target_section}") client, prefix = _get_client_and_prefix() vector = list(req.query_vector) if req.query_vector else _get_query_vector(req) 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) + + # WP-24c v4.5.0-DEBUG: Retrieval-Tracer - Vor semantischer Suche + logger.debug(f"🔍 [RETRIEVAL] Starte semantische Seed-Suche (Top-K: {top_k * 3}, Target-Section: {target_section})") + + hits = _semantic_hits(client, prefix, vector, top_k=top_k * 3, filters=req.filters, target_section=target_section) + + # WP-24c v4.5.0-DEBUG: Retrieval-Tracer - Nach semantischer Suche + logger.debug(f"📊 [SEED-HITS] Semantische Suche lieferte {len(hits)} Seed-Treffer") # 2. Graph Expansion Konfiguration expand_cfg = req.expand if isinstance(req.expand, dict) else {} @@ -292,40 +399,93 @@ def hybrid_retrieve(req: QueryRequest) -> QueryResponse: subgraph: ga.Subgraph | None = None if depth > 0 and hits: - seed_ids = list({h[2].get("note_id") for h in hits if h[2].get("note_id")}) + # WP-24c v4.5.2: Chunk-Aware Graph Traversal + # Extrahiere sowohl note_id als auch chunk_id (pid) direkt aus den Hits + # Dies stellt sicher, dass Chunk-Scope Edges gefunden werden + seed_note_ids = list({h[2].get("note_id") for h in hits if h[2].get("note_id")}) + seed_chunk_ids = list({h[0] for h in hits if h[0]}) # pid ist die Chunk-ID - if seed_ids: + # Kombiniere beide Sets für vollständige Seed-Abdeckung + # Chunk-IDs können auch als Note-IDs fungieren (für Note-Scope Edges) + all_seed_ids = list(set(seed_note_ids + seed_chunk_ids)) + + if all_seed_ids: try: - subgraph = ga.expand(client, prefix, seed_ids, depth=depth, edge_types=expand_cfg.get("edge_types")) + # WP-24c v4.5.2: Chunk-IDs sind bereits aus Hits extrahiert + # Zusätzlich können wir noch weitere Chunk-IDs für die Note-IDs laden + # (für den Fall, dass nicht alle Chunks in den Top-K Hits sind) + additional_chunk_ids = _get_chunk_ids_for_notes(client, prefix, seed_note_ids) + # Kombiniere direkte Chunk-IDs aus Hits mit zusätzlich geladenen + all_chunk_ids = list(set(seed_chunk_ids + additional_chunk_ids)) - # --- WP-15c: Edge-Aggregation & Deduplizierung (Super-Kanten) --- + # WP-24c v4.5.2: Erweiterte Edge-Retrieval mit Chunk-Scope und Section-Filtering + # Verwende all_seed_ids (enthält sowohl note_id als auch chunk_id) + # und all_chunk_ids für explizite Chunk-Scope Edge-Suche + subgraph = ga.expand( + client, prefix, all_seed_ids, + depth=depth, + edge_types=expand_cfg.get("edge_types"), + chunk_ids=all_chunk_ids, + target_section=target_section + ) + + # WP-24c v4.5.2: Debug-Logging für Chunk-Awareness + logger.debug(f"🔍 [SEEDS] Note-IDs: {len(seed_note_ids)}, Chunk-IDs: {len(seed_chunk_ids)}, Total Seeds: {len(all_seed_ids)}") + logger.debug(f" -> Zusätzliche Chunk-IDs geladen: {len(additional_chunk_ids)}, Total Chunk-IDs: {len(all_chunk_ids)}") + + # --- 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 +495,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}") @@ -357,7 +528,24 @@ def hybrid_retrieve(req: QueryRequest) -> QueryResponse: # 3. Scoring & Explanation Generierung # top_k wird erst hier final angewandt - return _build_hits_from_semantic(hits, top_k, "hybrid", subgraph, req.explain, boost_edges) + # WP-24c v4.5.0-DEBUG: Retrieval-Tracer - Vor finaler Hit-Erstellung + if subgraph: + # WP-24c v4.5.1: Subgraph hat kein .edges Attribut, sondern .adj (Adjazenzliste) + # Zähle alle Kanten aus der Adjazenzliste + edge_count = sum(len(edges) for edges in subgraph.adj.values()) if hasattr(subgraph, 'adj') else 0 + logger.debug(f"📊 [GRAPH] Subgraph enthält {edge_count} Kanten") + else: + logger.debug(f"📊 [GRAPH] Kein Subgraph (depth=0 oder keine Seed-IDs)") + + result = _build_hits_from_semantic(hits, top_k, "hybrid", subgraph, req.explain, boost_edges) + + # WP-24c v4.5.0-DEBUG: Retrieval-Tracer - Nach finaler Hit-Erstellung + if not result.results: + logger.warning(f"⚠️ [EMPTY] Hybride Suche lieferte nach Scoring 0 finale Ergebnisse") + else: + logger.info(f"✨ [SUCCESS] Hybride Suche lieferte {len(result.results)} finale Treffer (Mode: {result.used_mode})") + + return result def semantic_retrieve(req: QueryRequest) -> QueryResponse: diff --git a/app/core/retrieval/retriever_scoring.py b/app/core/retrieval/retriever_scoring.py index ce913cb..b8be453 100644 --- a/app/core/retrieval/retriever_scoring.py +++ b/app/core/retrieval/retriever_scoring.py @@ -110,6 +110,11 @@ def compute_wp22_score( # Sicherstellen, dass der Score niemals 0 oder negativ ist (Floor) final_score = max(0.0001, float(total)) + # WP-24c v4.5.0-DEBUG: Retrieval-Tracer - Protokollierung der Score-Berechnung + chunk_id = payload.get("chunk_id", payload.get("id", "unknown")) + logger.debug(f"📈 [SCORE-TRACE] Chunk: {chunk_id} | Base: {base_val:.4f} | Multiplier: {total_boost:.2f} | Final: {final_score:.4f}") + logger.debug(f" -> Details: StatusMult={status_mult:.2f}, TypeImpact={type_impact:.2f}, EdgeImpact={edge_impact_final:.4f}, CentImpact={cent_impact_final:.4f}") + return { "total": final_score, "edge_bonus": float(edge_bonus_raw), diff --git a/app/core/type_registry.py b/app/core/type_registry.py index 36763a5..824ebd6 100644 --- a/app/core/type_registry.py +++ b/app/core/type_registry.py @@ -1,11 +1,12 @@ """ FILE: app/core/type_registry.py -DESCRIPTION: Loader für types.yaml. Achtung: Wird in der aktuellen Pipeline meist durch lokale Loader in 'ingestion.py' oder 'note_payload.py' umgangen. -VERSION: 1.0.0 -STATUS: Deprecated (Redundant) +DESCRIPTION: Loader für types.yaml. + WP-24c: Robustheits-Fix für chunking_profile vs chunk_profile. + WP-14: Support für zentrale Registry-Strukturen. +VERSION: 1.1.0 (Audit-Fix: Profile Key Consistency) +STATUS: Active (Support für Legacy-Loader) DEPENDENCIES: yaml, os, functools EXTERNAL_CONFIG: config/types.yaml -LAST_ANALYSIS: 2025-12-15 """ from __future__ import annotations @@ -18,12 +19,12 @@ try: except Exception: yaml = None # wird erst benötigt, wenn eine Datei gelesen werden soll -# Konservativer Default – bewusst minimal +# Konservativer Default – WP-24c: Nutzt nun konsistent 'chunking_profile' _DEFAULT_REGISTRY: Dict[str, Any] = { "version": "1.0", "types": { "concept": { - "chunk_profile": "medium", + "chunking_profile": "medium", "edge_defaults": ["references", "related_to"], "retriever_weight": 1.0, } @@ -33,7 +34,6 @@ _DEFAULT_REGISTRY: Dict[str, Any] = { } # Chunk-Profile → Overlap-Empfehlungen (nur für synthetische Fensterbildung) -# Die absoluten Chunk-Längen bleiben Aufgabe des Chunkers (assemble_chunks). _PROFILE_TO_OVERLAP: Dict[str, Tuple[int, int]] = { "short": (20, 30), "medium": (40, 60), @@ -45,7 +45,7 @@ _PROFILE_TO_OVERLAP: Dict[str, Tuple[int, int]] = { def load_type_registry(path: str = "config/types.yaml") -> Dict[str, Any]: """ Lädt die Registry aus 'path'. Bei Fehlern wird ein konserviver Default geliefert. - Die Rückgabe ist *prozessweit* gecached. + Die Rückgabe ist prozessweit gecached. """ if not path: return dict(_DEFAULT_REGISTRY) @@ -54,7 +54,6 @@ def load_type_registry(path: str = "config/types.yaml") -> Dict[str, Any]: return dict(_DEFAULT_REGISTRY) if yaml is None: - # PyYAML fehlt → auf Default zurückfallen return dict(_DEFAULT_REGISTRY) try: @@ -71,6 +70,7 @@ def load_type_registry(path: str = "config/types.yaml") -> Dict[str, Any]: def get_type_config(note_type: Optional[str], reg: Dict[str, Any]) -> Dict[str, Any]: + """Extrahiert die Konfiguration für einen spezifischen Typ.""" t = (note_type or "concept").strip().lower() types = (reg or {}).get("types", {}) if isinstance(reg, dict) else {} return types.get(t) or types.get("concept") or _DEFAULT_REGISTRY["types"]["concept"] @@ -84,8 +84,13 @@ def resolve_note_type(fm_type: Optional[str], reg: Dict[str, Any]) -> str: def effective_chunk_profile(note_type: Optional[str], reg: Dict[str, Any]) -> Optional[str]: + """ + Ermittelt das aktive Chunking-Profil für einen Notiz-Typ. + Fix (Audit-Problem 2): Prüft beide Key-Varianten für 100% Kompatibilität. + """ cfg = get_type_config(note_type, reg) - prof = cfg.get("chunk_profile") + # Check 'chunking_profile' (Standard) OR 'chunk_profile' (Legacy/Fallback) + prof = cfg.get("chunking_profile") or cfg.get("chunk_profile") if isinstance(prof, str) and prof.strip(): return prof.strip().lower() return None @@ -95,4 +100,4 @@ def profile_overlap(profile: Optional[str]) -> Tuple[int, int]: """Gibt eine Overlap-Empfehlung (low, high) für das Profil zurück.""" if not profile: return _PROFILE_TO_OVERLAP["medium"] - return _PROFILE_TO_OVERLAP.get(profile.strip().lower(), _PROFILE_TO_OVERLAP["medium"]) + return _PROFILE_TO_OVERLAP.get(profile.strip().lower(), _PROFILE_TO_OVERLAP["medium"]) \ No newline at end of file diff --git a/app/models/dto.py b/app/models/dto.py index f0a1258..15d6eea 100644 --- a/app/models/dto.py +++ b/app/models/dto.py @@ -46,7 +46,14 @@ class EdgeDTO(BaseModel): target: str weight: float direction: Literal["out", "in", "undirected"] = "out" - provenance: Optional[Literal["explicit", "rule", "smart", "structure"]] = "explicit" + # WP-24c v4.5.3: Erweiterte Provenance-Werte für Chunk-Aware Edges + # Unterstützt alle tatsächlich verwendeten Provenance-Typen im System + provenance: Optional[Literal[ + "explicit", "rule", "smart", "structure", + "explicit:callout", "explicit:wikilink", "explicit:note_zone", "explicit:note_scope", + "inline:rel", "callout:edge", "semantic_ai", "structure:belongs_to", "structure:order", + "derived:backlink", "edge_defaults", "global_pool" + ]] = "explicit" confidence: float = 1.0 target_section: Optional[str] = None @@ -56,6 +63,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 +75,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 +136,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 +149,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/app/routers/chat.py b/app/routers/chat.py index 0c3ebd6..ec7fcf7 100644 --- a/app/routers/chat.py +++ b/app/routers/chat.py @@ -3,9 +3,11 @@ FILE: app/routers/chat.py DESCRIPTION: Haupt-Chat-Interface (WP-25b Edition). Kombiniert die spezialisierte Interview-Logik mit der neuen Lazy-Prompt-Orchestration und MoE-Synthese. -VERSION: 3.0.5 (WP-25b: Lazy Prompt Integration) + WP-24c: Integration der Discovery API für proaktive Vernetzung. +VERSION: 3.1.0 (WP-24c: Discovery API Integration) STATUS: Active FIX: +- WP-24c: Neuer Endpunkt /query/discover für proaktive Kanten-Vorschläge. - WP-25b: Umstellung des Interview-Modus auf Lazy-Prompt (prompt_key + variables). - WP-25b: Delegation der RAG-Phase an die Engine v1.3.0 für konsistente MoE-Steuerung. - WP-25a: Voller Erhalt der v3.0.2 Logik (Interview, Schema-Resolution, FastPaths). @@ -13,6 +15,7 @@ FIX: from fastapi import APIRouter, HTTPException, Depends from typing import List, Dict, Any, Optional +from pydantic import BaseModel import time import uuid import logging @@ -22,13 +25,27 @@ import asyncio from pathlib import Path from app.config import get_settings -from app.models.dto import ChatRequest, ChatResponse, QueryHit +from app.models.dto import ChatRequest, ChatResponse, QueryHit, QueryRequest from app.services.llm_service import LLMService from app.services.feedback_service import log_search router = APIRouter() logger = logging.getLogger(__name__) +# --- EBENE 0: DTOs FÜR DISCOVERY (WP-24c) --- + +class DiscoveryRequest(BaseModel): + content: str + top_k: int = 8 + min_confidence: float = 0.6 + +class DiscoveryHit(BaseModel): + target_note: str # Note ID + target_title: str # Menschenlesbarer Titel + suggested_edge_type: str # Kanonischer Typ aus edge_vocabulary + confidence_score: float # Kombinierter Vektor- + KI-Score + reasoning: str # Kurze Begründung der KI + # --- EBENE 1: CONFIG LOADER & CACHING (WP-25 Standard) --- _DECISION_CONFIG_CACHE = None @@ -135,8 +152,7 @@ async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]: return "INTERVIEW", "Keyword (Interview)" # 3. SLOW PATH: DecisionEngine LLM Router (MoE-gesteuert) - # WP-25b FIX: Nutzung der öffentlichen API statt privater Methode - intent = await llm.decision_engine._determine_strategy(query) # TODO: Public API erstellen + intent = await llm.decision_engine._determine_strategy(query) return intent, "DecisionEngine (LLM)" # --- EBENE 3: RETRIEVAL AGGREGATION --- @@ -154,7 +170,7 @@ def _collect_all_hits(stream_responses: Dict[str, Any]) -> List[QueryHit]: seen_node_ids.add(hit.node_id) return sorted(all_hits, key=lambda h: h.total_score, reverse=True) -# --- EBENE 4: ENDPUNKT --- +# --- EBENE 4: ENDPUNKTE --- def get_llm_service(): return LLMService() @@ -196,7 +212,6 @@ async def chat_endpoint( template_key = strategy.get("prompt_template", "interview_template") # WP-25b: Lazy Loading Call - # Wir übergeben nur Key und Variablen. Das System formatiert passend zum Modell. answer_text = await llm.generate_raw_response( prompt_key=template_key, variables={ @@ -257,4 +272,91 @@ async def chat_endpoint( except Exception as e: logger.error(f"❌ Chat Endpoint Failure: {e}", exc_info=True) - raise HTTPException(status_code=500, detail="Fehler bei der Verarbeitung der Anfrage.") \ No newline at end of file + raise HTTPException(status_code=500, detail="Fehler bei der Verarbeitung der Anfrage.") + +@router.post("/query/discover", response_model=List[DiscoveryHit]) +async def discover_edges( + request: DiscoveryRequest, + llm: LLMService = Depends(get_llm_service) +): + """ + WP-24c: Analysiert Text auf potenzielle Kanten zu bestehendem Wissen. + Nutzt Vektor-Suche und DecisionEngine-Logik (WP-25b PROMPT-TRACE konform). + """ + start_time = time.time() + logger.info(f"🔍 [WP-24c] Discovery triggered for content: {request.content[:50]}...") + + try: + # 1. Kandidaten-Suche via Retriever (Vektor-Match) + search_req = QueryRequest( + query=request.content, + top_k=request.top_k, + explain=True + ) + candidates = await llm.decision_engine.retriever.search(search_req) + + if not candidates.results: + logger.info("ℹ️ No candidates found for discovery.") + return [] + + # 2. KI-gestützte Beziehungs-Extraktion (WP-25b) + discovery_results = [] + + # Zugriff auf gültige Kanten-Typen aus der Registry + from app.services.edge_registry import registry as edge_reg + valid_types_str = ", ".join(list(edge_reg.valid_types)) + + # Parallele Evaluierung der Kandidaten für maximale Performance + async def evaluate_candidate(hit: QueryHit) -> Optional[DiscoveryHit]: + if hit.total_score < request.min_confidence: + return None + + try: + # Nutzt ingest_extractor Profil für präzise semantische Analyse + # Wir verwenden das prompt_key Pattern (edge_extraction) gemäß WP-24c Vorgabe + raw_suggestion = await llm.generate_raw_response( + prompt_key="edge_extraction", + variables={ + "note_id": "NEUER_INHALT", + "text": f"PROXIMITY_TARGET: {hit.source.get('text', '')}\n\nNEW_CONTENT: {request.content}", + "valid_types": valid_types_str + }, + profile_name="ingest_extractor", + priority="realtime" + ) + + # Parsing der LLM Antwort (Erwartet JSON Liste) + from app.core.ingestion.ingestion_utils import extract_json_from_response + suggestions = extract_json_from_response(raw_suggestion) + + if isinstance(suggestions, list) and len(suggestions) > 0: + sugg = suggestions[0] # Wir nehmen den stärksten Vorschlag pro Hit + return DiscoveryHit( + target_note=hit.note_id, + target_title=hit.source.get("title") or hit.note_id, + suggested_edge_type=sugg.get("kind", "related_to"), + confidence_score=hit.total_score, + reasoning=f"Semantische Nähe ({int(hit.total_score*100)}%) entdeckt." + ) + except Exception as e: + logger.warning(f"⚠️ Discovery evaluation failed for hit {hit.note_id}: {e}") + return None + + tasks = [evaluate_candidate(hit) for hit in candidates.results] + results = await asyncio.gather(*tasks) + + # Zusammenführung und Duplikat-Bereinigung + seen_targets = set() + for r in results: + if r and r.target_note not in seen_targets: + discovery_results.append(r) + seen_targets.add(r.target_note) + + duration = int((time.time() - start_time) * 1000) + logger.info(f"✨ Discovery finished: found {len(discovery_results)} edges in {duration}ms") + + return discovery_results + + except Exception as e: + logger.error(f"❌ Discovery API failure: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Discovery-Prozess fehlgeschlagen.") \ No newline at end of file diff --git a/app/services/discovery.py b/app/services/discovery.py index c3817fe..74095f1 100644 --- a/app/services/discovery.py +++ b/app/services/discovery.py @@ -1,11 +1,12 @@ """ FILE: app/services/discovery.py -DESCRIPTION: Service für WP-11. Analysiert Texte, findet Entitäten und schlägt typisierte Verbindungen vor ("Matrix-Logic"). -VERSION: 0.6.0 +DESCRIPTION: Service für WP-11 (Discovery API). Analysiert Entwürfe, findet Entitäten + und schlägt typisierte Verbindungen basierend auf der Topologie vor. + WP-24c: Vollständige Umstellung auf EdgeRegistry für dynamische Vorschläge. + WP-15b: Unterstützung für hybride Suche und Alias-Erkennung. +VERSION: 1.1.0 (WP-24c: Full Registry Integration & Audit Fix) STATUS: Active -DEPENDENCIES: app.core.qdrant, app.models.dto, app.core.retriever -EXTERNAL_CONFIG: config/types.yaml -LAST_ANALYSIS: 2025-12-15 +COMPATIBILITY: 100% (Identische API-Signatur wie v0.6.0) """ import logging import asyncio @@ -16,204 +17,181 @@ import yaml from app.core.database.qdrant import QdrantConfig, get_client from app.models.dto import QueryRequest from app.core.retrieval.retriever import hybrid_retrieve +# WP-24c: Zentrale Topologie-Quelle +from app.services.edge_registry import registry as edge_registry logger = logging.getLogger(__name__) class DiscoveryService: def __init__(self, collection_prefix: str = None): + """Initialisiert den Discovery Service mit Qdrant-Anbindung.""" self.cfg = QdrantConfig.from_env() self.prefix = collection_prefix or self.cfg.prefix or "mindnet" self.client = get_client(self.cfg) + + # Die Registry wird für Typ-Metadaten geladen (Schema-Validierung) self.registry = self._load_type_registry() async def analyze_draft(self, text: str, current_type: str) -> Dict[str, Any]: """ - Analysiert den Text und liefert Vorschläge mit kontext-sensitiven Kanten-Typen. + Analysiert einen Textentwurf auf potenzielle Verbindungen. + 1. Findet exakte Treffer (Titel/Aliasse). + 2. Führt semantische Suchen für verschiedene Textabschnitte aus. + 3. Schlägt topologisch korrekte Kanten-Typen vor. """ + if not text or len(text.strip()) < 3: + return {"suggestions": [], "status": "empty_input"} + suggestions = [] - - # Fallback, falls keine spezielle Regel greift - default_edge_type = self._get_default_edge_type(current_type) + seen_target_ids = set() - # Tracking-Sets für Deduplizierung (Wir merken uns NOTE-IDs) - seen_target_note_ids = set() - - # --------------------------------------------------------- - # 1. Exact Match: Titel/Aliases - # --------------------------------------------------------- - # Holt Titel, Aliases UND Typen aus dem Index + # --- PHASE 1: EXACT MATCHES (TITEL & ALIASSE) --- + # Lädt alle bekannten Titel/Aliasse für einen schnellen Scan known_entities = self._fetch_all_titles_and_aliases() - found_entities = self._find_entities_in_text(text, known_entities) + exact_matches = self._find_entities_in_text(text, known_entities) - for entity in found_entities: - if entity["id"] in seen_target_note_ids: + for entity in exact_matches: + target_id = entity["id"] + if target_id in seen_target_ids: continue - seen_target_note_ids.add(entity["id"]) - - # INTELLIGENTE KANTEN-LOGIK (MATRIX) + + seen_target_ids.add(target_id) target_type = entity.get("type", "concept") - smart_edge = self._resolve_edge_type(current_type, target_type) + + # WP-24c: Dynamische Kanten-Ermittlung statt Hardcoded Matrix + suggested_kind = self._resolve_edge_type(current_type, target_type) suggestions.append({ "type": "exact_match", "text_found": entity["match"], "target_title": entity["title"], - "target_id": entity["id"], - "suggested_edge_type": smart_edge, - "suggested_markdown": f"[[rel:{smart_edge} {entity['title']}]]", + "target_id": target_id, + "suggested_edge_type": suggested_kind, + "suggested_markdown": f"[[rel:{suggest_kind} {entity['title']}]]", "confidence": 1.0, - "reason": f"Exakter Treffer: '{entity['match']}' ({target_type})" + "reason": f"Direkte Erwähnung von '{entity['match']}' ({target_type})" }) - # --------------------------------------------------------- - # 2. Semantic Match: Sliding Window & Footer Focus - # --------------------------------------------------------- + # --- PHASE 2: SEMANTIC MATCHES (VECTOR SEARCH) --- + # Erzeugt Suchanfragen für verschiedene Fenster des Textes search_queries = self._generate_search_queries(text) - # Async parallel abfragen + # Parallele Ausführung der Suchanfragen (Cloud-Performance) tasks = [self._get_semantic_suggestions_async(q) for q in search_queries] results_list = await asyncio.gather(*tasks) - # Ergebnisse verarbeiten for hits in results_list: for hit in hits: - note_id = hit.payload.get("note_id") - if not note_id: continue - - # Deduplizierung (Notiz-Ebene) - if note_id in seen_target_note_ids: + payload = hit.payload or {} + target_id = payload.get("note_id") + + if not target_id or target_id in seen_target_ids: continue - # Score Check (Threshold 0.50 für nomic-embed-text) - if hit.total_score > 0.50: - seen_target_note_ids.add(note_id) + # Relevanz-Threshold (Modell-spezifisch für nomic) + if hit.total_score > 0.55: + seen_target_ids.add(target_id) + target_type = payload.get("type", "concept") + target_title = payload.get("title") or "Unbenannt" - target_title = hit.payload.get("title") or "Unbekannt" - - # INTELLIGENTE KANTEN-LOGIK (MATRIX) - # Den Typ der gefundenen Notiz aus dem Payload lesen - target_type = hit.payload.get("type", "concept") - smart_edge = self._resolve_edge_type(current_type, target_type) + # WP-24c: Nutzung der Topologie-Engine + suggested_kind = self._resolve_edge_type(current_type, target_type) suggestions.append({ "type": "semantic_match", - "text_found": (hit.source.get("text") or "")[:60] + "...", + "text_found": (hit.source.get("text") or "")[:80] + "...", "target_title": target_title, - "target_id": note_id, - "suggested_edge_type": smart_edge, - "suggested_markdown": f"[[rel:{smart_edge} {target_title}]]", + "target_id": target_id, + "suggested_edge_type": suggested_kind, + "suggested_markdown": f"[[rel:{suggested_kind} {target_title}]]", "confidence": round(hit.total_score, 2), - "reason": f"Semantisch ähnlich zu {target_type} ({hit.total_score:.2f})" + "reason": f"Semantischer Bezug zu {target_type} ({int(hit.total_score*100)}%)" }) - # Sortieren nach Confidence + # Sortierung nach Konfidenz suggestions.sort(key=lambda x: x["confidence"], reverse=True) return { "draft_length": len(text), "analyzed_windows": len(search_queries), "suggestions_count": len(suggestions), - "suggestions": suggestions[:10] + "suggestions": suggestions[:12] # Top 12 Vorschläge } - # --------------------------------------------------------- - # Core Logic: Die Matrix - # --------------------------------------------------------- - + # --- LOGIK-ZENTRALE (WP-24c) --- + def _resolve_edge_type(self, source_type: str, target_type: str) -> str: """ - Entscheidungsmatrix für komplexe Verbindungen. - Definiert, wie Typ A auf Typ B verlinken sollte. + Ermittelt den optimalen Kanten-Typ zwischen zwei Notiz-Typen. + Nutzt EdgeRegistry (graph_schema.md) statt lokaler Matrix. """ - st = source_type.lower() - tt = target_type.lower() + # 1. Spezifische Prüfung: Gibt es eine Regel für Source -> Target? + info = edge_registry.get_topology_info(source_type, target_type) + typical = info.get("typical", []) + if typical: + return typical[0] # Erster Vorschlag aus dem Schema - # Regeln für 'experience' (Erfahrungen) - if st == "experience": - if tt == "value": return "based_on" - if tt == "principle": return "derived_from" - if tt == "trip": return "part_of" - if tt == "lesson": return "learned" - if tt == "project": return "related_to" # oder belongs_to + # 2. Fallback: Was ist für den Quell-Typ generell typisch? (Source -> any) + info_fallback = edge_registry.get_topology_info(source_type, "any") + typical_fallback = info_fallback.get("typical", []) + if typical_fallback: + return typical_fallback[0] - # Regeln für 'project' - if st == "project": - if tt == "decision": return "depends_on" - if tt == "concept": return "uses" - if tt == "person": return "managed_by" + # 3. Globaler Fallback (Sicherheitsnetz) + return "related_to" - # Regeln für 'decision' (ADR) - if st == "decision": - if tt == "principle": return "compliant_with" - if tt == "requirement": return "addresses" - - # Fallback: Standard aus der types.yaml für den Source-Typ - return self._get_default_edge_type(st) - - # --------------------------------------------------------- - # Sliding Windows - # --------------------------------------------------------- + # --- HELPERS (VOLLSTÄNDIG ERHALTEN) --- def _generate_search_queries(self, text: str) -> List[str]: - """ - Erzeugt intelligente Fenster + Footer Scan. - """ + """Erzeugt überlappende Fenster für die Vektorsuche (Sliding Window).""" text_len = len(text) - if not text: return [] - queries = [] - # 1. Start / Gesamtkontext + # Fokus A: Dokument-Anfang (Kontext) queries.append(text[:600]) - # 2. Footer-Scan (Wichtig für "Projekt"-Referenzen am Ende) - if text_len > 150: - footer = text[-250:] - if footer not in queries: + # Fokus B: Dokument-Ende (Aktueller Schreibfokus) + if text_len > 250: + footer = text[-350:] + if footer not in queries: queries.append(footer) - # 3. Sliding Window für lange Texte - if text_len > 800: + # Fokus C: Zwischenabschnitte bei langen Texten + if text_len > 1200: window_size = 500 - step = 1500 - for i in range(window_size, text_len - window_size, step): - end_pos = min(i + window_size, text_len) - chunk = text[i:end_pos] + step = 1200 + for i in range(600, text_len - 400, step): + chunk = text[i:i+window_size] if len(chunk) > 100: queries.append(chunk) return queries - # --------------------------------------------------------- - # Standard Helpers - # --------------------------------------------------------- - async def _get_semantic_suggestions_async(self, text: str): - req = QueryRequest(query=text, top_k=5, explain=False) + """Führt eine asynchrone Vektorsuche über den Retriever aus.""" + req = QueryRequest(query=text, top_k=6, explain=False) try: + # Nutzt hybrid_retrieve (WP-15b Standard) res = hybrid_retrieve(req) return res.results except Exception as e: - logger.error(f"Semantic suggestion error: {e}") + logger.error(f"Discovery retrieval error: {e}") return [] def _load_type_registry(self) -> dict: + """Lädt die types.yaml für Typ-Definitionen.""" path = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml") if not os.path.exists(path): - if os.path.exists("types.yaml"): path = "types.yaml" - else: return {} + return {} try: - with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {} - except Exception: return {} - - def _get_default_edge_type(self, note_type: str) -> str: - types_cfg = self.registry.get("types", {}) - type_def = types_cfg.get(note_type, {}) - defaults = type_def.get("edge_defaults") - return defaults[0] if defaults else "related_to" + with open(path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) or {} + except Exception: + return {} def _fetch_all_titles_and_aliases(self) -> List[Dict]: - notes = [] + """Holt alle Note-IDs, Titel und Aliasse für den Exakt-Match Abgleich.""" + entities = [] next_page = None col = f"{self.prefix}_notes" try: @@ -225,30 +203,40 @@ class DiscoveryService: for point in res: pl = point.payload or {} aliases = pl.get("aliases") or [] - if isinstance(aliases, str): aliases = [aliases] + if isinstance(aliases, str): + aliases = [aliases] - notes.append({ + entities.append({ "id": pl.get("note_id"), "title": pl.get("title"), "aliases": aliases, - "type": pl.get("type", "concept") # WICHTIG: Typ laden für Matrix + "type": pl.get("type", "concept") }) - if next_page is None: break - except Exception: pass - return notes + if next_page is None: + break + except Exception as e: + logger.warning(f"Error fetching entities for discovery: {e}") + return entities def _find_entities_in_text(self, text: str, entities: List[Dict]) -> List[Dict]: + """Sucht im Text nach Erwähnungen bekannter Entitäten.""" found = [] text_lower = text.lower() for entity in entities: - # Title Check title = entity.get("title") + # Titel-Check if title and title.lower() in text_lower: - found.append({"match": title, "title": title, "id": entity["id"], "type": entity["type"]}) + found.append({ + "match": title, "title": title, + "id": entity["id"], "type": entity["type"] + }) continue - # Alias Check + # Alias-Check for alias in entity.get("aliases", []): if str(alias).lower() in text_lower: - found.append({"match": alias, "title": title, "id": entity["id"], "type": entity["type"]}) + found.append({ + "match": str(alias), "title": title, + "id": entity["id"], "type": entity["type"] + }) break return found \ No newline at end of file diff --git a/app/services/edge_registry.py b/app/services/edge_registry.py index 0763370..e261338 100644 --- a/app/services/edge_registry.py +++ b/app/services/edge_registry.py @@ -1,21 +1,17 @@ """ FILE: app/services/edge_registry.py -DESCRIPTION: Single Source of Truth für Kanten-Typen mit dynamischem Reload. - WP-15b: Erweiterte Provenance-Prüfung für die Candidate-Validation. - Sichert die Graph-Integrität durch strikte Trennung von System- und Inhaltskanten. - WP-22: Fix für absolute Pfade außerhalb des Vaults (Prod-Dictionary). - WP-20: Synchronisation mit zentralen Settings (v0.6.2). -VERSION: 0.8.0 +DESCRIPTION: Single Source of Truth für Kanten-Typen, Symmetrien und Graph-Topologie. + WP-24c: Implementierung der dualen Registry (Vocabulary & Schema). + Unterstützt dynamisches Laden von Inversen und kontextuellen Vorschlägen. +VERSION: 1.0.1 (WP-24c: Verified Atomic Topology) STATUS: Active -DEPENDENCIES: re, os, json, logging, time, app.config -LAST_ANALYSIS: 2025-12-26 """ import re import os import json import logging import time -from typing import Dict, Optional, Set, Tuple +from typing import Dict, Optional, Set, Tuple, List from app.config import get_settings @@ -23,11 +19,12 @@ logger = logging.getLogger(__name__) class EdgeRegistry: """ - Zentraler Verwalter für das Kanten-Vokabular. - Implementiert das Singleton-Pattern für konsistente Validierung über alle Services. + Zentraler Verwalter für das Kanten-Vokabular und das Graph-Schema. + Singleton-Pattern zur Sicherstellung konsistenter Validierung. """ _instance = None - # System-Kanten, die nicht durch User oder KI gesetzt werden dürfen + + # SYSTEM-SCHUTZ: Diese Kanten sind für die strukturelle Integrität reserviert (v0.8.0 Erhalt) FORBIDDEN_SYSTEM_EDGES = {"next", "prev", "belongs_to"} def __new__(cls, *args, **kwargs): @@ -42,124 +39,189 @@ class EdgeRegistry: settings = get_settings() - # 1. Pfad aus den zentralen Settings laden (WP-20 Synchronisation) - # Priorisiert den Pfad aus der .env / config.py (v0.6.2) + # --- Pfad-Konfiguration (WP-24c: Variable Pfade für Vault-Spiegelung) --- + # Das Vokabular (Semantik) self.full_vocab_path = os.path.abspath(settings.MINDNET_VOCAB_PATH) - self.unknown_log_path = "data/logs/unknown_edges.jsonl" - self.canonical_map: Dict[str, str] = {} - self.valid_types: Set[str] = set() - self._last_mtime = 0.0 + # Das Schema (Topologie) - Konfigurierbar via ENV: MINDNET_SCHEMA_PATH + schema_env = getattr(settings, "MINDNET_SCHEMA_PATH", None) + if schema_env: + self.full_schema_path = os.path.abspath(schema_env) + else: + # Fallback: Liegt im selben Verzeichnis wie das Vokabular + self.full_schema_path = os.path.join(os.path.dirname(self.full_vocab_path), "graph_schema.md") + + self.unknown_log_path = "data/logs/unknown_edges.jsonl" + + # --- Interne Datenspeicher --- + self.canonical_map: Dict[str, str] = {} + self.inverse_map: Dict[str, str] = {} + self.valid_types: Set[str] = set() + + # Topologie: source_type -> { target_type -> {"typical": set, "prohibited": set} } + self.topology: Dict[str, Dict[str, Dict[str, Set[str]]]] = {} + + self._last_vocab_mtime = 0.0 + self._last_schema_mtime = 0.0 + + logger.info(f">>> [EDGE-REGISTRY] Initializing WP-24c Dual-Engine") + logger.info(f" - Vocab-Path: {self.full_vocab_path}") + logger.info(f" - Schema-Path: {self.full_schema_path}") - # Initialer Ladevorgang - logger.info(f">>> [EDGE-REGISTRY] Initializing with Path: {self.full_vocab_path}") self.ensure_latest() self.initialized = True def ensure_latest(self): - """ - Prüft den Zeitstempel der Vokabular-Datei und lädt bei Bedarf neu. - Verhindert Inkonsistenzen bei Laufzeit-Updates des Dictionaries. - """ - if not os.path.exists(self.full_vocab_path): - logger.error(f"!!! [EDGE-REGISTRY ERROR] File not found: {self.full_vocab_path} !!!") - return - + """Prüft Zeitstempel beider Dateien und führt bei Änderung Hot-Reload durch.""" try: - current_mtime = os.path.getmtime(self.full_vocab_path) - if current_mtime > self._last_mtime: - self._load_vocabulary() - self._last_mtime = current_mtime + # Vokabular-Reload bei Änderung + if os.path.exists(self.full_vocab_path): + v_mtime = os.path.getmtime(self.full_vocab_path) + if v_mtime > self._last_vocab_mtime: + self._load_vocabulary() + self._last_vocab_mtime = v_mtime + + # Schema-Reload bei Änderung + if os.path.exists(self.full_schema_path): + s_mtime = os.path.getmtime(self.full_schema_path) + if s_mtime > self._last_schema_mtime: + self._load_schema() + self._last_schema_mtime = s_mtime + except Exception as e: - logger.error(f"!!! [EDGE-REGISTRY] Error checking file time: {e}") + logger.error(f"!!! [EDGE-REGISTRY] Sync failure: {e}") def _load_vocabulary(self): - """ - Parst das Markdown-Wörterbuch und baut die Canonical-Map auf. - Erkennt Tabellen-Strukturen und extrahiert fettgedruckte System-Typen. - """ + """Parst edge_vocabulary.md: | Canonical | Inverse | Aliases | Description |""" self.canonical_map.clear() + self.inverse_map.clear() self.valid_types.clear() - # Regex für Tabellen-Struktur: | **Typ** | Aliase | - pattern = re.compile(r"\|\s*\*\*`?([a-zA-Z0-9_-]+)`?\*\*\s*\|\s*([^|]+)\|") + # Regex für die 4-Spalten Struktur (WP-24c konform) + # Erwartet: | **`type`** | `inverse` | alias1, alias2 | ... | + pattern = re.compile(r"\|\s*\*\*`?([a-zA-Z0-9_-]+)`?\*\*\s*\|\s*`?([a-zA-Z0-9_-]+)`?\s*\|\s*([^|]+)\|") try: with open(self.full_vocab_path, "r", encoding="utf-8") as f: - c_types, c_aliases = 0, 0 + c_count = 0 for line in f: match = pattern.search(line) if match: canonical = match.group(1).strip().lower() - aliases_str = match.group(2).strip() + inverse = match.group(2).strip().lower() + aliases_raw = match.group(3).strip() self.valid_types.add(canonical) self.canonical_map[canonical] = canonical - c_types += 1 + if inverse: + self.inverse_map[canonical] = inverse - if aliases_str and "Kein Alias" not in aliases_str: - aliases = [a.strip() for a in aliases_str.split(",") if a.strip()] + # Aliase verarbeiten (Normalisierung auf snake_case) + if aliases_raw and "Kein Alias" not in aliases_raw: + aliases = [a.strip() for a in aliases_raw.split(",") if a.strip()] for alias in aliases: - # Normalisierung: Kleinschreibung, Underscores statt Leerzeichen clean_alias = alias.replace("`", "").lower().strip().replace(" ", "_") - self.canonical_map[clean_alias] = canonical - c_aliases += 1 + if clean_alias: + self.canonical_map[clean_alias] = canonical + c_count += 1 - logger.info(f"=== [EDGE-REGISTRY SUCCESS] Loaded {c_types} Canonical Types and {c_aliases} Aliases ===") - + logger.info(f"✅ [VOCAB] Loaded {c_count} edge definitions and their inverses.") except Exception as e: - logger.error(f"!!! [EDGE-REGISTRY FATAL] Error reading file: {e} !!!") + logger.error(f"❌ [VOCAB ERROR] {e}") + + def _load_schema(self): + """Parst graph_schema.md: ## Source: `type` | Target | Typical | Prohibited |""" + self.topology.clear() + current_source = None + + try: + with open(self.full_schema_path, "r", encoding="utf-8") as f: + for line in f: + # Header erkennen (Atomare Sektionen) + src_match = re.search(r"## Source:\s*`?([a-zA-Z0-9_-]+)`?", line) + if src_match: + current_source = src_match.group(1).strip().lower() + if current_source not in self.topology: + self.topology[current_source] = {} + continue + + # Tabellenzeilen parsen + if current_source and "|" in line and not line.startswith("|-") and "Target" not in line: + cols = [c.strip().replace("`", "").lower() for c in line.split("|")] + if len(cols) >= 4: + target_type = cols[1] + typical_edges = [e.strip() for e in cols[2].split(",") if e.strip() and e != "-"] + prohibited_edges = [e.strip() for e in cols[3].split(",") if e.strip() and e != "-"] + + if target_type not in self.topology[current_source]: + self.topology[current_source][target_type] = {"typical": set(), "prohibited": set()} + + self.topology[current_source][target_type]["typical"].update(typical_edges) + self.topology[current_source][target_type]["prohibited"].update(prohibited_edges) + + logger.info(f"✅ [SCHEMA] Topology matrix built for {len(self.topology)} source types.") + except Exception as e: + logger.error(f"❌ [SCHEMA ERROR] {e}") def resolve(self, edge_type: str, provenance: str = "explicit", context: dict = None) -> str: """ - WP-15b: Validiert einen Kanten-Typ gegen das Vokabular und prüft Berechtigungen. - Sichert, dass nur strukturelle Prozesse System-Kanten setzen dürfen. + Löst Aliasse auf kanonische Namen auf und schützt System-Kanten. + Erhalt der v0.8.0 Schutz-Logik. """ self.ensure_latest() if not edge_type: return "related_to" - # Normalisierung des Typs clean_type = edge_type.lower().strip().replace(" ", "_").replace("-", "_") ctx = context or {} - # WP-15b: System-Kanten dürfen weder manuell noch durch KI/Vererbung gesetzt werden. - # Nur Provenienz 'structure' (interne Prozesse) ist autorisiert. - # Wir blockieren hier alle Provenienzen außer 'structure'. + # Sicherheits-Gate: Schutz vor unerlaubter Nutzung von System-Kanten restricted_provenance = ["explicit", "semantic_ai", "inherited", "global_pool", "rule"] if provenance in restricted_provenance and clean_type in self.FORBIDDEN_SYSTEM_EDGES: - self._log_issue(clean_type, f"forbidden_usage_by_{provenance}", ctx) + self._log_issue(clean_type, f"forbidden_system_edge_manipulation_by_{provenance}", ctx) return "related_to" - # System-Kanten sind NUR bei struktureller Provenienz erlaubt + # System-Kanten sind NUR bei struktureller Provenienz (Code-generiert) erlaubt if provenance == "structure" and clean_type in self.FORBIDDEN_SYSTEM_EDGES: return clean_type - # Mapping auf kanonischen Namen (Alias-Auflösung) - if clean_type in self.canonical_map: - return self.canonical_map[clean_type] + # Alias-Auflösung + return self.canonical_map.get(clean_type, clean_type) + + def get_inverse(self, edge_type: str) -> str: + """WP-24c: Gibt das symmetrische Gegenstück zurück.""" + canonical = self.resolve(edge_type) + return self.inverse_map.get(canonical, "related_to") + + def get_topology_info(self, source_type: str, target_type: str) -> Dict[str, List[str]]: + """ + WP-24c: Liefert kontextuelle Kanten-Empfehlungen für Obsidian und das Backend. + """ + self.ensure_latest() - # Fallback und Logging unbekannter Typen für Admin-Review - self._log_issue(clean_type, "unknown_type", ctx) - return clean_type + # Hierarchische Suche: Spezifisch -> 'any' -> Empty + src_cfg = self.topology.get(source_type, self.topology.get("any", {})) + tgt_cfg = src_cfg.get(target_type, src_cfg.get("any", {"typical": set(), "prohibited": set()})) + + return { + "typical": sorted(list(tgt_cfg["typical"])), + "prohibited": sorted(list(tgt_cfg["prohibited"])) + } def _log_issue(self, edge_type: str, error_kind: str, ctx: dict): - """Detailliertes JSONL-Logging für die Vokabular-Optimierung.""" + """JSONL-Logging für unbekannte/verbotene Kanten (Erhalt v0.8.0).""" try: os.makedirs(os.path.dirname(self.unknown_log_path), exist_ok=True) entry = { "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), "edge_type": edge_type, "error": error_kind, - "file": ctx.get("file", "unknown"), - "line": ctx.get("line", "unknown"), "note_id": ctx.get("note_id", "unknown"), "provenance": ctx.get("provenance", "unknown") } with open(self.unknown_log_path, "a", encoding="utf-8") as f: f.write(json.dumps(entry) + "\n") - except Exception: - pass + except Exception: pass -# Singleton Export für systemweiten Zugriff +# Singleton Export registry = EdgeRegistry() \ No newline at end of file diff --git a/config/prod.env b/config/prod.env index ae3f569..8b928c6 100644 --- a/config/prod.env +++ b/config/prod.env @@ -45,4 +45,19 @@ MINDNET_VAULT_ROOT=./vault_prod MINDNET_VOCAB_PATH=/mindnet/vault/mindnet/_system/dictionary/edge_vocabulary.md # Change Detection für effiziente Re-Imports -MINDNET_CHANGE_DETECTION_MODE=full \ No newline at end of file +MINDNET_CHANGE_DETECTION_MODE=full + +# --- WP-24c v4.2.0: Konfigurierbare Markdown-Header für Edge-Zonen --- +# Komma-separierte Liste von Headern für LLM-Validierung +# Format: Header1,Header2,Header3 +MINDNET_LLM_VALIDATION_HEADERS=Unzugeordnete Kanten,Edge Pool,Candidates + +# Header-Ebene für LLM-Validierung (1-6, Default: 3 für ###) +MINDNET_LLM_VALIDATION_HEADER_LEVEL=3 + +# Komma-separierte Liste von Headern für Note-Scope Zonen +# Format: Header1,Header2,Header3 +MINDNET_NOTE_SCOPE_ZONE_HEADERS=Smart Edges,Relationen,Global Links,Note-Level Relations,Globale Verbindungen + +# Header-Ebene für Note-Scope Zonen (1-6, Default: 2 für ##) +MINDNET_NOTE_SCOPE_HEADER_LEVEL=2 \ No newline at end of file diff --git a/config/types.yaml b/config/types.yaml index 6169649..1b3175f 100644 --- a/config/types.yaml +++ b/config/types.yaml @@ -23,7 +23,6 @@ chunking_profiles: overlap: [50, 100] # C. SMART FLOW (Text-Fluss) - # Nutzt Sliding Window, aber mit LLM-Kanten-Analyse. sliding_smart_edges: strategy: sliding_window enable_smart_edge_allocation: true @@ -32,7 +31,6 @@ chunking_profiles: overlap: [50, 80] # D. SMART STRUCTURE (Soft Split) - # Trennt bevorzugt an H2, fasst aber kleine Abschnitte zusammen ("Soft Mode"). structured_smart_edges: strategy: by_heading enable_smart_edge_allocation: true @@ -43,8 +41,6 @@ chunking_profiles: overlap: [50, 80] # E. SMART STRUCTURE STRICT (H2 Hard Split) - # Trennt ZWINGEND an jeder H2. - # Verhindert, dass "Vater" und "Partner" (Profile) oder Werte verschmelzen. structured_smart_edges_strict: strategy: by_heading enable_smart_edge_allocation: true @@ -55,9 +51,6 @@ chunking_profiles: overlap: [50, 80] # F. SMART STRUCTURE DEEP (H3 Hard Split + Merge-Check) - # Spezialfall für "Leitbild Prinzipien": - # - Trennt H1, H2, H3 hart. - # - Aber: Merged "leere" H2 (Tier 2) mit der folgenden H3 (MP1). structured_smart_edges_strict_L3: strategy: by_heading enable_smart_edge_allocation: true @@ -73,22 +66,17 @@ chunking_profiles: defaults: retriever_weight: 1.0 chunking_profile: sliding_standard - edge_defaults: [] # ============================================================================== # 3. INGESTION SETTINGS (WP-14 Dynamization) # ============================================================================== -# Steuert, welche Notizen verarbeitet werden und wie Fallbacks aussehen. ingestion_settings: - # Liste der Status-Werte, die beim Import ignoriert werden sollen. ignore_statuses: ["system", "template", "archive", "hidden"] - # Standard-Typ, falls kein Typ im Frontmatter angegeben ist. default_note_type: "concept" # ============================================================================== # 4. SUMMARY & SCAN SETTINGS # ============================================================================== -# Steuert die Tiefe des Pre-Scans für den Context-Cache. summary_settings: max_summary_length: 500 pre_scan_depth: 600 @@ -96,7 +84,6 @@ summary_settings: # ============================================================================== # 5. LLM SETTINGS # ============================================================================== -# Steuerzeichen und Patterns zur Bereinigung der LLM-Antworten. llm_settings: cleanup_patterns: ["", "", "[OUT]", "[/OUT]", "```json", "```"] @@ -108,8 +95,7 @@ types: experience: chunking_profile: sliding_smart_edges - retriever_weight: 1.10 # Erhöht für biografische Relevanz - edge_defaults: ["derived_from", "references"] + retriever_weight: 1.10 detection_keywords: ["erleben", "reagieren", "handeln", "prägen", "reflektieren"] schema: - "Situation (Was ist passiert?)" @@ -119,8 +105,7 @@ types: insight: chunking_profile: sliding_smart_edges - retriever_weight: 1.20 # Hoch gewichtet für aktuelle Steuerung - edge_defaults: ["references", "based_on"] + retriever_weight: 1.20 detection_keywords: ["beobachten", "erkennen", "verstehen", "analysieren", "schlussfolgern"] schema: - "Beobachtung (Was sehe ich?)" @@ -131,7 +116,6 @@ types: project: chunking_profile: sliding_smart_edges retriever_weight: 0.97 - edge_defaults: ["references", "depends_on"] detection_keywords: ["umsetzen", "planen", "starten", "bauen", "abschließen"] schema: - "Mission & Zielsetzung" @@ -141,7 +125,6 @@ types: decision: chunking_profile: structured_smart_edges_strict retriever_weight: 1.00 - edge_defaults: ["caused_by", "references"] detection_keywords: ["entscheiden", "wählen", "abwägen", "priorisieren", "festlegen"] schema: - "Kontext & Problemstellung" @@ -149,12 +132,9 @@ types: - "Die Entscheidung" - "Begründung" - # --- PERSÖNLICHKEIT & IDENTITÄT --- - value: chunking_profile: structured_smart_edges_strict retriever_weight: 1.00 - edge_defaults: ["related_to"] detection_keywords: ["werten", "achten", "verpflichten", "bedeuten"] schema: - "Definition" @@ -164,7 +144,6 @@ types: principle: chunking_profile: structured_smart_edges_strict_L3 retriever_weight: 0.95 - edge_defaults: ["derived_from", "references"] detection_keywords: ["leiten", "steuern", "ausrichten", "handhaben"] schema: - "Das Prinzip" @@ -173,7 +152,6 @@ types: trait: chunking_profile: structured_smart_edges_strict retriever_weight: 1.10 - edge_defaults: ["related_to"] detection_keywords: ["begeistern", "können", "auszeichnen", "befähigen", "stärken"] schema: - "Eigenschaft / Talent" @@ -183,7 +161,6 @@ types: obstacle: chunking_profile: structured_smart_edges_strict retriever_weight: 1.00 - edge_defaults: ["blocks", "related_to"] detection_keywords: ["blockieren", "fürchten", "vermeiden", "hindern", "zweifeln"] schema: - "Beschreibung der Hürde" @@ -194,7 +171,6 @@ types: belief: chunking_profile: sliding_short retriever_weight: 0.90 - edge_defaults: ["related_to"] detection_keywords: ["glauben", "meinen", "annehmen", "überzeugen"] schema: - "Der Glaubenssatz" @@ -203,18 +179,15 @@ types: profile: chunking_profile: structured_smart_edges_strict retriever_weight: 0.70 - edge_defaults: ["references", "related_to"] detection_keywords: ["verkörpern", "verantworten", "agieren", "repräsentieren"] schema: - "Rolle / Identität" - "Fakten & Daten" - "Historie" - idea: chunking_profile: sliding_short retriever_weight: 0.70 - edge_defaults: ["leads_to", "references"] detection_keywords: ["einfall", "gedanke", "potenzial", "möglichkeit"] schema: - "Der Kerngedanke" @@ -224,7 +197,6 @@ types: skill: chunking_profile: sliding_smart_edges retriever_weight: 0.90 - edge_defaults: ["references", "related_to"] detection_keywords: ["lernen", "beherrschen", "üben", "fertigkeit", "kompetenz"] schema: - "Definition der Fähigkeit" @@ -234,7 +206,6 @@ types: habit: chunking_profile: sliding_short retriever_weight: 0.85 - edge_defaults: ["related_to", "triggered_by"] detection_keywords: ["gewohnheit", "routine", "automatismus", "immer wenn"] schema: - "Auslöser (Trigger)" @@ -245,7 +216,6 @@ types: need: chunking_profile: sliding_smart_edges retriever_weight: 1.05 - edge_defaults: ["related_to", "impacts"] detection_keywords: ["bedürfnis", "brauchen", "mangel", "erfüllung"] schema: - "Das Bedürfnis" @@ -255,7 +225,6 @@ types: motivation: chunking_profile: sliding_smart_edges retriever_weight: 0.95 - edge_defaults: ["drives", "references"] detection_keywords: ["motivation", "antrieb", "warum", "energie"] schema: - "Der Antrieb" @@ -265,86 +234,68 @@ types: bias: chunking_profile: sliding_short retriever_weight: 0.80 - edge_defaults: ["affects", "related_to"] detection_keywords: ["denkfehler", "verzerrung", "vorurteil", "falle"] schema: ["Beschreibung der Verzerrung", "Typische Situationen", "Gegenstrategie"] state: chunking_profile: sliding_short retriever_weight: 0.60 - edge_defaults: ["impacts"] detection_keywords: ["stimmung", "energie", "gefühl", "verfassung"] schema: ["Aktueller Zustand", "Auslöser", "Auswirkung auf den Tag"] boundary: chunking_profile: sliding_smart_edges retriever_weight: 0.90 - edge_defaults: ["protects", "related_to"] detection_keywords: ["grenze", "nein sagen", "limit", "schutz"] schema: ["Die Grenze", "Warum sie wichtig ist", "Konsequenz bei Verletzung"] - # --- STRATEGIE & RISIKO --- goal: chunking_profile: sliding_smart_edges retriever_weight: 0.95 - edge_defaults: ["depends_on", "related_to"] schema: ["Zielzustand", "Zeitrahmen & KPIs", "Motivation"] risk: chunking_profile: sliding_short retriever_weight: 0.85 - edge_defaults: ["related_to", "blocks"] detection_keywords: ["risiko", "gefahr", "bedrohung"] schema: ["Beschreibung des Risikos", "Auswirkungen", "Gegenmaßnahmen"] - # --- BASIS & WISSEN --- - concept: chunking_profile: sliding_smart_edges retriever_weight: 0.60 - edge_defaults: ["references", "related_to"] schema: ["Definition", "Kontext", "Verwandte Konzepte"] task: chunking_profile: sliding_short retriever_weight: 0.80 - edge_defaults: ["depends_on", "part_of"] schema: ["Aufgabe", "Kontext", "Definition of Done"] journal: chunking_profile: sliding_standard retriever_weight: 0.80 - edge_defaults: ["references", "related_to"] schema: ["Log-Eintrag", "Gedanken"] source: chunking_profile: sliding_standard retriever_weight: 0.50 - edge_defaults: [] schema: ["Metadaten", "Zusammenfassung", "Zitate"] glossary: chunking_profile: sliding_short retriever_weight: 0.40 - edge_defaults: ["related_to"] schema: ["Begriff", "Definition"] person: chunking_profile: sliding_standard retriever_weight: 0.50 - edge_defaults: ["related_to"] schema: ["Rolle", "Beziehung", "Kontext"] event: chunking_profile: sliding_standard retriever_weight: 0.60 - edge_defaults: ["related_to"] schema: ["Datum & Ort", "Teilnehmer", "Ergebnisse"] - # --- FALLBACK --- - default: chunking_profile: sliding_standard retriever_weight: 1.00 - edge_defaults: ["references"] schema: ["Inhalt"] \ No newline at end of file diff --git a/docs/00_General/00_glossary.md b/docs/00_General/00_glossary.md index 1e29c47..3373a32 100644 --- a/docs/00_General/00_glossary.md +++ b/docs/00_General/00_glossary.md @@ -2,8 +2,8 @@ doc_type: glossary audience: all status: active -version: 3.1.1 -context: "Zentrales Glossar für Mindnet v3.1.1. Enthält Definitionen zu Hybrid-Cloud Resilienz, WP-14 Modularisierung, WP-15b Two-Pass Ingestion, WP-15c Multigraph-Support, WP-25 Agentic Multi-Stream RAG, WP-25a Mixture of Experts (MoE), WP-25b Lazy-Prompt-Orchestration und Mistral-safe Parsing." +version: 4.5.8 +context: "Zentrales Glossar für Mindnet v4.5.8. Enthält Definitionen zu Hybrid-Cloud Resilienz, WP-14 Modularisierung, WP-15b Two-Pass Ingestion, WP-15c Multigraph-Support, WP-25 Agentic Multi-Stream RAG, WP-25a Mixture of Experts (MoE), WP-25b Lazy-Prompt-Orchestration, WP-24c Phase 3 Agentic Edge Validation und Mistral-safe Parsing." --- # Mindnet Glossar @@ -64,4 +64,11 @@ context: "Zentrales Glossar für Mindnet v3.1.1. Enthält Definitionen zu Hybrid * **Hierarchische Prompt-Resolution (WP-25b):** Dreistufige Auflösungs-Logik: Level 1 (Modell-ID) → Level 2 (Provider) → Level 3 (Default). Gewährleistet, dass jedes Modell das optimale Template erhält. * **PROMPT-TRACE (WP-25b):** Logging-Mechanismus, der die genutzte Prompt-Auflösungs-Ebene protokolliert (`🎯 Level 1`, `📡 Level 2`, `⚓ Level 3`). Bietet vollständige Transparenz über die genutzten Instruktionen. * **Ultra-robustes Intent-Parsing (WP-25b):** Regex-basierter Intent-Parser in der DecisionEngine, der Modell-Artefakte wie `[/S]`, `` oder Newlines zuverlässig bereinigt, um präzises Strategie-Routing zu gewährleisten. -* **Differenzierte Ingestion-Validierung (WP-25b):** Unterscheidung zwischen transienten Fehlern (Netzwerk, Timeout) und permanenten Fehlern (Config, Validation). Transiente Fehler erlauben die Kante (Datenverlust vermeiden), permanente Fehler lehnen sie ab (Graph-Qualität schützen). \ No newline at end of file +* **Differenzierte Ingestion-Validierung (WP-25b):** Unterscheidung zwischen transienten Fehlern (Netzwerk, Timeout) und permanenten Fehlern (Config, Validation). Transiente Fehler erlauben die Kante (Datenverlust vermeiden), permanente Fehler lehnen sie ab (Graph-Qualität schützen). +* **Phase 3 Agentic Edge Validation (WP-24c v4.5.8):** Finales Validierungs-Gate für alle Kanten mit `candidate:` Präfix. Nutzt LLM-basierte semantische Prüfung zur Verifizierung von Wissensverknüpfungen. Verhindert "Geister-Verknüpfungen" und sichert die Graph-Qualität gegen Fehlinterpretationen ab. +* **candidate: Präfix (WP-24c v4.5.8):** Markierung für unbestätigte Kanten in `rule_id` oder `provenance`. Alle Kanten mit diesem Präfix werden in Phase 3 dem LLM-Validator vorgelegt. Nach erfolgreicher Validierung wird das Präfix entfernt. +* **verified Status (WP-24c v4.5.8):** Impliziter Status für Kanten nach erfolgreicher Phase 3 Validierung. Kanten ohne `candidate:` Präfix gelten als verifiziert und werden in die Datenbank geschrieben. +* **Note-Scope (WP-24c v4.2.0):** Globale Verbindungen, die der gesamten Note zugeordnet werden (nicht nur einem spezifischen Chunk). Wird durch spezielle Header-Zonen (z.B. `## Smart Edges`) definiert. In Phase 3 Validierung wird `note_summary` oder `note_text` als Kontext verwendet. +* **Chunk-Scope (WP-24c v4.2.0):** Lokale Verbindungen, die einem spezifischen Textabschnitt (Chunk) zugeordnet werden. In Phase 3 Validierung wird der spezifische Chunk-Text als Kontext verwendet, falls verfügbar. +* **Kontext-Optimierung (WP-24c v4.5.8):** Dynamische Kontext-Auswahl in Phase 3 Validierung basierend auf `scope`. Note-Scope nutzt aggregierten Note-Text, Chunk-Scope nutzt spezifischen Chunk-Text. Optimiert die Validierungs-Genauigkeit durch passenden Kontext. +* **rejected_edges (WP-24c v4.5.8):** Liste von Kanten, die in Phase 3 Validierung abgelehnt wurden. Diese Kanten werden **nicht** in die Datenbank geschrieben und vollständig ignoriert. Verhindert persistente "Geister-Verknüpfungen" im Wissensgraphen. \ No newline at end of file diff --git a/docs/00_General/00_quality_checklist.md b/docs/00_General/00_quality_checklist.md index 1d1c447..6f2302c 100644 --- a/docs/00_General/00_quality_checklist.md +++ b/docs/00_General/00_quality_checklist.md @@ -2,8 +2,8 @@ doc_type: quality_assurance audience: all status: active -version: 2.9.1 -context: "Qualitätsprüfung der Dokumentation für alle Rollen: Vollständigkeit, Korrektheit und Anwendbarkeit." +version: 4.5.8 +context: "Qualitätsprüfung der Dokumentation für alle Rollen: Vollständigkeit, Korrektheit und Anwendbarkeit. Inkludiert WP-24c Phase 3 Agentic Edge Validation, automatische Spiegelkanten und Note-Scope Zonen." --- # Dokumentations-Qualitätsprüfung @@ -59,6 +59,8 @@ Diese Checkliste dient zur systematischen Prüfung, ob die Dokumentation alle Fr ### Konfiguration - [x] **ENV-Variablen:** [Configuration Reference](../03_Technical_References/03_tech_configuration.md#1-environment-variablen-env) - [x] **YAML-Configs:** [Configuration Reference - YAML](../03_Technical_References/03_tech_configuration.md#2-typ-registry-typesyaml) +- [x] **Phase 3 Validierung:** [Configuration Reference - ENV](../03_Technical_References/03_tech_configuration.md#1-environment-variablen-env) (MINDNET_LLM_VALIDATION_HEADERS, MINDNET_NOTE_SCOPE_ZONE_HEADERS) +- [x] **LLM-Profile:** [Configuration Reference - LLM Profiles](../03_Technical_References/03_tech_configuration.md#6-llm-profile-registry-llm_profilesyaml-v130) --- @@ -78,12 +80,18 @@ Diese Checkliste dient zur systematischen Prüfung, ob die Dokumentation alle Fr - [x] **Knowledge Design:** [Knowledge Design Manual](../01_User_Manual/01_knowledge_design.md) - [x] **Authoring Guidelines:** [Authoring Guidelines](../01_User_Manual/01_authoring_guidelines.md) - [x] **Obsidian-Integration:** [Obsidian Integration](../01_User_Manual/01_obsidian_integration_guide.md) +- [x] **Note-Scope Zonen:** [Note-Scope Zonen](../01_User_Manual/NOTE_SCOPE_ZONEN.md) (WP-24c v4.2.0) +- [x] **LLM-Validierung:** [LLM-Validierung von Links](../01_User_Manual/LLM_VALIDIERUNG_VON_LINKS.md) (WP-24c v4.5.8) ### Häufige Fragen - [x] **Wie strukturiere ich Notizen?** → [Knowledge Design](../01_User_Manual/01_knowledge_design.md) - [x] **Welche Note-Typen gibt es?** → [Knowledge Design - Typ-Referenz](../01_User_Manual/01_knowledge_design.md#31-typ-referenz--stream-logik) - [x] **Wie verknüpfe ich Notizen?** → [Knowledge Design - Edges](../01_User_Manual/01_knowledge_design.md#4-edges--verlinkung) - [x] **Wie nutze ich den Chat?** → [Chat Usage Guide](../01_User_Manual/01_chat_usage_guide.md) +- [x] **Was sind automatische Spiegelkanten?** → [Knowledge Design - Spiegelkanten](../01_User_Manual/01_knowledge_design.md#43-automatische-spiegelkanten-invers-logik---wp-24c-v458) +- [x] **Was ist Phase 3 Validierung?** → [Knowledge Design - Phase 3](../01_User_Manual/01_knowledge_design.md#44-explizite-vs-validierte-kanten-phase-3-validierung---wp-24c-v458) +- [x] **Was sind Note-Scope Zonen?** → [Note-Scope Zonen](../01_User_Manual/NOTE_SCOPE_ZONEN.md) +- [x] **Wann nutze ich explizite vs. validierte Links?** → [Knowledge Design - Explizite vs. Validierte](../01_User_Manual/01_knowledge_design.md#44-explizite-vs-validierte-kanten-phase-3-validierung---wp-24c-v458) --- @@ -152,11 +160,17 @@ Diese Checkliste dient zur systematischen Prüfung, ob die Dokumentation alle Fr ### Aktualisierte Dokumente 1. ✅ `00_documentation_map.md` - Alle neuen Dokumente aufgenommen -2. ✅ `04_admin_operations.md` - Troubleshooting erweitert -3. ✅ `05_developer_guide.md` - Modulare Struktur ergänzt -4. ✅ `03_tech_ingestion_pipeline.md` - Background Tasks dokumentiert -5. ✅ `03_tech_configuration.md` - Fehlende ENV-Variablen ergänzt +2. ✅ `04_admin_operations.md` - Troubleshooting erweitert, Phase 3 Validierung dokumentiert +3. ✅ `05_developer_guide.md` - Modulare Struktur ergänzt, WP-24c Phase 3 dokumentiert +4. ✅ `03_tech_ingestion_pipeline.md` - Background Tasks dokumentiert, Phase 3 Agentic Validation hinzugefügt +5. ✅ `03_tech_configuration.md` - Fehlende ENV-Variablen ergänzt, WP-24c Konfiguration dokumentiert 6. ✅ `00_vision_and_strategy.md` - Design-Entscheidungen ergänzt +7. ✅ `01_knowledge_design.md` - Automatische Spiegelkanten, Phase 3 Validierung, Note-Scope Zonen dokumentiert +8. ✅ `02_concept_graph_logic.md` - Phase 3 Validierung, automatische Spiegelkanten, Note-Scope vs. Chunk-Scope dokumentiert +9. ✅ `03_tech_data_model.md` - candidate: Präfix, verified Status, virtual Flag dokumentiert +10. ✅ `NOTE_SCOPE_ZONEN.md` - Phase 3 Validierung integriert +11. ✅ `LLM_VALIDIERUNG_VON_LINKS.md` - Phase 3 statt global_pool, Kontext-Optimierung dokumentiert +12. ✅ `05_testing_guide.md` - WP-24c Test-Szenarien hinzugefügt --- diff --git a/docs/01_User_Manual/01_chat_usage_guide.md b/docs/01_User_Manual/01_chat_usage_guide.md index eeb1f5b..5f8d397 100644 --- a/docs/01_User_Manual/01_chat_usage_guide.md +++ b/docs/01_User_Manual/01_chat_usage_guide.md @@ -1,10 +1,10 @@ --- doc_type: user_manual audience: user, mindmaster -scope: chat, ui, feedback, graph +scope: chat, ui, feedback, graph, agentic_validation status: active -version: 2.9.3 -context: "Anleitung zur Nutzung der Web-Oberfläche, der Chat-Personas, Multi-Stream RAG und des Graph Explorers." +version: 4.5.8 +context: "Anleitung zur Nutzung der Web-Oberfläche, der Chat-Personas, Multi-Stream RAG und des Graph Explorers. Inkludiert WP-24c Chunk-Aware Multigraph-System und automatische Spiegelkanten." --- # Chat & Graph Usage Guide @@ -17,11 +17,13 @@ context: "Anleitung zur Nutzung der Web-Oberfläche, der Chat-Personas, Multi-St Mindnet ist ein **assoziatives Gedächtnis** mit Persönlichkeit. Es unterscheidet sich von einer reinen Suche dadurch, dass es **kontextsensitiv** agiert. -**Das Gedächtnis (Der Graph):** +**Das Gedächtnis (Der Graph - Chunk-Aware Multigraph):** Wenn du nach "Projekt Alpha" suchst, findet Mindnet auch: * **Abhängigkeiten:** "Technologie X wird benötigt". * **Entscheidungen:** "Warum nutzen wir X?". * **Ähnliches:** "Projekt Beta war ähnlich". +* **Beide Richtungen:** Dank automatischer Spiegelkanten findest du auch Notizen, die auf "Projekt Alpha" verweisen (z.B. "Projekt Beta enforced_by: Projekt Alpha"). +* **Präzise Abschnitte:** Deep-Links zu spezifischen Abschnitten (`[[Note#Section]]`) ermöglichen präzise Verknüpfungen innerhalb langer Dokumente. **Der Zwilling (Die Personas):** Mindnet passt seinen Charakter an: Mal ist es der neutrale Bibliothekar, mal der strategische Berater, mal der empathische Spiegel. diff --git a/docs/01_User_Manual/01_knowledge_design.md b/docs/01_User_Manual/01_knowledge_design.md index 885664e..9abc8b2 100644 --- a/docs/01_User_Manual/01_knowledge_design.md +++ b/docs/01_User_Manual/01_knowledge_design.md @@ -1,10 +1,10 @@ --- doc_type: user_manual audience: user, author -scope: vault, markdown, schema +scope: vault, markdown, schema, agentic_validation, note_scope status: active -version: 2.9.1 -context: "Regelwerk für das Erstellen von Notizen im Vault. Die 'Source of Truth' für Autoren." +version: 4.5.8 +context: "Regelwerk für das Erstellen von Notizen im Vault. Die 'Source of Truth' für Autoren. Inkludiert WP-24c Phase 3 Agentic Edge Validation, automatische Spiegelkanten und Note-Scope Zonen." --- # Knowledge Design Manual @@ -238,8 +238,14 @@ Callout-Blocks mit mehreren Zeilen werden korrekt verarbeitet. Das System erkenn **Format-agnostische De-Duplizierung:** Wenn Kanten bereits via `[!edge]` Callout vorhanden sind, werden sie nicht mehrfach injiziert. Das System erkennt vorhandene Kanten unabhängig vom Format (Inline, Callout, Wikilink). -### 4.3 Implizite Bidirektionalität (Edger-Logik) [NEU] [PRÜFEN!] -In Mindnet musst du Kanten **nicht** manuell in beide Richtungen pflegen. Der **Edger** übernimmt die Paarbildung automatisch im Hintergrund. +### 4.3 Automatische Spiegelkanten (Invers-Logik) - WP-24c v4.5.8 + +In Mindnet musst du Kanten **nicht** manuell in beide Richtungen pflegen. Das System erzeugt automatisch **Spiegelkanten** (Invers-Kanten) im Hintergrund. + +**Wie es funktioniert:** +1. **Du setzt eine explizite Kante:** Z.B. `[[rel:depends_on Projekt Alpha]]` in Note A +2. **System erzeugt automatisch die Spiegelkante:** Note "Projekt Alpha" erhält automatisch `enforced_by: Note A` +3. **Vorteil:** Beide Richtungen sind durchsuchbar, ohne dass du beide manuell setzen musst **Deine Aufgabe:** Setze die Kante in der Datei, die du gerade bearbeitest, so wie es der **logische Fluss** vorgibt. @@ -247,10 +253,112 @@ In Mindnet musst du Kanten **nicht** manuell in beide Richtungen pflegen. Der ** * **Blick nach vorn (Vorwärtslink):** Wenn du einen Plan oder ein Protokoll schreibst, nutze `resulted_in`, `supports` oder `next`. **System-Logik (Beispiele):** -- Schreibst du in Note A: `next: [[B]]`, weiß das System automatisch: `B prev A`. -- Schreibst du in Note B: `derived_from: [[A]]`, weiß das System automatisch: `A resulted_in B`. +- Schreibst du in Note A: `[[rel:next Projekt B]]`, erzeugt das System automatisch: `Projekt B prev: Note A` +- Schreibst du in Note B: `[[rel:derived_from Note A]]`, erzeugt das System automatisch: `Note A resulted_in: Note B` +- Schreibst du in Note A: `[[rel:impacts Projekt B]]`, erzeugt das System automatisch: `Projekt B impacted_by: Note A` -**Vorteil:** Keine redundante Datenpflege, kein "Link-Nightmare", volle Konsistenz im Graphen. +**Wichtig:** +- **Explizite Kanten haben Vorrang:** Wenn du bereits beide Richtungen explizit gesetzt hast, wird keine automatische Spiegelkante erzeugt (keine Duplikate) +- **Höhere Wirksamkeit expliziter Kanten:** Explizit gesetzte Kanten haben höhere Priorität und Confidence-Werte als automatisch generierte Spiegelkanten +- **Schutz vor Manipulation:** System-Kanten (`belongs_to`, `next`, `prev`) können nicht manuell überschrieben werden (Provenance Firewall) + +**Vorteil:** Keine redundante Datenpflege, kein "Link-Nightmare", volle Konsistenz im Graphen. Beide Richtungen sind durchsuchbar, was die Auffindbarkeit von Informationen verdoppelt. + +### 4.4 Explizite vs. Validierte Kanten (Phase 3 Validierung) - WP-24c v4.5.8 + +Mindnet unterscheidet zwischen **expliziten Kanten** (sofort übernommen) und **validierten Kanten** (Phase 3 LLM-Prüfung). + +#### Explizite Kanten (Höchste Priorität) + +Diese Kanten werden **sofort** in den Graph übernommen, ohne LLM-Validierung: + +1. **Typed Relations im Text:** + ```markdown + Diese Entscheidung [[rel:depends_on Performance-Analyse]] wurde getroffen. + ``` + +2. **Callout-Edges:** + ```markdown + > [!edge] depends_on + > [[Performance-Analyse]] + > [[Projekt Alpha]] + ``` + +3. **Note-Scope Zonen:** + ```markdown + ## Smart Edges + [[rel:depends_on|System-Architektur]] + [[rel:part_of|Gesamt-System]] + ``` + *(Siehe auch: [Note-Scope Zonen](NOTE_SCOPE_ZONEN.md))* + +**Vorteil expliziter Kanten:** +- ✅ **Sofortige Übernahme:** Keine Wartezeit auf LLM-Validierung +- ✅ **Höchste Priorität:** Werden immer beibehalten, auch bei Duplikaten +- ✅ **Höhere Confidence:** Explizite Kanten haben `confidence: 1.0` (maximal) +- ✅ **Keine Validierungs-Kosten:** Keine LLM-Aufrufe erforderlich + +#### Validierte Kanten (Phase 3 - candidate: Präfix) + +Kanten, die in speziellen Validierungs-Zonen stehen, erhalten das `candidate:` Präfix und werden in **Phase 3** durch ein LLM semantisch geprüft: + +**Format:** +```markdown +### Unzugeordnete Kanten + +related_to:Mögliche Verbindung +depends_on:Unsicherer Link +uses:Experimentelle Technologie +``` + +**Validierungsprozess:** +1. **Extraktion:** Links aus `### Unzugeordnete Kanten` erhalten `candidate:` Präfix +2. **Phase 3 Validierung:** LLM prüft semantisch: "Passt diese Verbindung zum Kontext?" +3. **Erfolg (VERIFIED):** `candidate:` Präfix wird entfernt, Kante wird persistiert +4. **Ablehnung (REJECTED):** Kante wird **nicht** in die Datenbank geschrieben + +**Kontext-Optimierung:** +- **Note-Scope Kanten:** LLM nutzt Note-Summary oder gesamten Note-Text (besser für globale Verbindungen) +- **Chunk-Scope Kanten:** LLM nutzt spezifischen Chunk-Text (besser für lokale Referenzen) + +**Wann nutze ich validierte Kanten?** +- ✅ **Explorative Verbindungen:** Du bist unsicher, ob die Verbindung wirklich passt +- ✅ **Experimentelle Links:** Du willst testen, ob eine Verbindung semantisch Sinn macht +- ✅ **Automatische Vorschläge:** Das System hat Links vorgeschlagen, die du prüfen lassen willst + +**Wann nutze ich explizite Kanten?** +- ✅ **Sichere Verbindungen:** Du bist dir sicher, dass die Verbindung korrekt ist +- ✅ **Schnelle Übernahme:** Du willst keine Wartezeit auf Validierung +- ✅ **Höchste Priorität:** Die Verbindung soll definitiv im Graph sein + +*(Siehe auch: [LLM-Validierung von Links](LLM_VALIDIERUNG_VON_LINKS.md))* + +### 4.5 Note-Scope Zonen (Globale Verbindungen) - WP-24c v4.2.0 + +Für Verbindungen, die der **gesamten Note** zugeordnet werden sollen (nicht nur einem spezifischen Chunk), nutze **Note-Scope Zonen**: + +```markdown +## Smart Edges + +[[rel:depends_on|Projekt-Übersicht]] +[[rel:part_of|Größeres System]] +``` + +**Vorteile:** +- ✅ **Globale Verbindungen:** Links gelten für die gesamte Note, nicht nur einen Abschnitt +- ✅ **Höchste Priorität:** Note-Scope Links haben Vorrang bei Duplikaten +- ✅ **Bessere Validierung:** In Phase 3 nutzt das LLM den gesamten Note-Kontext (Note-Summary/Text) + +**Wann nutze ich Note-Scope?** +- ✅ **Projekt-Abhängigkeiten:** "Dieses Projekt hängt von X ab" (gilt für die ganze Note) +- ✅ **System-Zugehörigkeit:** "Dieses Konzept ist Teil von Y" (gilt für die ganze Note) +- ✅ **Globale Prinzipien:** "Diese Entscheidung basiert auf Prinzip Z" (gilt für die ganze Note) + +**Wann nutze ich Chunk-Scope (Standard)?** +- ✅ **Lokale Referenzen:** "In diesem Abschnitt nutzen wir Technologie X" (nur für diesen Abschnitt) +- ✅ **Spezifische Kontexte:** Links, die nur in einem bestimmten Textabschnitt relevant sind + +*(Siehe auch: [Note-Scope Zonen - Detaillierte Anleitung](NOTE_SCOPE_ZONEN.md))* --- 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..5613ea9 --- /dev/null +++ b/docs/01_User_Manual/LLM_VALIDIERUNG_VON_LINKS.md @@ -0,0 +1,282 @@ +# LLM-Validierung von Links in Notizen (Phase 3 Agentic Edge Validation) + +**Version:** v4.5.8 +**Status:** Aktiv +**Aktualisiert:** WP-24c Phase 3 Agentic Edge Validation mit Kontext-Optimierung + +## Ü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. + +## Validierte Links (Phase 3 - candidate: Präfix) - WP-24c v4.5.8 + +Links, die vom LLM validiert werden sollen, müssen in einer speziellen Sektion am Ende der Notiz definiert werden. Diese Links erhalten das `candidate:` Präfix und durchlaufen **Phase 3 Agentic Edge Validation**. + +### 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 +``` + +### Phase 3 Validierungsprozess (WP-24c v4.5.8) + +1. **Extraktion:** Links aus der "Unzugeordnete Kanten" Sektion werden extrahiert +2. **candidate: Präfix:** Erhalten `candidate:` Präfix in `rule_id` oder `provenance` +3. **Kontext-Optimierung:** + - **Note-Scope (`scope: note`):** LLM nutzt `note_summary` (Top 5 Chunks) oder `note_text` (aggregierter Gesamttext) + - **Chunk-Scope (`scope: chunk`):** LLM nutzt spezifischen Chunk-Text, falls verfügbar, sonst Note-Text +4. **Validierung:** LLM prüft semantisch (via `ingest_validator` Profil, Temperature 0.0): + - Ist der Link semantisch relevant für den Kontext? + - Passt die Relation (`kind`) zum Ziel? +5. **Ergebnis:** + - ✅ **VERIFIED:** `candidate:` Präfix wird entfernt, Kante wird in den Graph übernommen + - 🚫 **REJECTED:** Kante wird **nicht** in die Datenbank geschrieben (verhindert "Geister-Verknüpfungen") + +### 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 (Phase 3) + +Während der Ingestion sehen Sie im Log: + +``` +🚀 [PHASE 3] Validierung: Note-A -> Ziel-Notiz (related_to) | Scope: chunk | Kontext: Chunk-Scope (c00) +⚖️ [VALIDATING] Relation 'related_to' -> 'Ziel-Notiz' (Profile: ingest_validator)... +✅ [PHASE 3] VERIFIED: Note-A -> Ziel-Notiz (related_to) | rule_id: explicit +``` + +oder + +``` +🚀 [PHASE 3] Validierung: Note-A -> Ziel-Notiz (related_to) | Scope: note | Kontext: Note-Scope (aggregiert) +⚖️ [VALIDATING] Relation 'related_to' -> 'Ziel-Notiz' (Profile: ingest_validator)... +🚫 [PHASE 3] REJECTED: Note-A -> Ziel-Notiz (related_to) +``` + +**Hinweis:** Phase 3 Logs zeigen auch die Kontext-Optimierung (Note-Scope vs. Chunk-Scope) und den finalen Status (VERIFIED/REJECTED). + +## Technische Details + +### Provenance-System (WP-24c v4.5.8) + +- `explicit`: Explizite Links (keine Validierung, höchste Priorität) +- `explicit:note_zone`: Note-Scope Links aus `## Smart Edges` (keine Validierung) +- `candidate:`: Links aus `### Unzugeordnete Kanten` (Phase 3 Validierung erforderlich) +- `semantic_ai`: KI-generierte Links +- `rule`: Regel-basierte Links (z.B. aus types.yaml) +- `structure`: System-generierte Spiegelkanten (automatische Invers-Logik) + +### 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: Das System unterscheidet zwischen: +- **Transienten Fehlern (Netzwerk, Timeout):** Kante wird erlaubt (Integrität vor Präzision - verhindert Datenverlust) +- **Permanenten Fehlern (Config, Validation):** Kante wird abgelehnt (Graph-Qualität schützen) + +**Q: Was ist der Unterschied zwischen expliziten und validierten Links?** +A: +- **Explizite Links:** Sofortige Übernahme, höchste Priorität, keine Validierung, `confidence: 1.0` +- **Validierte Links:** Phase 3 Prüfung, `candidate:` Präfix, können abgelehnt werden, höhere Graph-Qualität + +**Q: Warum sollte ich explizite Links nutzen statt validierte?** +A: Explizite Links haben: +- ✅ Sofortige Übernahme (keine Wartezeit) +- ✅ Höchste Priorität (werden immer beibehalten) +- ✅ Keine Validierungs-Kosten (keine LLM-Aufrufe) +- ✅ Höhere Confidence-Werte + +Nutze validierte Links nur, wenn du unsicher bist, ob die Verbindung wirklich passt. + +**Q: Kann ich mehrere Links in einer Zeile angeben?** +A: Nein, jeder Link muss in einer eigenen Zeile stehen: `kind:target`. + +## Zusammenfassung (WP-24c v4.5.8) + +- ✅ **Explizite Links:** `[[rel:kind|target]]`, `> [!edge]` oder `## Smart Edges` → Keine Validierung, höchste Priorität +- ✅ **Validierte Links:** Sektion `### Unzugeordnete Kanten` → Phase 3 Validierung mit `candidate:` Präfix +- ✅ **Phase 3 Validierung:** LLM prüft semantisch mit Kontext-Optimierung (Note-Scope vs. Chunk-Scope) +- ✅ **Ergebnis:** VERIFIED (Präfix entfernt, persistiert) oder REJECTED (nicht in DB geschrieben) +- ✅ **Format:** `kind:target` (eine pro Zeile in `### Unzugeordnete Kanten`) +- ✅ **Automatische Spiegelkanten:** Explizite Kanten erzeugen automatisch Invers-Kanten (beide Richtungen durchsuchbar) 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..2b4f944 --- /dev/null +++ b/docs/01_User_Manual/NOTE_SCOPE_ZONEN.md @@ -0,0 +1,275 @@ +# Note-Scope Extraktions-Zonen (v4.5.8) + +**Version:** v4.5.8 +**Status:** Aktiv +**Aktualisiert:** WP-24c Phase 3 Agentic Edge Validation + +## Ü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 Phase 3 Validierung (WP-24c v4.5.8) + +Note-Scope Links können **zwei verschiedene Provenance** haben: + +### Explizite Note-Scope Links (Keine Validierung) + +Links in `## Smart Edges` Zonen werden als `explicit:note_zone` markiert und **direkt übernommen** (keine Phase 3 Validierung): + +```markdown +## Smart Edges + +[[rel:depends_on|System-Architektur]] +[[rel:part_of|Gesamt-System]] +``` + +**Vorteil:** Sofortige Übernahme, höchste Priorität, keine Validierungs-Kosten. + +### Validierte Note-Scope Links (Phase 3 Validierung) + +Links in `### Unzugeordnete Kanten` erhalten `candidate:` Präfix und werden in **Phase 3** validiert: + +```markdown +### Unzugeordnete Kanten + +related_to:Mögliche Verbindung +depends_on:Unsicherer Link +``` + +**Validierungsprozess:** +1. Links erhalten `candidate:` Präfix +2. **Phase 3 Validierung:** LLM prüft semantisch gegen Note-Summary oder Note-Text (Note-Scope Kontext-Optimierung) +3. **Erfolg (VERIFIED):** `candidate:` Präfix wird entfernt, Kante wird persistiert +4. **Ablehnung (REJECTED):** Kante wird **nicht** in die Datenbank geschrieben + +**Wichtig:** +- Links in `### Unzugeordnete Kanten` werden als `candidate:` markiert und durchlaufen Phase 3 +- Links in `## Smart Edges` werden als `explicit:note_zone` markiert und **nicht** validiert (direkt übernommen) +- **Note-Scope Kontext-Optimierung:** Bei Note-Scope Kanten nutzt Phase 3 `note_summary` (Top 5 Chunks) oder `note_text` (aggregierter Gesamttext) für bessere Validierungs-Genauigkeit + +## 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: Das hängt von der Zone ab: +- **`## Smart Edges`:** Nein, werden direkt übernommen (explizite Links, keine Validierung) +- **`### Unzugeordnete Kanten`:** Ja, durchlaufen Phase 3 Validierung (candidate: Präfix) + +**Q: Was ist der Unterschied zwischen Note-Scope in Smart Edges vs. Unzugeordnete Kanten?** +A: +- **Smart Edges:** Explizite Links, sofortige Übernahme, höchste Priorität +- **Unzugeordnete Kanten:** Validierte Links, Phase 3 Prüfung, candidate: Präfix + +**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/02_concepts/02_concept_graph_logic.md b/docs/02_concepts/02_concept_graph_logic.md index 429b95d..dda784e 100644 --- a/docs/02_concepts/02_concept_graph_logic.md +++ b/docs/02_concepts/02_concept_graph_logic.md @@ -1,10 +1,10 @@ --- doc_type: concept audience: architect, product_owner -scope: graph, logic, provenance +scope: graph, logic, provenance, agentic_validation, note_scope status: active -version: 2.9.1 -context: "Fachliche Beschreibung des Wissensgraphen: Knoten, Kanten, Provenance, Matrix-Logik, WP-15c Multigraph-Support und WP-22 Scoring-Prinzipien." +version: 4.5.8 +context: "Fachliche Beschreibung des Wissensgraphen: Knoten, Kanten, Provenance, Matrix-Logik, WP-15c Multigraph-Support, WP-22 Scoring-Prinzipien, WP-24c Phase 3 Agentic Edge Validation und automatische Spiegelkanten." --- # Konzept: Die Graph-Logik @@ -156,9 +156,127 @@ Die Deduplizierung basiert auf dem `src->tgt:kind@sec` Key, um sicherzustellen, --- -## 7. Idempotenz & Konsistenz +## 7. Automatische Spiegelkanten (Invers-Logik) - WP-24c v4.5.8 + +Das System erzeugt automatisch **Spiegelkanten** (Invers-Kanten) für explizite Verbindungen, um die Auffindbarkeit von Informationen zu verdoppeln. + +### 7.1 Funktionsweise + +**Beispiel:** +- **Explizite Kante:** Note A `depends_on: Note B` +- **Automatische Spiegelkante:** Note B `enforced_by: Note A` + +**Vorteil:** Beide Richtungen sind durchsuchbar. Wenn du nach "Note B" suchst, findest du auch alle Notizen, die von "Note B" abhängen (via `enforced_by`). + +### 7.2 Invers-Mapping + +Die Edge Registry definiert für jeden Kanten-Typ das symmetrische Gegenstück: +- `depends_on` ↔ `enforced_by` +- `derived_from` ↔ `resulted_in` +- `impacts` ↔ `impacted_by` +- `blocks` ↔ `blocked_by` +- `next` ↔ `prev` +- `related_to` ↔ `related_to` (symmetrisch) + +### 7.3 Priorität & Schutz + +* **Explizite Kanten haben Vorrang:** Wenn du bereits beide Richtungen explizit gesetzt hast, wird keine automatische Spiegelkante erzeugt (keine Duplikate) +* **Höhere Wirksamkeit expliziter Kanten:** Explizit gesetzte Kanten haben höhere Confidence-Werte (`confidence: 1.0`) als automatisch generierte Spiegelkanten (`confidence: 0.9 * original`) +* **Provenance Firewall:** System-Kanten (`belongs_to`, `next`, `prev`) können nicht manuell überschrieben werden + +### 7.4 Phase 2 Symmetrie-Injektion + +Spiegelkanten werden am Ende des gesamten Imports (Phase 2) in einem Batch-Prozess injiziert: +- **Authority-Check:** Nur wenn keine explizite Kante existiert, wird die Spiegelkante erzeugt +- **ID-Konsistenz:** Verwendet exakt dieselbe ID-Generierung wie Phase 1 (inkl. `target_section`) +- **Logging:** `🔄 [SYMMETRY]` zeigt die erzeugten Spiegelkanten + +--- + +## 8. Phase 3 Agentic Edge Validation - WP-24c v4.5.8 + +Das System implementiert ein finales Validierungs-Gate für alle Kanten mit `candidate:` Präfix, um "Geister-Verknüpfungen" zu verhindern und die Graph-Qualität zu sichern. + +### 8.1 Trigger-Kriterium + +Kanten erhalten `candidate:` Präfix, wenn sie: +- In `### Unzugeordnete Kanten` Sektionen stehen +- Von der Smart Edge Allocation als Kandidaten vorgeschlagen wurden +- Explizit als `candidate:` markiert wurden + +### 8.2 Validierungsprozess + +1. **Kontext-Optimierung:** + - **Note-Scope (`scope: note`):** LLM nutzt `note_summary` (Top 5 Chunks) oder `note_text` (aggregierter Gesamttext) + - **Chunk-Scope (`scope: chunk`):** LLM nutzt spezifischen Chunk-Text, falls verfügbar, sonst Note-Text + +2. **LLM-Validierung:** + - Nutzt `ingest_validator` Profil (Temperature 0.0 für Determinismus) + - Prüft semantisch: "Passt diese Verbindung zum Kontext?" + - Binäre Entscheidung: YES (VERIFIED) oder NO (REJECTED) + +3. **Ergebnis:** + - **VERIFIED:** `candidate:` Präfix wird entfernt, Kante wird persistiert + - **REJECTED:** Kante wird **nicht** in die Datenbank geschrieben (verhindert persistente "Geister-Verknüpfungen") + +### 8.3 Fehlertoleranz + +Das System unterscheidet zwischen: +- **Transienten Fehlern (Netzwerk, Timeout):** Kante wird erlaubt (Integrität vor Präzision) +- **Permanenten Fehlern (Config, Validation):** Kante wird abgelehnt (Graph-Qualität schützen) + +### 8.4 Provenance nach Validierung + +- **Vor Validierung:** `provenance: "candidate:global_pool"` oder `rule_id: "candidate:..."` +- **Nach VERIFIED:** `provenance: "global_pool"` oder `rule_id: "explicit"` (Präfix entfernt) +- **Nach REJECTED:** Kante existiert nicht im Graph (wird nicht persistiert) + +--- + +## 9. Note-Scope vs. Chunk-Scope - WP-24c v4.2.0 + +Das System unterscheidet zwischen **Note-Scope** (globale Verbindungen) und **Chunk-Scope** (lokale Referenzen). + +### 9.1 Chunk-Scope (Standard) + +- **Quelle:** `source_id = chunk_id` (z.B. `note-id#c00`) +- **Kontext:** Spezifischer Textabschnitt (Chunk) +- **Verwendung:** Lokale Referenzen innerhalb eines Abschnitts +- **Phase 3 Validierung:** Nutzt spezifischen Chunk-Text + +**Beispiel:** +```markdown +In diesem Abschnitt nutzen wir [[rel:uses|Technologie X]]. +``` + +### 9.2 Note-Scope + +- **Quelle:** `source_id = note_id` (nicht `chunk_id`) +- **Kontext:** Gesamte Note (Note-Summary oder Note-Text) +- **Verwendung:** Globale Verbindungen, die für die ganze Note gelten +- **Phase 3 Validierung:** Nutzt `note_summary` (Top 5 Chunks) oder `note_text` (aggregierter Gesamttext) + +**Beispiel:** +```markdown +## Smart Edges + +[[rel:depends_on|Projekt-Übersicht]] +[[rel:part_of|Größeres System]] +``` + +### 9.3 Priorität + +Bei Duplikaten (gleiche Kante in Chunk-Scope und Note-Scope): +1. **Note-Scope Links** haben **höchste Priorität** +2. Dann Confidence-Wert +3. Dann Provenance-Priority + +--- + +## 10. Idempotenz & Konsistenz Das System garantiert fachliche Konsistenz auch bei mehrfachen Importen. * **Stabile IDs:** Deterministische IDs verhindern Duplikate bei Re-Imports. * **Deduplizierung:** Kanten werden anhand ihrer Identität (inkl. Section) erkannt. Die "stärkere" Provenance gewinnt. -* **Format-agnostische Erkennung:** Kanten werden unabhängig vom Format (Inline, Callout, Wikilink) erkannt, um Dopplungen zu vermeiden. \ No newline at end of file +* **Format-agnostische Erkennung:** Kanten werden unabhängig vom Format (Inline, Callout, Wikilink) erkannt, um Dopplungen zu vermeiden. +* **Phase 3 Validierung:** Verhindert persistente "Geister-Verknüpfungen" durch Ablehnung irrelevanter Kanten. \ No newline at end of file diff --git a/docs/03_Technical_References/03_tech_configuration.md b/docs/03_Technical_References/03_tech_configuration.md index 1f0b2d7..e8998da 100644 --- a/docs/03_Technical_References/03_tech_configuration.md +++ b/docs/03_Technical_References/03_tech_configuration.md @@ -1,10 +1,10 @@ --- doc_type: technical_reference audience: developer, admin -scope: configuration, env, registry, scoring, resilience, modularization, agentic_rag, moe, lazy_prompts +scope: configuration, env, registry, scoring, resilience, modularization, agentic_rag, moe, lazy_prompts, agentic_validation status: active -version: 3.1.1 -context: "Umfassende Referenztabellen für Umgebungsvariablen (inkl. Hybrid-Cloud & WP-76), YAML-Konfigurationen, Edge Registry Struktur, WP-25 Multi-Stream RAG, WP-25a Mixture of Experts (MoE) und WP-25b Lazy-Prompt-Orchestration unter Berücksichtigung von WP-14." +version: 4.5.8 +context: "Umfassende Referenztabellen für Umgebungsvariablen (inkl. Hybrid-Cloud & WP-76), YAML-Konfigurationen, Edge Registry Struktur, WP-25 Multi-Stream RAG, WP-25a Mixture of Experts (MoE), WP-25b Lazy-Prompt-Orchestration und WP-24c Phase 3 Agentic Edge Validation (v4.5.8) unter Berücksichtigung von WP-14." --- # Konfigurations-Referenz @@ -50,6 +50,11 @@ Diese Variablen steuern die Infrastruktur, Pfade und globale Timeouts. Seit der | `MINDNET_LL_BACKGROUND_LIMIT`| `2` | **Traffic Control:** Max. parallele Hintergrund-Tasks (Semaphore). | | `MINDNET_CHANGE_DETECTION_MODE` | `full` | `full` (Text + Meta) oder `body` (nur Text). | | `MINDNET_DEFAULT_RETRIEVER_WEIGHT` | `1.0` | **Neu (WP-22):** Systemweiter Standard für das Retriever-Gewicht einer Notiz. | +| `MINDNET_LLM_VALIDATION_HEADERS` | `Unzugeordnete Kanten,Edge Pool,Candidates` | **Neu (v4.2.0, WP-24c):** Komma-separierte Header-Namen für LLM-Validierung. Kanten in diesen Zonen erhalten `candidate:` Präfix und werden in Phase 3 validiert. | +| `MINDNET_LLM_VALIDATION_HEADER_LEVEL` | `3` | **Neu (v4.2.0, WP-24c):** Header-Ebene für LLM-Validierung (1-6, Default: 3 für ###). Bestimmt, welche Überschriften als Validierungs-Zonen erkannt werden. | +| `MINDNET_NOTE_SCOPE_ZONE_HEADERS` | `Smart Edges,Relationen,Global Links,Note-Level Relations,Globale Verbindungen` | **Neu (v4.2.0, WP-24c):** Komma-separierte Header-Namen für Note-Scope Zonen. Links in diesen Zonen werden als `scope: note` behandelt und nutzen Note-Summary/Text in Phase 3 Validierung. | +| `MINDNET_NOTE_SCOPE_HEADER_LEVEL` | `2` | **Neu (v4.2.0, WP-24c):** Header-Ebene für Note-Scope Zonen (1-6, Default: 2 für ##). Bestimmt, welche Überschriften als Note-Scope Zonen erkannt werden. | +| `MINDNET_IGNORE_FOLDERS` | *(leer)* | **Neu (v4.1.0):** Komma-separierte Liste von Ordnernamen, die beim Import ignoriert werden. Beispiel: `.trash,.obsidian,.git,.sync` | --- diff --git a/docs/03_Technical_References/03_tech_data_model.md b/docs/03_Technical_References/03_tech_data_model.md index 9dc235a..d74832e 100644 --- a/docs/03_Technical_References/03_tech_data_model.md +++ b/docs/03_Technical_References/03_tech_data_model.md @@ -1,10 +1,10 @@ --- doc_type: technical_reference audience: developer, architect -scope: database, qdrant, schema +scope: database, qdrant, schema, agentic_validation status: active -version: 2.9.1 -context: "Exakte Definition der Datenmodelle (Payloads) in Qdrant und Index-Anforderungen. Berücksichtigt WP-14 Modularisierung und WP-15b Multi-Hashes." +version: 4.5.8 +context: "Exakte Definition der Datenmodelle (Payloads) in Qdrant und Index-Anforderungen. Berücksichtigt WP-14 Modularisierung, WP-15b Multi-Hashes und WP-24c Phase 3 Agentic Edge Validation (candidate: Präfix, verified Status)." --- # Technisches Datenmodell (Qdrant Schema) @@ -113,10 +113,12 @@ Gerichtete Kanten zwischen Knoten. Stark erweitert in v2.6 für Provenienz-Track "scope": "string (keyword)", // Immer 'chunk' (Legacy-Support: 'note') "note_id": "string (keyword)", // Owner Note ID (Ursprung der Kante) - // Provenance & Quality (WP03/WP15) - "provenance": "keyword", // 'explicit', 'rule', 'smart', 'structure' - "rule_id": "string (keyword)", // Traceability: 'inline:rel', 'explicit:wikilink', 'smart:llm' - "confidence": "float" // Vertrauenswürdigkeit (0.0 - 1.0) + // Provenance & Quality (WP03/WP15/WP-24c) + "provenance": "keyword", // 'explicit', 'explicit:note_zone', 'explicit:callout', 'rule', 'semantic_ai', 'structure', 'candidate:...' (vor Phase 3) + "rule_id": "string (keyword)", // Traceability: 'inline:rel', 'explicit:wikilink', 'candidate:...' (vor Phase 3), 'explicit' (nach Phase 3 VERIFIED) + "confidence": "float", // Vertrauenswürdigkeit (0.0 - 1.0) + "scope": "string (keyword)", // 'chunk' (Standard) oder 'note' (Note-Scope Zonen) - WP-24c v4.2.0 + "virtual": "boolean (optional)" // true für automatisch generierte Spiegelkanten (Invers-Logik) - WP-24c v4.5.8 } ``` @@ -127,6 +129,23 @@ Gerichtete Kanten zwischen Knoten. Stark erweitert in v2.6 für Provenienz-Track * Semantische Deduplizierung basiert auf `src->tgt:kind@sec` Key, um "Phantom-Knoten" zu vermeiden. * **Metadaten-Persistenz:** `target_section`, `provenance` und `confidence` werden durchgängig im In-Memory Subgraph und Datenbank-Adapter erhalten. +**Phase 3 Validierung (WP-24c v4.5.8):** +* **candidate: Präfix:** Kanten mit `candidate:` in `rule_id` oder `provenance` durchlaufen Phase 3 Validierung +* **Vor Validierung:** `provenance: "candidate:global_pool"` oder `rule_id: "candidate:..."` +* **Nach VERIFIED:** `candidate:` Präfix wird entfernt, Kante wird persistiert +* **Nach REJECTED:** Kante wird **nicht** in die Datenbank geschrieben (verhindert "Geister-Verknüpfungen") +* **Wichtig:** Nur Kanten ohne `candidate:` Präfix werden im Graph persistiert + +**Note-Scope vs. Chunk-Scope (WP-24c v4.2.0):** +* **Chunk-Scope (`scope: "chunk"`):** Standard, `source_id = chunk_id` (z.B. `note-id#c00`) +* **Note-Scope (`scope: "note"`):** Aus Note-Scope Zonen, `source_id = note_id` (nicht `chunk_id`) +* **Phase 3 Kontext-Optimierung:** Note-Scope nutzt `note_summary`/`note_text`, Chunk-Scope nutzt spezifischen Chunk-Text + +**Automatische Spiegelkanten (WP-24c v4.5.8):** +* **virtual: true:** Markiert automatisch generierte Invers-Kanten (Spiegelkanten) +* **Provenance:** `structure` (System-generiert, geschützt durch Provenance Firewall) +* **Confidence:** Leicht gedämpft (`original * 0.9`) im Vergleich zu expliziten Kanten + **Erforderliche Indizes:** Es müssen Payload-Indizes für folgende Felder existieren: * `source_id` diff --git a/docs/03_Technical_References/03_tech_ingestion_pipeline.md b/docs/03_Technical_References/03_tech_ingestion_pipeline.md index 3371b2e..f7e741a 100644 --- a/docs/03_Technical_References/03_tech_ingestion_pipeline.md +++ b/docs/03_Technical_References/03_tech_ingestion_pipeline.md @@ -1,10 +1,10 @@ --- doc_type: technical_reference audience: developer, devops -scope: backend, ingestion, smart_edges, edge_registry, modularization, moe, lazy_prompts +scope: backend, ingestion, smart_edges, edge_registry, modularization, moe, lazy_prompts, agentic_validation status: active -version: 2.14.0 -context: "Detaillierte technische Beschreibung der Import-Pipeline, Two-Pass-Workflow (WP-15b), modularer Datenbank-Architektur (WP-14), WP-25a profilgesteuerte Validierung und WP-25b Lazy-Prompt-Orchestration. Integriert Mistral-safe Parsing und Deep Fallback." +version: 4.5.8 +context: "Detaillierte technische Beschreibung der Import-Pipeline, Two-Pass-Workflow (WP-15b), modularer Datenbank-Architektur (WP-14), WP-25a profilgesteuerte Validierung, WP-25b Lazy-Prompt-Orchestration und WP-24c Phase 3 Agentic Edge Validation (v4.5.8). Integriert Mistral-safe Parsing und Deep Fallback." --- # Ingestion Pipeline & Smart Processing @@ -15,9 +15,9 @@ Die Ingestion transformiert Markdown in den Graphen. Entrypoint: `scripts/import -## 1. Der Import-Prozess (16-Schritte-Workflow) +## 1. Der Import-Prozess (17-Schritte-Workflow - 3-Phasen-Modell) -Der Prozess ist **asynchron**, **idempotent** und wird nun in zwei logische Durchläufe (Passes) unterteilt, um die semantische Genauigkeit zu maximieren. +Der Prozess ist **asynchron**, **idempotent** und wird nun in **drei logische Phasen** unterteilt, um die semantische Genauigkeit zu maximieren und die Graph-Qualität durch agentische Validierung zu sichern. ### Phase 1: Pre-Scan & Context (Pass 1) 1. **Trigger & Async Dispatch:** @@ -50,18 +50,10 @@ Der Prozess ist **asynchron**, **idempotent** und wird nun in zwei logische Durc * Bei Änderungen löscht `purge_artifacts()` via `app.core.ingestion.ingestion_db` alle alten Chunks und Edges der Note. * Die Namensauflösung erfolgt nun über das modularisierte `database`-Paket. 10. **Chunking anwenden:** Zerlegung des Textes basierend auf dem ermittelten Profil (siehe Kap. 3). -11. **Smart Edge Allocation & Semantic Validation (WP-15b / WP-25a / WP-25b):** +11. **Smart Edge Allocation & Kandidaten-Erzeugung (WP-15b / WP-25a / WP-25b):** * Der `SemanticAnalyzer` schlägt Kanten-Kandidaten vor. - * **Validierung (WP-25a/25b):** Jeder Kandidat wird durch das LLM semantisch gegen das Ziel im **LocalBatchCache** geprüft. - * **Profilgesteuerte Validierung:** Nutzt das MoE-Profil `ingest_validator` (Temperature 0.0 für maximale Determinismus). - * **Lazy-Prompt-Loading (WP-25b):** Nutzt `prompt_key="edge_validation"` mit `variables` statt vorformatierter Strings. - * **Hierarchische Resolution:** Level 1 (Modell-ID) → Level 2 (Provider) → Level 3 (Default) - * **Differenzierte Fehlerbehandlung (WP-25b):** Unterscheidung zwischen transienten (Netzwerk) und permanenten (Config) Fehlern: - * **Transiente Fehler:** Timeout, Connection, Network → Kante wird erlaubt (Datenverlust vermeiden) - * **Permanente Fehler:** Config, Validation, Invalid Response → Kante wird abgelehnt (Graph-Qualität schützen) - * **Fallback-Kaskade:** Bei Fehlern erfolgt automatischer Fallback via `fallback_profile` (z.B. `compression_fast` → `identity_safe`). - * **Traffic Control:** Nutzung der neutralen `clean_llm_text` Funktion zur Bereinigung von Steuerzeichen (, [OUT]). - * **Deep Fallback (v2.11.14):** Erkennt "Silent Refusals". Liefert die Cloud keine verwertbaren Kanten, wird ein lokaler Fallback via Ollama erzwungen. + * **Kandidaten-Markierung:** Alle vorgeschlagenen Kanten erhalten `candidate:` Präfix in `rule_id` oder `provenance`. + * **Hinweis:** Die eigentliche LLM-Validierung erfolgt erst in **Phase 3** (siehe Schritt 17). 12. **Inline-Kanten finden:** Parsing von `[[rel:...]]` und Callouts. 13. **Alias-Auflösung & Kanonisierung (WP-22):** * Jede Kante wird via `EdgeRegistry` normalisiert (z.B. `basiert_auf` -> `based_on`). @@ -70,7 +62,28 @@ Der Prozess ist **asynchron**, **idempotent** und wird nun in zwei logische Durc 15. **Embedding (Async - WP-25a):** Generierung der Vektoren via `embedding_expert` Profil aus `llm_profiles.yaml`. * **Profil-Auflösung:** Das `EmbeddingsClient` lädt Modell und Dimensionen direkt aus dem Profil (z.B. `nomic-embed-text`, 768 Dimensionen). * **Konsolidierung:** Entfernung der Embedding-Konfiguration aus der `.env` zugunsten zentraler Profil-Registry. -16. **Database Sync (WP-14):** Batch-Upsert aller Points in die Collections `{prefix}_chunks` und `{prefix}_edges` über die zentrale Infrastruktur. + +### Phase 3: Agentic Edge Validation (WP-24c v4.5.8) + +17. **Finales Validierungs-Gate für candidate: Kanten:** + * **Trigger-Kriterium:** Alle Kanten mit `rule_id` ODER `provenance` beginnend mit `"candidate:"` werden dem LLM-Validator vorgelegt. + * **Kontext-Optimierung:** Dynamische Kontext-Auswahl basierend auf `scope`: + * **Note-Scope (`scope: note`):** Verwendet `note_summary` (Top 5 Chunks) oder `note_text` (aggregierter Gesamttext) für globale Verbindungen. + * **Chunk-Scope (`scope: chunk`):** Versucht spezifischen Chunk-Text zu finden, sonst Fallback auf Note-Text. + * **Validierung:** Nutzt `validate_edge_candidate()` mit MoE-Profil `ingest_validator` (Temperature 0.0 für Determinismus). + * **Erfolg (VERIFIED):** Entfernt `candidate:` Präfix aus `rule_id` und `provenance`. Kante wird zu `validated_edges` hinzugefügt. + * **Ablehnung (REJECTED):** Kante wird zu `rejected_edges` hinzugefügt und **nicht** weiterverarbeitet (keine DB-Persistierung). + * **Fehlertoleranz:** Unterscheidung zwischen transienten (Netzwerk) und permanenten (Config) Fehlern: + * **Transiente Fehler:** Timeout, Connection, Network → Kante wird erlaubt (Integrität vor Präzision) + * **Permanente Fehler:** Config, Validation, Invalid Response → Kante wird abgelehnt (Graph-Qualität schützen) + * **Logging:** `🚀 [PHASE 3]` für Start, `✅ [PHASE 3] VERIFIED` für Erfolg, `🚫 [PHASE 3] REJECTED` für Ablehnung. + +**Wichtig:** Nur `validated_edges` (ohne `candidate:` Präfix) werden in Phase 2 (Symmetrie) verarbeitet und in die Datenbank geschrieben. `rejected_edges` werden vollständig ignoriert. + +### Phase 2 (Fortsetzung): Symmetrie & Persistence + +18. **Database Sync (WP-14):** Batch-Upsert aller Points in die Collections `{prefix}_chunks` und `{prefix}_edges` über die zentrale Infrastruktur. + * **Nur verified Kanten:** Nur Kanten ohne `candidate:` Präfix werden persistiert. --- @@ -198,6 +211,8 @@ Kanten werden nach Vertrauenswürdigkeit (`provenance`) priorisiert. Die höhere **2. Mistral-safe Parsing:** Automatisierte Bereinigung von LLM-Antworten in `ingestion_validation.py`. Stellt sicher, dass semantische Entscheidungen ("YES"/"NO") nicht durch technische Header verfälscht werden. -**3. Profilgesteuerte Validierung (WP-25a):** Die semantische Kanten-Validierung erfolgt zwingend über das MoE-Profil `ingest_validator` (Temperature 0.0 für Determinismus). Dies gewährleistet konsistente binäre Entscheidungen (YES/NO) unabhängig von der globalen Provider-Konfiguration. +**3. Phase 3 Agentic Edge Validation (WP-24c v4.5.8):** Finales Validierungs-Gate für alle `candidate:` Kanten. Nutzt das MoE-Profil `ingest_validator` (Temperature 0.0 für Determinismus) und dynamische Kontext-Optimierung (Note-Scope vs. Chunk-Scope). Gewährleistet konsistente binäre Entscheidungen (YES/NO) und verhindert "Geister-Verknüpfungen" im Wissensgraphen. + +**4. Profilgesteuerte Validierung (WP-25a):** Die semantische Kanten-Validierung erfolgt zwingend über das MoE-Profil `ingest_validator` (Temperature 0.0 für Determinismus). Dies gewährleistet konsistente binäre Entscheidungen (YES/NO) unabhängig von der globalen Provider-Konfiguration. **3. Purge Integrity:** Validierung, dass vor jedem Upsert alle assoziierten Artefakte in den Collections `{prefix}_chunks` und `{prefix}_edges` gelöscht wurden, um Daten-Duplikate zu vermeiden. \ 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 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/docs/03_Technical_References/AUDIT_SYSTEM_INTEGRITY_V4.5.8.md b/docs/03_Technical_References/AUDIT_SYSTEM_INTEGRITY_V4.5.8.md new file mode 100644 index 0000000..495069f --- /dev/null +++ b/docs/03_Technical_References/AUDIT_SYSTEM_INTEGRITY_V4.5.8.md @@ -0,0 +1,510 @@ +# System-Integrity & Regression-Audit (v4.5.8) + +**Datum:** 2026-01-XX +**Version:** v4.5.8 +**Status:** Audit abgeschlossen +**Auditor:** AI Assistant (Auto) + +## Kontext + +Nach umfangreichen Änderungen in WP24c (insbesondere v4.5.7/8) wurde ein vollständiges System-Integrity & Regression-Audit durchgeführt, um sicherzustellen, dass keine unbeabsichtigten Beeinträchtigungen oder "Logic-Drift" eingeführt wurden. + +## Audit-Scope + +1. **WP-22 Scoring Integrität**: Prüfung der mathematischen Berechnung des `total_score` +2. **WP-25a/b MoE & Prompts**: Verifizierung der Profil-Ladung und MoE-Kaskade +3. **Deduplizierungs-Logik**: Prüfung der De-Duplizierung von Kanten +4. **Phase 3 Validierungs-Gate**: Verifizierung der neuen Validierungs-Logik +5. **Note-Scope Kontext-Optimierung**: Prüfung der Kontext-Optimierung + +--- + +## 1. WP-22 Scoring Integrität + +### Prüfpunkt: Hat die Einführung von `candidate:` oder `verified` Status Auswirkungen auf die mathematische Berechnung des `total_score`? + +**Status:** ✅ **KEIN PROBLEM** + +**Ergebnis:** +- `candidate:` und `verified` sind **KEINE Status-Werte** für die Scoring-Funktion +- Sie sind **Präfixe** in `rule_id` und `provenance` für Kanten (Edge-Metadaten) +- Die `get_status_multiplier()` Funktion in `retriever_scoring.py` behandelt ausschließlich: + - `stable`: 1.2 (Multiplikator) + - `active`: 1.0 (Standard) + - `draft`: 0.5 (Dämpfung) +- Die mathematische Formel in `compute_wp22_score()` bleibt vollständig unangetastet + +**Code-Referenz:** +- `app/core/retrieval/retriever_scoring.py` Zeile 49-63: `get_status_multiplier()` +- `app/core/retrieval/retriever_scoring.py` Zeile 65-128: `compute_wp22_score()` + +**Bewertung:** Die Scoring-Mathematik ist **vollständig isoliert** von den Edge-Metadaten (`candidate:`, `verified`). Keine Regression festgestellt. + +--- + +## 2. WP-25a/b MoE & Prompts + +### Prüfpunkt 2a: Werden die korrekten Profile aus `llm_profiles.yaml` geladen? + +**Status:** ✅ **FUNKTIONIERT KORREKT** + +**Ergebnis:** +- `LLMService._load_llm_profiles()` lädt Profile aus `llm_profiles.yaml` (nicht `prompts.yaml`) +- Pfad wird korrekt aus Settings geladen: `LLM_PROFILES_PATH` (Default: `config/llm_profiles.yaml`) +- Profile werden im `__init__` geladen und im Instanz-Attribut `self.profiles` gespeichert +- Fehlerbehandlung vorhanden: Bei fehlender Datei wird leeres Dict zurückgegeben mit Warnung + +**Code-Referenz:** +- `app/services/llm_service.py` Zeile 87-100: `_load_llm_profiles()` +- `app/services/llm_service.py` Zeile 36: Initialisierung in `__init__` + +**Bewertung:** Profil-Ladung funktioniert korrekt. Keine Regression. + +### Prüfpunkt 2b: Nutzt die neue Validierungs-Logik in Phase 3 die bestehende MoE-Kaskade? + +**Status:** ✅ **FUNKTIONIERT KORREKT** + +**Ergebnis:** +- Phase 3 Validierung nutzt `profile_name="ingest_validator"` (siehe `ingestion_processor.py` Zeile 345) +- `LLMService.generate_raw_response()` unterstützt vollständig die MoE-Kaskade: + - Profil-Auflösung aus `llm_profiles.yaml` (Zeile 151-161) + - Fallback-Kaskade via `fallback_profile` (Zeile 214-227) + - `visited_profiles` Schutz verhindert Endlosschleifen (Zeile 214) + - Rekursiver Aufruf mit `visited_profiles` Parameter (Zeile 226) +- Die Kaskade wird **nicht umgangen**, sondern vollständig genutzt + +**Code-Referenz:** +- `app/core/ingestion/ingestion_processor.py` Zeile 340-346: Phase 3 Validierung +- `app/services/llm_service.py` Zeile 150-227: MoE-Kaskade Implementierung +- `config/llm_profiles.yaml`: Profil-Definitionen mit `fallback_profile` + +**Bewertung:** MoE-Kaskade wird korrekt genutzt. Keine Regression. + +### Prüfpunkt 2c: Werden Prompts korrekt aus `prompts.yaml` geladen? + +**Status:** ✅ **FUNKTIONIERT KORREKT** + +**Ergebnis:** +- `LLMService._load_prompts()` lädt Prompts aus `prompts.yaml` (Zeile 76-85) +- `DecisionEngine` nutzt `prompt_key` und `variables` für Lazy-Loading (Zeile 108-113, 309-315) +- `LLMService.get_prompt()` unterstützt Hierarchie: Model-ID → Provider → Default (Zeile 102-123) +- Prompt-Formatierung erfolgt via `template.format(**(variables or {}))` (Zeile 179) + +**Code-Referenz:** +- `app/services/llm_service.py` Zeile 76-85: `_load_prompts()` +- `app/services/llm_service.py` Zeile 102-123: `get_prompt()` mit Hierarchie +- `app/core/retrieval/decision_engine.py` Zeile 107-113: Intent-Routing mit `prompt_key` +- `app/core/retrieval/decision_engine.py` Zeile 309-315: Finale Synthese mit `prompt_key` + +**Bewertung:** Prompt-Ladung funktioniert korrekt. Keine Regression. + +--- + +## 3. Deduplizierungs-Logik + +### Prüfpunkt: Gefährden die Änderungen an `all_chunk_callout_keys` in v4.5.7/8 die gewollte De-Duplizierung von Kanten (WP-24c)? + +**Status:** ✅ **FUNKTIONIERT KORREKT** + +**Ergebnis:** +- `all_chunk_callout_keys` wird **VOR jeder Verwendung** initialisiert (Zeile 531-533) +- Initialisierung erfolgt **VOR** Phase 1 (Sammeln aus `candidate_pool`) und **VOR** Phase 2 (Chunk-Verarbeitung) +- Die De-Duplizierungs-Logik ist **vollständig intakt**: + - Phase 1: Sammeln aller `explicit:callout` Keys aus `candidate_pool` (Zeile 657-697) + - Phase 2: Prüfung gegen `all_chunk_callout_keys` vor Erstellung neuer Callout-Kanten (Zeile 768) + - Globaler Scan: Nutzung von `all_chunk_callout_keys` als Ausschlusskriterium (Zeile 855) +- LLM-Validierungs-Zonen: Callouts werden korrekt zu `all_chunk_callout_keys` hinzugefügt (Zeile 615) + +**Code-Referenz:** +- `app/core/graph/graph_derive_edges.py` Zeile 531-533: Initialisierung +- `app/core/graph/graph_derive_edges.py` Zeile 657-697: Phase 1 (Sammeln) +- `app/core/graph/graph_derive_edges.py` Zeile 768: Phase 2 (Prüfung) +- `app/core/graph/graph_derive_edges.py` Zeile 855: Globaler Scan (Ausschluss) + +**Bewertung:** De-Duplizierungs-Logik ist intakt. Keine Regression. + +--- + +## 4. Phase 3 Validierungs-Gate + +### Prüfpunkt: Ist das Phase 3 Validierungs-Gate korrekt implementiert und nutzt es die MoE-Kaskade? + +**Status:** ✅ **GEWOLLTE ÄNDERUNG** (v4.5.8) + +**Ergebnis:** +- Phase 3 Validierung ist **korrekt implementiert** in `ingestion_processor.py` (Zeile 274-371) +- **Trigger-Kriterium:** Kanten mit `rule_id` ODER `provenance` beginnend mit `"candidate:"` (Zeile 292) +- **Validierung:** Nutzt `validate_edge_candidate()` mit `profile_name="ingest_validator"` (Zeile 340-346) +- **Erfolg:** Entfernt `candidate:` Präfix aus `rule_id` und `provenance` (Zeile 349-357) +- **Ablehnung:** Kanten werden zu `rejected_edges` hinzugefügt und **nicht** weiterverarbeitet (Zeile 362-363) +- **MoE-Kaskade:** Wird vollständig genutzt via `llm_service.generate_raw_response()` (siehe Prüfpunkt 2b) + +**Code-Referenz:** +- `app/core/ingestion/ingestion_processor.py` Zeile 274-371: Phase 3 Implementierung +- `app/core/ingestion/ingestion_validation.py` Zeile 24-91: `validate_edge_candidate()` + +**Bewertung:** Phase 3 Validierungs-Gate ist korrekt implementiert. **Gewollte Änderung**, keine Regression. + +--- + +## 5. Note-Scope Kontext-Optimierung + +### Prüfpunkt: Ist die Note-Scope Kontext-Optimierung korrekt implementiert? + +**Status:** ✅ **GEWOLLTE ÄNDERUNG** (v4.5.8) + +**Ergebnis:** +- Kontext-Optimierung ist **korrekt implementiert** in Phase 3 Validierung (Zeile 311-326) +- **Note-Scope:** Verwendet `note_summary` oder `note_text` (aggregierter Kontext) (Zeile 314-316) +- **Chunk-Scope:** Versucht spezifischen Chunk-Text zu finden, sonst Note-Text (Zeile 318-326) +- **Note-Summary:** Wird aus Top 5 Chunks erstellt (Zeile 282) +- **Note-Text:** Wird aus `markdown_body` oder aggregiert aus allen Chunks erstellt (Zeile 280) + +**Code-Referenz:** +- `app/core/ingestion/ingestion_processor.py` Zeile 278-282: Note-Summary/Text Erstellung +- `app/core/ingestion/ingestion_processor.py` Zeile 311-326: Kontext-Optimierung + +**Bewertung:** Note-Scope Kontext-Optimierung ist korrekt implementiert. **Gewollte Änderung**, keine Regression. + +--- + +## 6. Weitere Prüfungen + +### 6.1 Edge-Registry Integration + +**Status:** ✅ **FUNKTIONIERT KORREKT** + +**Ergebnis:** +- Edge-Registry wird korrekt für Typ-Auflösung genutzt (Zeile 383 in `ingestion_processor.py`) +- Symmetrie-Generierung nutzt `edge_registry.get_inverse()` (Zeile 397) +- Keine Regression festgestellt + +### 6.2 Context-Reuse Logik + +**Status:** ✅ **FUNKTIONIERT KORREKT** + +**Ergebnis:** +- Context-Reuse ist in `decision_engine.py` implementiert (Zeile 154-196) +- Bei Kompressions-Fehlern wird Original-Content zurückgegeben (Zeile 232-235) +- Bei Synthese-Fehlern wird Fallback mit vorhandenem Context genutzt (Zeile 328-365) +- Keine Regression festgestellt + +### 6.3 Prompt-Template Validierung + +**Status:** ✅ **FUNKTIONIERT KORREKT** + +**Ergebnis:** +- Prompt-Validierung in `llm_service.py` prüft auf leere Templates (Zeile 172-175) +- Fehlerbehandlung vorhanden: `ValueError` bei fehlendem oder leerem `prompt_key` +- Keine Regression festgestellt + +--- + +## Zusammenfassung + +### ✅ Keine Regressionen festgestellt + +Alle geprüften Funktionen arbeiten korrekt und entsprechen den ursprünglichen WP-Spezifikationen: + +1. **WP-22 Scoring:** Mathematik bleibt unangetastet ✅ +2. **WP-25a/b MoE & Prompts:** Profile und Prompts werden korrekt geladen, MoE-Kaskade funktioniert ✅ +3. **Deduplizierungs-Logik:** `all_chunk_callout_keys` funktioniert korrekt ✅ +4. **Phase 3 Validierung:** Korrekt implementiert, nutzt MoE-Kaskade ✅ +5. **Note-Scope Kontext-Optimierung:** Korrekt implementiert ✅ + +### 📋 Gewollte Änderungen (v4.5.8) + +Die folgenden Änderungen sind **explizit gewollt** und stellen keine Regressionen dar: + +1. **Phase 3 Validierungs-Gate:** Neue Validierungs-Logik für `candidate:` Kanten +2. **Note-Scope Kontext-Optimierung:** Optimierte Kontext-Auswahl für Note-Scope vs. Chunk-Scope Kanten + +### 🔍 Empfehlungen + +**Keine kritischen Probleme gefunden.** Das System ist in einem stabilen Zustand. + +**Optional (nicht kritisch):** +- Erwägen Sie zusätzliche Unit-Tests für Phase 3 Validierung +- Dokumentation der `candidate:` → `verified` Transformation könnte erweitert werden + +--- + +## Audit-Methodik + +1. **Code-Analyse:** Vollständige Analyse der relevanten Dateien +2. **Semantic Search:** Suche nach Verwendungen von `candidate:`, `verified`, `all_chunk_callout_keys` +3. **Grep-Suche:** Exakte String-Suche nach kritischen Patterns +4. **Dokumentations-Review:** Prüfung der technischen Dokumentation + +**Geprüfte Dateien:** +- `app/core/retrieval/retriever_scoring.py` +- `app/services/llm_service.py` +- `app/core/retrieval/decision_engine.py` +- `app/core/graph/graph_derive_edges.py` +- `app/core/ingestion/ingestion_processor.py` +- `app/core/ingestion/ingestion_validation.py` +- `config/prompts.yaml` +- `config/llm_profiles.yaml` + +--- + +## 7. Zusätzliche Prüfungen & Bekannte Schwachstellen + +### 7.1 Callout-Extraktion aus Edge-Zonen (aus AUDIT_CLEAN_CONTEXT_V4.2.0) + +**Status:** ⚠️ **POTENZIELL BEHOBEN** (verifizieren erforderlich) + +**Hintergrund:** +- AUDIT_CLEAN_CONTEXT_V4.2.0 identifizierte ein kritisches Problem: Callouts in Edge-Zonen wurden nicht extrahiert +- Problem: Callouts wurden nur aus gefilterten Chunks extrahiert, nicht aus Original-Markdown + +**Aktueller Status:** +- ✅ Funktion `extract_callouts_from_markdown()` existiert in `graph_derive_edges.py` (Zeile 263-501) +- ✅ Funktion wird in `build_edges_for_note()` aufgerufen (Zeile 852-864) +- ⚠️ **VERIFIZIERUNG ERFORDERLICH:** Prüfen, ob Callouts in LLM-Validierungs-Zonen korrekt extrahiert werden + +**Code-Referenz:** +- `app/core/graph/graph_derive_edges.py` Zeile 263-501: `extract_callouts_from_markdown()` +- `app/core/graph/graph_derive_edges.py` Zeile 852-864: Aufruf in `build_edges_for_note()` + +**Empfehlung:** +- Test mit Callout in LLM-Validierungs-Zone durchführen +- Verifizieren, dass Edge in Qdrant `_edges` Collection existiert +- Prüfen, ob `candidate:` Präfix korrekt gesetzt wird + +--- + +### 7.2 Rejected Edges Tracking & Monitoring + +**Status:** ⚠️ **POTENZIELLE SCHWACHSTELLE** + +**Problem:** +- Phase 3 Validierung lehnt Kanten ab und fügt sie zu `rejected_edges` hinzu (Zeile 363) +- `rejected_edges` werden geloggt, aber **nicht persistiert** oder analysiert +- Keine Möglichkeit, abgelehnte Kanten zu überprüfen oder zu debuggen + +**Konsequenz:** +- **Fehlende Transparenz:** Keine Nachvollziehbarkeit, warum Kanten abgelehnt wurden +- **Keine Metriken:** Keine Statistiken über Ablehnungsrate +- **Schwieriges Debugging:** Bei Problemen keine Möglichkeit, abgelehnte Kanten zu analysieren + +**Code-Referenz:** +- `app/core/ingestion/ingestion_processor.py` Zeile 363: `rejected_edges.append(e)` +- `app/core/ingestion/ingestion_processor.py` Zeile 370-371: Logging, aber keine Persistierung + +**Empfehlung:** +- Optional: Persistierung von `rejected_edges` in Log-Datei oder separater Collection +- Metriken: Tracking der Ablehnungsrate pro Note/Typ +- Debug-Modus: Detailliertes Logging der Ablehnungsgründe + +--- + +### 7.3 Transiente vs. Permanente Fehler in Phase 3 Validierung + +**Status:** ✅ **FUNKTIONIERT KORREKT** + +**Ergebnis:** +- `validate_edge_candidate()` unterscheidet korrekt zwischen transienten und permanenten Fehlern (Zeile 79-91) +- Transiente Fehler (Netzwerk) → Kante wird erlaubt (Integrität vor Präzision) +- Permanente Fehler → Kante wird abgelehnt (Graph-Qualität schützen) + +**Code-Referenz:** +- `app/core/ingestion/ingestion_validation.py` Zeile 79-91: Fehlerbehandlung + +**Bewertung:** Korrekt implementiert. Keine Regression. + +--- + +### 7.4 Note-Scope Kontext-Optimierung: Chunk-Text Fallback + +**Status:** ⚠️ **POTENZIELLE SCHWACHSTELLE** + +**Problem:** +- Bei Chunk-Scope Kanten wird versucht, spezifischen Chunk-Text zu finden (Zeile 319-325) +- Fallback auf `note_text`, wenn Chunk-Text nicht gefunden wird +- **Risiko:** Bei fehlendem Chunk-Text wird Note-Text verwendet, was weniger präzise ist + +**Code-Referenz:** +- `app/core/ingestion/ingestion_processor.py` Zeile 318-326: Chunk-Text Suche + +**Empfehlung:** +- Prüfen, ob Chunk-Text immer verfügbar ist +- Bei fehlendem Chunk-Text: Warnung loggen +- Optional: Bessere Fehlerbehandlung für fehlende Chunk-IDs + +--- + +### 7.5 LLM-Validierungs-Zonen: Callout-Key Tracking + +**Status:** ✅ **FUNKTIONIERT KORREKT** + +**Ergebnis:** +- Callouts aus LLM-Validierungs-Zonen werden korrekt zu `all_chunk_callout_keys` hinzugefügt (Zeile 615) +- Verhindert Duplikate im globalen Scan +- Korrekte `candidate:` Präfix-Setzung + +**Code-Referenz:** +- `app/core/graph/graph_derive_edges.py` Zeile 604-616: LLM-Validierungs-Zone Callout-Tracking + +**Bewertung:** Korrekt implementiert. Keine Regression. + +--- + +### 7.6 Scope-Aware Edge Retrieval (aus AUDIT_RETRIEVER_V4.1.0) + +**Status:** ⚠️ **POTENZIELL BEHOBEN** (verifizieren erforderlich) + +**Hintergrund:** +- AUDIT_RETRIEVER_V4.1.0 identifizierte ein Problem: Retriever suchte nur nach Note-Level Edges, nicht Chunk-Level +- Problem: Chunk-Scope Edges wurden nicht explizit berücksichtigt + +**Aktueller Status:** +- ⚠️ **VERIFIZIERUNG ERFORDERLICH:** Prüfen, ob `fetch_edges_from_qdrant` Chunk-Level Edges korrekt lädt +- Dokumentation besagt, dass Optimierungen implementiert wurden + +**Empfehlung:** +- Test mit Chunk-Scope Edge durchführen +- Verifizieren, dass Edge im Retrieval-Ergebnis enthalten ist +- Prüfen, ob `chunk_id` Filter korrekt funktioniert + +--- + +### 7.7 Section-Filtering im Retrieval (aus AUDIT_RETRIEVER_V4.1.0) + +**Status:** ⚠️ **POTENZIELL BEHOBEN** (verifizieren erforderlich) + +**Hintergrund:** +- AUDIT_RETRIEVER_V4.1.0 identifizierte fehlende Filterung nach `target_section` +- Problem: Section-Links (`[[Note#Section]]`) wurden nicht präzise gefiltert + +**Aktueller Status:** +- ⚠️ **VERIFIZIERUNG ERFORDERLICH:** Prüfen, ob `target_section` Filter im Retrieval funktioniert +- Dokumentation besagt, dass Optimierungen implementiert wurden + +**Empfehlung:** +- Test mit Section-Link durchführen +- Verifizieren, dass nur relevante Chunks zurückgegeben werden +- Prüfen, ob `QueryRequest.target_section` korrekt verwendet wird + +--- + +### 7.8 Prompt-Integration: Explanation Layer + +**Status:** ⚠️ **UNKLAR** (aus AUDIT_CLEAN_CONTEXT_V4.2.0) + +**Problem:** +- Unklar, ob `explanation.related_edges` im LLM-Prompt verwendet werden +- Keine explizite Dokumentation der Prompt-Struktur für RAG-Kontext + +**Code-Referenz:** +- `app/core/retrieval/retriever.py` Zeile 150-252: `_build_explanation()` +- `app/routers/chat.py`: Prompt-Verwendung + +**Empfehlung:** +- Prüfen Sie `config/prompts.yaml` für `interview_template` und andere Templates +- Stellen Sie sicher, dass `{related_edges}` oder ähnliche Variablen im Prompt verwendet werden +- Dokumentieren Sie die Prompt-Struktur für RAG-Kontext + +--- + +### 7.9 Fallback-Synthese: Hardcodierter Prompt (aus AUDIT_WP25B_CODE_REVIEW) + +**Status:** ⚠️ **ARCHITEKTONISCHE INKONSISTENZ** + +**Problem:** +- Fallback-Synthese in `decision_engine.py` verwendet `prompt=` statt `prompt_key=` (Zeile 361) +- Inkonsistent mit WP25b-Architektur (Lazy-Loading) +- Keine modell-spezifischen Prompts im Fallback + +**Code-Referenz:** +- `app/core/retrieval/decision_engine.py` Zeile 360-363: Hardcodierter Prompt + +**Empfehlung:** +- Umstellen auf `prompt_key="fallback_synthesis"` mit `variables` +- Konsistenz mit WP25b-Architektur +- Modell-spezifische Optimierungen auch im Fallback + +**Schweregrad:** 🟡 Mittel (funktional, aber architektonisch inkonsistent) + +--- + +### 7.10 Edge-Registry: Unbekannte Kanten + +**Status:** ✅ **FUNKTIONIERT KORREKT** + +**Ergebnis:** +- Unbekannte Kanten-Typen werden in `unknown_edges.jsonl` protokolliert +- Edge-Registry normalisiert Kanten-Typen korrekt +- Keine Regression festgestellt + +**Code-Referenz:** +- `app/services/edge_registry.py`: Edge-Registry Implementierung + +**Bewertung:** Korrekt implementiert. Keine Regression. + +--- + +## 8. Zusammenfassung der zusätzlichen Prüfungen + +### ✅ Bestätigt funktionierend: +1. **Transiente vs. Permanente Fehler:** Korrekte Unterscheidung ✅ +2. **LLM-Validierungs-Zonen Callout-Tracking:** Korrekt implementiert ✅ +3. **Edge-Registry:** Funktioniert korrekt ✅ + +### ⚠️ Verifizierung erforderlich: +1. **Callout-Extraktion aus Edge-Zonen:** Funktion existiert, aber Verifizierung erforderlich +2. **Scope-Aware Edge Retrieval:** Potenziell behoben, Verifizierung erforderlich +3. **Section-Filtering:** Potenziell behoben, Verifizierung erforderlich + +### ⚠️ Potenzielle Schwachstellen: +1. **Rejected Edges Tracking:** Keine Persistierung oder Metriken +2. **Note-Scope Kontext-Optimierung:** Chunk-Text Fallback könnte verbessert werden +3. **Prompt-Integration:** Unklar, ob `explanation.related_edges` verwendet werden +4. **Fallback-Synthese:** Architektonische Inkonsistenz (hardcodierter Prompt) + +--- + +## 9. Empfohlene Follow-up Prüfungen + +### 9.1 Funktionale Tests + +1. **Callout in LLM-Validierungs-Zone:** + - Erstellen Sie eine Notiz mit Callout in `### Unzugeordnete Kanten` + - Verifizieren: Edge existiert in Qdrant mit `candidate:` Präfix + - Verifizieren: Edge wird in Phase 3 validiert + +2. **Chunk-Scope Edge Retrieval:** + - Erstellen Sie eine Note mit Chunk-Scope Edge + - Query mit `explain=True` + - Verifizieren: Edge erscheint in `explanation.related_edges` + +3. **Section-Link Retrieval:** + - Erstellen Sie einen Section-Link (`[[Note#Section]]`) + - Query mit `target_section="Section"` + - Verifizieren: Nur relevante Chunks werden zurückgegeben + +### 9.2 Metriken & Monitoring + +1. **Phase 3 Validierung Metriken:** + - Tracking der Validierungsrate (verified/rejected) + - Tracking der Ablehnungsgründe + - Monitoring der LLM-Validierungs-Performance + +2. **Edge-Statistiken:** + - Anzahl der `candidate:` Kanten pro Note + - Anzahl der verifizierten Kanten pro Note + - Anzahl der abgelehnten Kanten pro Note + +### 9.3 Dokumentation + +1. **Prompt-Struktur:** + - Dokumentieren Sie die Verwendung von `explanation.related_edges` in Prompts + - Erstellen Sie Beispiele für RAG-Kontext-Integration + +2. **Phase 3 Validierung:** + - Dokumentieren Sie den Validierungs-Prozess + - Erstellen Sie Troubleshooting-Guide für abgelehnte Kanten + +--- + +**Audit abgeschlossen:** ✅ System-Integrität bestätigt mit zusätzlichen Prüfungen diff --git a/docs/03_Technical_References/KONFIGURATION_EDGE_ZONEN.md b/docs/03_Technical_References/KONFIGURATION_EDGE_ZONEN.md new file mode 100644 index 0000000..c6bfc8b --- /dev/null +++ b/docs/03_Technical_References/KONFIGURATION_EDGE_ZONEN.md @@ -0,0 +1,242 @@ +# Konfiguration von Edge-Zonen Headern (v4.2.0) + +**Version:** v4.2.0 +**Status:** Aktiv + +## Übersicht + +Das Mindnet-System unterstützt zwei Arten von speziellen Markdown-Sektionen für Kanten: + +1. **LLM-Validierung Zonen** - Links, die vom LLM validiert werden +2. **Note-Scope Zonen** - Links, die der gesamten Note zugeordnet werden + +Die Header-Namen für beide Zonen-Typen sind über Umgebungsvariablen konfigurierbar. + +## Konfiguration via .env + +### LLM-Validierung Header + +**Umgebungsvariablen:** +- `MINDNET_LLM_VALIDATION_HEADERS` - Komma-separierte Liste von Header-Namen +- `MINDNET_LLM_VALIDATION_HEADER_LEVEL` - Header-Ebene (1-6, Default: 3 für `###`) + +**Format:** Komma-separierte Liste von Header-Namen + +**Default:** +``` +MINDNET_LLM_VALIDATION_HEADERS=Unzugeordnete Kanten,Edge Pool,Candidates +MINDNET_LLM_VALIDATION_HEADER_LEVEL=3 +``` + +**Beispiel:** +```env +MINDNET_LLM_VALIDATION_HEADERS=Unzugeordnete Kanten,Edge Pool,Candidates,Zu prüfende Links +MINDNET_LLM_VALIDATION_HEADER_LEVEL=3 +``` + +**Verwendung in Markdown:** +```markdown +### Unzugeordnete Kanten + +related_to:Ziel-Notiz +depends_on:Andere Notiz +``` + +**Wichtig:** Diese Bereiche werden **nicht als Chunks angelegt**, sondern nur die Kanten extrahiert. + +### Note-Scope Zone Header + +**Umgebungsvariablen:** +- `MINDNET_NOTE_SCOPE_ZONE_HEADERS` - Komma-separierte Liste von Header-Namen +- `MINDNET_NOTE_SCOPE_HEADER_LEVEL` - Header-Ebene (1-6, Default: 2 für `##`) + +**Format:** Komma-separierte Liste von Header-Namen + +**Default:** +``` +MINDNET_NOTE_SCOPE_ZONE_HEADERS=Smart Edges,Relationen,Global Links,Note-Level Relations,Globale Verbindungen +MINDNET_NOTE_SCOPE_HEADER_LEVEL=2 +``` + +**Beispiel:** +```env +MINDNET_NOTE_SCOPE_ZONE_HEADERS=Smart Edges,Relationen,Globale Verbindungen,Note-Level Links +MINDNET_NOTE_SCOPE_HEADER_LEVEL=2 +``` + +**Verwendung in Markdown:** +```markdown +## Smart Edges + +[[rel:depends_on|Globale Notiz]] +[[rel:part_of|System-Übersicht]] +``` + +**Wichtig:** Diese Bereiche werden **nicht als Chunks angelegt**, sondern nur die Kanten extrahiert. + +## Konfiguration in prod.env + +Fügen Sie die folgenden Zeilen zu Ihrer `.env` oder `config/prod.env` hinzu: + +```env +# --- WP-24c v4.2.0: Konfigurierbare Markdown-Header für Edge-Zonen --- +# Komma-separierte Liste von Headern für LLM-Validierung +MINDNET_LLM_VALIDATION_HEADERS=Unzugeordnete Kanten,Edge Pool,Candidates + +# Header-Ebene für LLM-Validierung (1-6, Default: 3 für ###) +MINDNET_LLM_VALIDATION_HEADER_LEVEL=3 + +# Komma-separierte Liste von Headern für Note-Scope Zonen +MINDNET_NOTE_SCOPE_ZONE_HEADERS=Smart Edges,Relationen,Global Links,Note-Level Relations,Globale Verbindungen + +# Header-Ebene für Note-Scope Zonen (1-6, Default: 2 für ##) +MINDNET_NOTE_SCOPE_HEADER_LEVEL=2 +``` + +**Wichtig:** Beide Zonen-Typen werden **nicht als Chunks angelegt**. Nur die Kanten werden extrahiert, der Text selbst wird vom Chunking ausgeschlossen. + +## Unterschiede + +### LLM-Validierung Zonen + +- **Header-Ebene:** Konfigurierbar via `MINDNET_LLM_VALIDATION_HEADER_LEVEL` (Default: 3 = `###`) +- **Zweck:** Links werden vom LLM validiert +- **Provenance:** `global_pool` +- **Scope:** `chunk` (wird Chunks zugeordnet) +- **Aktivierung:** Nur wenn `enable_smart_edge_allocation: true` +- **Chunking:** ❌ **Diese Bereiche werden NICHT als Chunks angelegt** - nur Kanten werden extrahiert + +**Beispiel:** +```markdown +### Unzugeordnete Kanten + +related_to:Mögliche Verbindung +depends_on:Unsichere Notiz +``` + +### Note-Scope Zonen + +- **Header-Ebene:** Konfigurierbar via `MINDNET_NOTE_SCOPE_HEADER_LEVEL` (Default: 2 = `##`) +- **Zweck:** Links werden der gesamten Note zugeordnet +- **Provenance:** `explicit:note_zone` +- **Scope:** `note` (Note-weite Verbindung) +- **Aktivierung:** Immer aktiv +- **Chunking:** ❌ **Diese Bereiche werden NICHT als Chunks angelegt** - nur Kanten werden extrahiert + +**Beispiel:** +```markdown +## Smart Edges + +[[rel:depends_on|Globale Notiz]] +[[rel:part_of|System-Übersicht]] +``` + +## Best Practices + +### ✅ Empfohlen + +1. **Konsistente Header-Namen:** + - Nutzen Sie aussagekräftige Namen + - Dokumentieren Sie die verwendeten Header in Ihrem Team + +2. **Minimale Konfiguration:** + - Nutzen Sie die Defaults, wenn möglich + - Nur bei Bedarf anpassen + +3. **Dokumentation:** + - Dokumentieren Sie benutzerdefinierte Header in Ihrer Projekt-Dokumentation + +### ❌ Vermeiden + +1. **Zu viele Header:** + - Zu viele Optionen können verwirrend sein + - Beschränken Sie sich auf 3-5 Header pro Typ + +2. **Ähnliche Namen:** + - Vermeiden Sie Header, die sich zu ähnlich sind + - Klare Unterscheidung zwischen LLM-Validierung und Note-Scope + +## Technische Details + +### Code-Referenzen + +- **LLM-Validierung:** `app/core/chunking/chunking_processor.py` (Zeile 66-72) +- **Note-Scope Zonen:** `app/core/graph/graph_derive_edges.py` → `get_note_scope_zone_headers()` + +### Fallback-Verhalten + +- Wenn die Umgebungsvariable nicht gesetzt ist, werden die Defaults verwendet +- Wenn die Variable leer ist, werden ebenfalls die Defaults verwendet +- Header-Namen werden case-insensitive verglichen + +### Regex-Escape + +- Header-Namen werden automatisch für Regex escaped +- Sonderzeichen in Header-Namen sind sicher + +## Beispiel-Konfiguration + +```env +# Eigene Header-Namen für LLM-Validierung (H3) +MINDNET_LLM_VALIDATION_HEADERS=Zu prüfende Links,Kandidaten,Edge Pool +MINDNET_LLM_VALIDATION_HEADER_LEVEL=3 + +# Eigene Header-Namen für Note-Scope Zonen (H2) +MINDNET_NOTE_SCOPE_ZONE_HEADERS=Globale Relationen,Note-Verbindungen,Smart Links +MINDNET_NOTE_SCOPE_HEADER_LEVEL=2 +``` + +**Alternative:** Beide auf H2 setzen: +```env +MINDNET_LLM_VALIDATION_HEADER_LEVEL=2 +MINDNET_NOTE_SCOPE_HEADER_LEVEL=2 +``` + +**Verwendung:** +```markdown +--- +type: decision +title: Meine Notiz +--- + +# Inhalt + +## Globale Relationen + +[[rel:depends_on|System-Architektur]] + +### Zu prüfende Links + +related_to:Mögliche Verbindung +``` + +## FAQ + +**Q: Kann ich beide Zonen-Typen in einer Notiz verwenden?** +A: Ja, beide können gleichzeitig verwendet werden. + +**Q: Was passiert, wenn ein Header in beiden Listen steht?** +A: Die Note-Scope Zone hat Vorrang (wird als Note-Scope behandelt). + +**Q: Können Header-Namen Leerzeichen enthalten?** +A: Ja, Leerzeichen werden beibehalten. + +**Q: Werden Header-Namen case-sensitive verglichen?** +A: Nein, der Vergleich ist case-insensitive. + +**Q: Kann ich Header-Namen mit Sonderzeichen verwenden?** +A: Ja, Sonderzeichen werden automatisch für Regex escaped. + +## Zusammenfassung + +- ✅ **LLM-Validierung:** + - `MINDNET_LLM_VALIDATION_HEADERS` (Header-Namen, komma-separiert) + - `MINDNET_LLM_VALIDATION_HEADER_LEVEL` (Header-Ebene 1-6, Default: 3) + - ❌ **Nicht als Chunks angelegt** - nur Kanten werden extrahiert +- ✅ **Note-Scope Zonen:** + - `MINDNET_NOTE_SCOPE_ZONE_HEADERS` (Header-Namen, komma-separiert) + - `MINDNET_NOTE_SCOPE_HEADER_LEVEL` (Header-Ebene 1-6, Default: 2) + - ❌ **Nicht als Chunks angelegt** - nur Kanten werden extrahiert +- ✅ **Format:** Komma-separierte Liste für Header-Namen +- ✅ **Fallback:** Defaults werden verwendet, falls nicht konfiguriert +- ✅ **Case-insensitive:** Header-Namen werden case-insensitive verglichen diff --git a/docs/04_Operations/04_admin_operations.md b/docs/04_Operations/04_admin_operations.md index 73780aa..f9191ce 100644 --- a/docs/04_Operations/04_admin_operations.md +++ b/docs/04_Operations/04_admin_operations.md @@ -1,10 +1,10 @@ --- doc_type: operations_manual audience: admin, devops -scope: deployment, maintenance, backup, edge_registry, moe, lazy_prompts +scope: deployment, maintenance, backup, edge_registry, moe, lazy_prompts, agentic_validation status: active -version: 3.1.1 -context: "Installationsanleitung, Systemd-Units und Wartungsprozesse für Mindnet v3.1.1 inklusive WP-25a Mixture of Experts (MoE) und WP-25b Lazy-Prompt-Orchestration Konfiguration." +version: 4.5.8 +context: "Installationsanleitung, Systemd-Units und Wartungsprozesse für Mindnet v4.5.8 inklusive WP-25a Mixture of Experts (MoE), WP-25b Lazy-Prompt-Orchestration und WP-24c Phase 3 Agentic Edge Validation Konfiguration." --- # Admin Operations Guide @@ -246,6 +246,24 @@ Bevor du spezifische Fehler behebst, führe diese Checks durch: 1. Füge fehlende Typen als Aliase in `01_edge_vocabulary.md` hinzu 2. Oder verwende kanonische Typen aus der Registry +**Fehler: "Phase 3 Validierung schlägt fehl" (WP-24c v4.5.8)** +* **Symptom:** Links in `### Unzugeordnete Kanten` werden nicht validiert oder abgelehnt. +* **Diagnose:** Prüfe Logs auf `🚀 [PHASE 3]` und `🚫 [PHASE 3] REJECTED`. +* **Lösung:** + 1. Prüfe `MINDNET_LLM_VALIDATION_HEADERS` in `.env` (Standard: `Unzugeordnete Kanten,Edge Pool,Candidates`) + 2. Prüfe `MINDNET_LLM_VALIDATION_HEADER_LEVEL` (Standard: `3` für `###`) + 3. Prüfe `llm_profiles.yaml` - `ingest_validator` Profil muss existieren + 4. Prüfe LLM-Verfügbarkeit (Ollama/OpenRouter) + 5. **Hinweis:** Transiente Fehler (Netzwerk) erlauben die Kante, permanente Fehler lehnen sie ab + +**Fehler: "Note-Scope Links werden nicht erkannt" (WP-24c v4.2.0)** +* **Symptom:** Links in `## Smart Edges` Zonen werden nicht als Note-Scope behandelt. +* **Diagnose:** Prüfe Logs auf Note-Scope Extraktion. +* **Lösung:** + 1. Prüfe `MINDNET_NOTE_SCOPE_ZONE_HEADERS` in `.env` (Standard: `Smart Edges,Relationen,Global Links`) + 2. Prüfe `MINDNET_NOTE_SCOPE_HEADER_LEVEL` (Standard: `2` für `##`) + 3. Header-Namen müssen exakt (case-insensitive) übereinstimmen + #### Performance-Optimierung **Problem: Langsame Chat-Antworten** diff --git a/docs/05_Development/05_developer_guide.md b/docs/05_Development/05_developer_guide.md index 3fced84..f065559 100644 --- a/docs/05_Development/05_developer_guide.md +++ b/docs/05_Development/05_developer_guide.md @@ -1,10 +1,10 @@ --- doc_type: developer_guide audience: developer -scope: workflow, testing, architecture, modules, modularization, agentic_rag, lazy_prompts +scope: workflow, testing, architecture, modules, modularization, agentic_rag, lazy_prompts, agentic_validation status: active -version: 3.1.1 -context: "Umfassender Guide für Entwickler: Modularisierte Architektur (WP-14), Two-Pass Ingestion (WP-15b), WP-25 Agentic Multi-Stream RAG, WP-25a MoE, WP-25b Lazy-Prompt-Orchestration, Modul-Interna, Setup und Git-Workflow." +version: 4.5.8 +context: "Umfassender Guide für Entwickler: Modularisierte Architektur (WP-14), Two-Pass Ingestion (WP-15b), WP-25 Agentic Multi-Stream RAG, WP-25a MoE, WP-25b Lazy-Prompt-Orchestration, WP-24c Phase 3 Agentic Edge Validation (v4.5.8), Modul-Interna, Setup und Git-Workflow." --- # Mindnet Developer Guide & Workflow @@ -225,7 +225,7 @@ Das Backend ist das Herzstück. Es stellt die Logik via REST-API bereit. | **`app/core/chunking/`** | Text-Segmentierung | `chunking_strategies.py` (Sliding/Heading), `chunking_processor.py` (Orchestrierung) | | **`app/core/database/`** | Qdrant-Infrastruktur | `qdrant.py` (Client), `qdrant_points.py` (Point-Mapping) | | **`app/core/graph/`** | Graph-Logik | `graph_subgraph.py` (Expansion), `graph_weights.py` (Scoring) | -| **`app/core/ingestion/`** | Import-Pipeline | `ingestion_processor.py` (Two-Pass), `ingestion_validation.py` (Mistral-safe Parsing) | +| **`app/core/ingestion/`** | Import-Pipeline | `ingestion_processor.py` (3-Phasen-Modell: Pre-Scan, Semantic Processing, Phase 3 Agentic Validation), `ingestion_validation.py` (Mistral-safe Parsing, Phase 3 Validierung) | | **`app/core/parser/`** | Markdown-Parsing | `parsing_markdown.py` (Frontmatter/Body), `parsing_scanner.py` (File-Scan) | | **`app/core/retrieval/`** | Suche & Scoring | `retriever.py` (Orchestrator), `retriever_scoring.py` (Mathematik) | | **`app/core/registry.py`** | SSOT & Utilities | Text-Bereinigung, Circular-Import-Fix | @@ -434,6 +434,14 @@ Mindnet lernt nicht durch Training (Fine-Tuning), sondern durch **Konfiguration* ``` *Ergebnis (WP-25b):* Hierarchische Prompt-Resolution mit Lazy-Loading. Prompts werden erst zur Laufzeit geladen, basierend auf aktivem Modell. Maximale Resilienz bei Modell-Fallbacks. +5. **Phase 3 Validierung (WP-24c v4.5.8):** Kanten mit `candidate:` Präfix werden automatisch in Phase 3 validiert: + * **Trigger:** Kanten in Header-Zonen (konfiguriert via `MINDNET_LLM_VALIDATION_HEADERS`) erhalten `candidate:` Präfix + * **Validierung:** Nutzt `ingest_validator` Profil (Temperature 0.0) für deterministische YES/NO Entscheidungen + * **Kontext-Optimierung:** Note-Scope nutzt `note_summary`, Chunk-Scope nutzt spezifischen Chunk-Text + * **Erfolg:** Entfernt `candidate:` Präfix, Kante wird persistiert + * **Ablehnung:** Kante wird zu `rejected_edges` hinzugefügt und **nicht** in DB geschrieben + * **Logging:** `🚀 [PHASE 3]` für Start, `✅ [PHASE 3] VERIFIED` für Erfolg, `🚫 [PHASE 3] REJECTED` für Ablehnung + ### Workflow B: Graph-Farben ändern 1. Öffne `app/frontend/ui_config.py`. 2. Bearbeite das Dictionary `GRAPH_COLORS`. diff --git a/docs/05_Development/05_testing_guide.md b/docs/05_Development/05_testing_guide.md index 445b0de..6c3151c 100644 --- a/docs/05_Development/05_testing_guide.md +++ b/docs/05_Development/05_testing_guide.md @@ -1,10 +1,10 @@ --- doc_type: developer_guide audience: developer, tester -scope: testing, quality_assurance, test_strategies +scope: testing, quality_assurance, test_strategies, agentic_validation status: active -version: 2.9.3 -context: "Umfassender Test-Guide für Mindnet: Test-Strategien, Test-Frameworks, Test-Daten und Best Practices inklusive WP-25 Multi-Stream RAG." +version: 4.5.8 +context: "Umfassender Test-Guide für Mindnet: Test-Strategien, Test-Frameworks, Test-Daten und Best Practices inklusive WP-25 Multi-Stream RAG und WP-24c Phase 3 Agentic Edge Validation." --- # Testing Guide @@ -272,16 +272,26 @@ class TestIngest(unittest.IsolatedAsyncioTestCase): ### 4.5 Ingestion-Tests **Was wird getestet:** -- Two-Pass Workflow +- Two-Pass Workflow (Pre-Scan, Semantic Processing) +- Phase 3 Agentic Edge Validation (WP-24c v4.5.8) - Change Detection (Hash-basiert) - Background Tasks - Smart Edge Allocation +- Automatische Spiegelkanten (Invers-Logik) **Tests:** - `tests/test_dialog_full_flow.py` - `tests/test_WP22_intelligence.py` - `scripts/import_markdown.py` (mit `--dry-run`) +**WP-24c Spezifische Tests (geplant):** +- candidate: Präfix-Setzung (Links in `### Unzugeordnete Kanten`) +- Phase 3 Validierung (VERIFIED/REJECTED) +- Kontext-Optimierung (Note-Scope nutzt Note-Summary, Chunk-Scope nutzt Chunk-Text) +- Automatische Spiegelkanten (Invers-Logik) +- Fehlertoleranz (transient vs. permanent) +- Rejected Edges Tracking (Kanten werden nicht persistiert) + --- ## 5. Continuous Integration diff --git a/docs/06_Roadmap/06_active_roadmap.md b/docs/06_Roadmap/06_active_roadmap.md index 6456809..9ad4969 100644 --- a/docs/06_Roadmap/06_active_roadmap.md +++ b/docs/06_Roadmap/06_active_roadmap.md @@ -2,14 +2,14 @@ doc_type: roadmap audience: product_owner, developer status: active -version: 3.1.1 -context: "Aktuelle Planung für kommende Features (ab WP16), Release-Strategie und Historie der abgeschlossenen WPs nach WP-14/15b/15c/25/25a/25b." +version: 4.5.8 +context: "Aktuelle Planung für kommende Features (ab WP16), Release-Strategie und Historie der abgeschlossenen WPs nach WP-14/15b/15c/25/25a/25b/24c." --- # Mindnet Active Roadmap -**Aktueller Stand:** v3.1.1 (Post-WP25b: Lazy-Prompt-Orchestration & Full Resilience) -**Fokus:** Hierarchische Prompt-Resolution, Modell-spezifisches Tuning & maximale Resilienz. +**Aktueller Stand:** v4.5.8 (Post-WP24c: Phase 3 Agentic Edge Validation - Integrity Baseline) +**Fokus:** Chunk-Aware Multigraph-System, Agentic Edge Validation, Graph-Qualitätssicherung. | Phase | Fokus | Status | | :--- | :--- | :--- | @@ -52,6 +52,7 @@ Eine Übersicht der implementierten Features zum schnellen Auffinden von Funktio | **WP-25** | **Agentic Multi-Stream RAG Orchestration** | **Ergebnis:** Übergang von linearer RAG-Architektur zu paralleler Multi-Stream Engine. Intent-basiertes Routing (Hybrid Fast/Slow-Path), parallele Wissens-Streams (Values, Facts, Biography, Risk, Tech), Stream-Tracing und Template-basierte Wissens-Synthese. | | **WP-25a** | **Mixture of Experts (MoE) & Fallback-Kaskade** | **Ergebnis:** Profilbasierte Experten-Architektur, rekursive Fallback-Kaskade, Pre-Synthesis Kompression, profilgesteuerte Ingestion und Embedding-Konsolidierung. | | **WP-25b** | **Lazy-Prompt-Orchestration & Full Resilience** | **Ergebnis:** Hierarchisches Prompt-Resolution-System (3-stufig), Lazy-Prompt-Loading, ultra-robustes Intent-Parsing, differenzierte Ingestion-Validierung und PROMPT-TRACE Logging. | +| **WP-24c** | **Phase 3 Agentic Edge Validation & Graph Integrity** | **Ergebnis:** Finales Validierungs-Gate für `candidate:` Kanten, dynamische Kontext-Optimierung (Note-Scope vs. Chunk-Scope), Verhinderung von "Geister-Verknüpfungen" und Graph-Qualitätssicherung. Transformation zu einem Chunk-Aware Multigraph-System. | ### 2.1 WP-22 Lessons Learned * **Architektur:** Die Trennung von `retriever.py` und `retriever_scoring.py` war notwendig, um LLM-Context-Limits zu wahren und die Testbarkeit der mathematischen Formeln zu erhöhen. @@ -241,6 +242,36 @@ Der bisherige WP-15 Ansatz litt unter Halluzinationen (erfundene Kantentypen), h - Kontext-Budgeting: Intelligente Token-Verteilung - Stream-specific Provider: Unterschiedliche KI-Modelle pro Wissensbereich - Erweiterte Prompt-Optimierung: Dynamische Anpassung basierend auf Kontext und Historie + +### WP-24c: Phase 3 Agentic Edge Validation & Graph Integrity +**Status:** ✅ Fertig (v4.5.8) + +**Ergebnis:** Transformation des Systems von einem dokumentenbasierten RAG zu einem **Chunk-Aware Multigraph-System** mit finalem Validierungs-Gate für alle `candidate:` Kanten. Verhindert "Geister-Verknüpfungen" und sichert die Graph-Qualität durch agentische LLM-Validierung. + +**Kern-Features:** +1. **Phase 3 Validierungs-Gate:** Finales Validierungs-Gate für alle Kanten mit `candidate:` Präfix in `rule_id` oder `provenance` +2. **Dynamische Kontext-Optimierung:** Intelligente Kontext-Auswahl basierend auf `scope`: + - **Note-Scope:** Nutzt `note_summary` (Top 5 Chunks) oder `note_text` (aggregierter Gesamttext) + - **Chunk-Scope:** Nutzt spezifischen Chunk-Text, falls verfügbar, sonst Fallback auf Note-Text +3. **Agentic Edge Validation:** LLM-basierte semantische Prüfung via `ingest_validator` Profil (Temperature 0.0) +4. **Fehlertoleranz:** Differenzierte Behandlung von transienten (Netzwerk) vs. permanenten (Config) Fehlern +5. **Graph-Qualitätssicherung:** Rejected Edges werden **nicht** in die Datenbank geschrieben, verhindert persistente "Geister-Verknüpfungen" + +**Technische Details:** +- Ingestion Processor v4.5.8: 3-Phasen-Modell (Pre-Scan, Semantic Processing, Phase 3 Validation) +- Ingestion Validation v2.14.0: `validate_edge_candidate()` mit MoE-Integration +- Kontext-Optimierung: Note-Summary/Text für Note-Scope, Chunk-Text für Chunk-Scope +- Logging: `🚀 [PHASE 3]` für Start, `✅ [PHASE 3] VERIFIED` für Erfolg, `🚫 [PHASE 3] REJECTED` für Ablehnung + +**System-Historie (v4.1.0 - v4.5.8):** +- v4.1.0 (Gold-Standard): Einführung der Scope-Awareness und Section-Filterung +- v4.4.1 (Clean-Context): Entfernung technischer Callouts vor Vektorisierung +- v4.5.0 - v4.5.3: Debugging & Härtung (Pydantic EdgeDTO, Retrieval-Tracer) +- v4.5.4: Attribut-Synchronisation (QueryHit-Modelle) +- v4.5.5: Effizienz-Optimierung (Context-Persistence) +- v4.5.7: Stabilitäts-Fix & Zonen-Mapping (UnboundLocalError, Zonen-Inversion) +- v4.5.8: Agentic Validation Gate (Phase 3, Kontext-Optimierung, Audit verifiziert) + --- ### WP-24 – Proactive Discovery & Agentic Knowledge Mining diff --git a/docs/99_Archive/WP24c_merge_commit.md b/docs/99_Archive/WP24c_merge_commit.md new file mode 100644 index 0000000..471fd8f --- /dev/null +++ b/docs/99_Archive/WP24c_merge_commit.md @@ -0,0 +1,113 @@ +# Branch Merge Commit: WP-24c + +**Branch:** `WP24c` +**Target:** `main` +**Version:** v4.5.8 +**Date:** 2026-01-XX + +--- + +## Commit Message + +``` +feat: Phase 3 Agentic Edge Validation & Chunk-Aware Multigraph-System (v4.5.8) + +### Phase 3 Agentic Edge Validation +- Finales Validierungs-Gate für Kanten mit candidate: Präfix +- LLM-basierte semantische Prüfung gegen Kontext (Note-Scope vs. Chunk-Scope) +- Differenzierte Fehlerbehandlung: Transiente Fehler erlauben Kante, permanente Fehler lehnen ab +- Kontext-Optimierung: Note-Scope nutzt Note-Summary/Text, Chunk-Scope nutzt spezifischen Chunk-Text +- Implementierung in app/core/ingestion/ingestion_validation.py (v2.14.0) + +### Automatische Spiegelkanten (Invers-Logik) +- Automatische Erzeugung von Spiegelkanten für explizite Verbindungen +- Phase 2 Batch-Injektion am Ende des Imports +- Authority-Check: Explizite Kanten haben Vorrang (keine Duplikate) +- Provenance Firewall: System-Kanten können nicht manuell überschrieben werden +- Implementierung in app/core/ingestion/ingestion_processor.py (v2.13.12) + +### Note-Scope Zonen (v4.2.0) +- Globale Verbindungen für ganze Notizen (scope: note) +- Konfigurierbare Header-Namen via ENV-Variablen +- Höchste Priorität bei Duplikaten +- Phase 3 Validierung nutzt Note-Summary/Text für bessere Präzision +- Implementierung in app/core/graph/graph_derive_edges.py (v1.1.2) + +### Chunk-Aware Multigraph-System +- Section-basierte Links: [[Note#Section]] wird präzise in target_id und target_section aufgeteilt +- Multigraph-Support: Mehrere Kanten zwischen denselben Knoten möglich (verschiedene Sections) +- Semantische Deduplizierung basierend auf src->tgt:kind@sec Key +- Metadaten-Persistenz: target_section, provenance, confidence bleiben erhalten + +### Code-Komponenten +- app/core/ingestion/ingestion_validation.py: v2.14.0 (Phase 3 Validierung, Kontext-Optimierung) +- app/core/ingestion/ingestion_processor.py: v2.13.12 (Automatische Spiegelkanten, Authority-Check) +- app/core/graph/graph_derive_edges.py: v1.1.2 (Note-Scope Zonen, LLM-Validierung Zonen) +- app/core/chunking/chunking_processor.py: v2.13.0 (LLM-Validierung Zonen Erkennung) +- app/core/chunking/chunking_parser.py: v2.12.0 (Header-Level Erkennung, Zonen-Extraktion) + +### Konfiguration +- Neue ENV-Variablen für konfigurierbare Header: + - MINDNET_LLM_VALIDATION_HEADERS (Default: "Unzugeordnete Kanten,Edge Pool,Candidates") + - MINDNET_LLM_VALIDATION_HEADER_LEVEL (Default: 3) + - MINDNET_NOTE_SCOPE_ZONE_HEADERS (Default: "Smart Edges,Relationen,Global Links,Note-Level Relations,Globale Verbindungen") + - MINDNET_NOTE_SCOPE_HEADER_LEVEL (Default: 2) +- config/llm_profiles.yaml: ingest_validator Profil für Phase 3 Validierung (Temperature 0.0) +- config/prompts.yaml: edge_validation Prompt für Phase 3 Validierung + +### Dokumentation +- 01_knowledge_design.md: Automatische Spiegelkanten, Phase 3 Validierung, Note-Scope Zonen +- NOTE_SCOPE_ZONEN.md: Phase 3 Validierung integriert +- LLM_VALIDIERUNG_VON_LINKS.md: Phase 3 statt global_pool, Kontext-Optimierung +- 02_concept_graph_logic.md: Phase 3 Validierung, automatische Spiegelkanten, Note-Scope vs. Chunk-Scope +- 03_tech_data_model.md: candidate: Präfix, verified Status, virtual Flag, scope Feld +- 03_tech_configuration.md: Neue ENV-Variablen dokumentiert +- 04_admin_operations.md: Troubleshooting für Phase 3 Validierung und Note-Scope Links +- 05_testing_guide.md: WP-24c Test-Szenarien hinzugefügt +- 00_quality_checklist.md: WP-24c Features in Checkliste aufgenommen +- README.md: Version auf v4.5.8 aktualisiert, WP-24c Features verlinkt + +### Breaking Changes +- Keine Breaking Changes für Endbenutzer +- Vollständige Rückwärtskompatibilität +- Bestehende Notizen funktionieren ohne Änderungen + +### Migration +- Keine Migration erforderlich +- System funktioniert ohne Änderungen +- Optional: ENV-Variablen können für Custom-Header konfiguriert werden + +--- + +**Status:** ✅ WP-24c ist zu 100% implementiert und audit-geprüft. +**Nächster Schritt:** WP-25c (Kontext-Budgeting & Erweiterte Prompt-Optimierung). +``` + +--- + +## Zusammenfassung + +Dieser Merge führt die **Phase 3 Agentic Edge Validation** und das **Chunk-Aware Multigraph-System** in MindNet ein. Das System validiert nun automatisch Kanten mit `candidate:` Präfix, erzeugt automatisch Spiegelkanten für explizite Verbindungen und unterstützt Note-Scope Zonen für globale Verbindungen. + +**Kern-Features:** +- Phase 3 Agentic Edge Validation (finales Validierungs-Gate) +- Automatische Spiegelkanten (Invers-Logik) +- Note-Scope Zonen (globale Verbindungen) +- Chunk-Aware Multigraph-System (Section-basierte Links) + +**Technische Integrität:** +- Alle Kanten durchlaufen Phase 3 Validierung (falls candidate: Präfix) +- Spiegelkanten werden automatisch erzeugt (Phase 2) +- Note-Scope Links haben höchste Priorität +- Kontext-Optimierung für bessere Validierungs-Genauigkeit + +**Dokumentation:** +- Vollständige Aktualisierung aller relevanten Dokumente +- Neue ENV-Variablen dokumentiert +- Troubleshooting-Guide erweitert +- Test-Szenarien hinzugefügt + +**Deployment:** +- Keine Breaking Changes +- Optional: ENV-Variablen für Custom-Header konfigurieren +- System funktioniert ohne Änderungen diff --git a/docs/99_Archive/WP24c_release_notes.md b/docs/99_Archive/WP24c_release_notes.md new file mode 100644 index 0000000..4d73a76 --- /dev/null +++ b/docs/99_Archive/WP24c_release_notes.md @@ -0,0 +1,407 @@ +# MindNet v4.5.8 - Release Notes: WP-24c + +**Release Date:** 2026-01-XX +**Type:** Feature Release - Phase 3 Agentic Edge Validation & Chunk-Aware Multigraph-System +**Version:** 4.5.8 (WP-24c) + +--- + +## 🎯 Überblick + +Mit WP-24c wurde MindNet um ein **finales Validierungs-Gate (Phase 3 Agentic Edge Validation)** erweitert, das "Geister-Verknüpfungen" verhindert und die Graph-Qualität sichert. Zusätzlich wurde das System um **automatische Spiegelkanten (Invers-Logik)** und **Note-Scope Zonen** erweitert, die es ermöglichen, globale Verbindungen für ganze Notizen zu definieren. + +Diese Version markiert einen wichtigen Schritt zur **Graph-Integrität**: Von manueller Kanten-Pflege hin zu automatischer Validierung und bidirektionaler Durchsuchbarkeit. + +--- + +## ✨ Neue Features + +### 1. Phase 3 Agentic Edge Validation + +**Implementierung (`app/core/ingestion/ingestion_validation.py` v2.14.0):** + +Finales Validierungs-Gate für alle Kanten mit `candidate:` Präfix: + +* **Trigger-Kriterium:** Kanten in `### Unzugeordnete Kanten` Sektionen erhalten `candidate:` Präfix +* **Validierungsprozess:** LLM prüft semantisch, ob die Verbindung zum Kontext passt +* **Ergebnis:** VERIFIED (Präfix entfernt, persistiert) oder REJECTED (nicht in DB geschrieben) +* **Kontext-Optimierung:** Note-Scope nutzt Note-Summary/Text, Chunk-Scope nutzt spezifischen Chunk-Text + +**Vorteile:** +* **Graph-Qualität:** Verhindert persistente "Geister-Verknüpfungen" +* **Präzision:** Höhere Validierungs-Genauigkeit durch Kontext-Optimierung +* **Fehlertoleranz:** Unterscheidung zwischen transienten (Netzwerk) und permanenten (Config) Fehlern + +### 2. Automatische Spiegelkanten (Invers-Logik) + +**Implementierung (`app/core/ingestion/ingestion_processor.py` v2.13.12):** + +Automatische Erzeugung von Spiegelkanten für explizite Verbindungen: + +* **Funktionsweise:** Explizite Kante `A depends_on: B` erzeugt automatisch `B enforced_by: A` +* **Priorität:** Explizite Kanten haben Vorrang (keine Duplikate) +* **Schutz:** System-Kanten (`belongs_to`, `next`, `prev`) können nicht manuell überschrieben werden +* **Phase 2 Injektion:** Spiegelkanten werden am Ende des Imports in einem Batch-Prozess injiziert + +**Vorteile:** +* **Bidirektionale Durchsuchbarkeit:** Beide Richtungen sind durchsuchbar ohne manuelle Pflege +* **Konsistenz:** Volle Graph-Konsistenz ohne "Link-Nightmare" +* **Höhere Wirksamkeit:** Explizite Kanten haben höhere Confidence-Werte als automatisch generierte + +### 3. Note-Scope Zonen (v4.2.0) + +**Implementierung (`app/core/graph/graph_derive_edges.py` v1.1.2):** + +Globale Verbindungen für ganze Notizen: + +* **Format:** Links in `## Smart Edges` Zonen werden als `scope: note` behandelt +* **Priorität:** Höchste Priorität bei Duplikaten +* **Phase 3 Validierung:** Nutzt Note-Summary (Top 5 Chunks) oder Note-Text für bessere Validierung +* **Konfigurierbar:** Header-Namen und -Ebene via ENV-Variablen + +**Vorteile:** +* **Globale Verbindungen:** Links gelten für die gesamte Note, nicht nur einen Abschnitt +* **Bessere Validierung:** Note-Kontext ermöglicht präzisere LLM-Validierung +* **Flexibilität:** Konfigurierbare Header-Namen für verschiedene Workflows + +### 4. Chunk-Aware Multigraph-System + +**Erweiterung des bestehenden Multigraph-Systems:** + +* **Section-basierte Links:** `[[Note#Section]]` wird präzise in `target_id` und `target_section` aufgeteilt +* **Multigraph-Support:** Mehrere Kanten zwischen denselben Knoten möglich, wenn sie auf verschiedene Sections zeigen +* **Semantische Deduplizierung:** Basierend auf `src->tgt:kind@sec` Key + +**Vorteile:** +* **Präzision:** Präzise Verlinkung innerhalb langer Dokumente +* **Flexibilität:** Mehrere Verbindungen zur gleichen Note möglich +* **Konsistenz:** Verhindert "Phantom-Knoten" + +--- + +## 🔧 Technische Änderungen + +### Konfigurationsdateien + +**`config/llm_profiles.yaml` (v1.3.0):** +* **Keine Änderungen:** Bestehende Profile bleiben unverändert +* **`ingest_validator` Profil:** Wird für Phase 3 Validierung genutzt (Temperature 0.0 für Determinismus) + +**`config/prompts.yaml` (v3.2.2):** +* **Keine Änderungen:** Bestehende Prompts bleiben unverändert +* **`edge_validation` Prompt:** Wird für Phase 3 Validierung genutzt + +### Environment Variablen (`.env`) + +**Neue Variablen für WP-24c:** + +```env +# --- WP-24c v4.2.0: Konfigurierbare Markdown-Header für Edge-Zonen --- +# Komma-separierte Liste von Headern für LLM-Validierung +# Format: Header1,Header2,Header3 +MINDNET_LLM_VALIDATION_HEADERS=Unzugeordnete Kanten,Edge Pool,Candidates + +# Header-Ebene für LLM-Validierung (1-6, Default: 3 für ###) +MINDNET_LLM_VALIDATION_HEADER_LEVEL=3 + +# Komma-separierte Liste von Headern für Note-Scope Zonen +# Format: Header1,Header2,Header3 +MINDNET_NOTE_SCOPE_ZONE_HEADERS=Smart Edges,Relationen,Global Links,Note-Level Relations,Globale Verbindungen + +# Header-Ebene für Note-Scope Zonen (1-6, Default: 2 für ##) +MINDNET_NOTE_SCOPE_HEADER_LEVEL=2 +``` + +**Default-Werte:** +* `MINDNET_LLM_VALIDATION_HEADERS`: `Unzugeordnete Kanten,Edge Pool,Candidates` +* `MINDNET_LLM_VALIDATION_HEADER_LEVEL`: `3` (für `###`) +* `MINDNET_NOTE_SCOPE_ZONE_HEADERS`: `Smart Edges,Relationen,Global Links,Note-Level Relations,Globale Verbindungen` +* `MINDNET_NOTE_SCOPE_HEADER_LEVEL`: `2` (für `##`) + +**Hinweis:** Falls diese Variablen nicht gesetzt sind, werden die Default-Werte verwendet. Das System funktioniert ohne explizite Konfiguration. + +### Code-Komponenten + +**Neue/Erweiterte Module:** + +* `app/core/ingestion/ingestion_validation.py`: v2.14.0 + * Phase 3 Validierung mit Kontext-Optimierung + * Differenzierte Fehlerbehandlung (transient vs. permanent) + * Lazy-Prompt-Orchestration Integration + +* `app/core/ingestion/ingestion_processor.py`: v2.13.12 + * Automatische Spiegelkanten-Generierung (Phase 2) + * Authority-Check für explizite Kanten + * ID-Konsistenz mit Phase 1 + +* `app/core/graph/graph_derive_edges.py`: v1.1.2 + * Note-Scope Zonen Extraktion + * LLM-Validierung Zonen Extraktion + * Konfigurierbare Header-Erkennung + +* `app/core/chunking/chunking_processor.py`: v2.13.0 + * LLM-Validierung Zonen Erkennung + * candidate: Präfix-Setzung + +* `app/core/chunking/chunking_parser.py`: v2.12.0 + * Header-Level Erkennung + * Zonen-Extraktion + +--- + +## 📋 Migration Guide + +### Für Endbenutzer + +**Keine Migration erforderlich!** Das System funktioniert ohne Änderungen. + +**Optionale Nutzung neuer Features:** + +1. **Explizite Links (empfohlen):** + ```markdown + Diese Entscheidung [[rel:depends_on Performance-Analyse]] wurde getroffen. + ``` + * Sofortige Übernahme, höchste Priorität, keine Validierung + +2. **Validierte Links (für explorative Verbindungen):** + ```markdown + ### Unzugeordnete Kanten + + related_to:Mögliche Verbindung + depends_on:Unsicherer Link + ``` + * Phase 3 Validierung, kann abgelehnt werden + +3. **Note-Scope Links (für globale Verbindungen):** + ```markdown + ## Smart Edges + + [[rel:depends_on|Projekt-Übersicht]] + [[rel:part_of|Größeres System]] + ``` + * Globale Verbindung für ganze Note, höchste Priorität + +### Für Administratoren + +**1. Environment Variablen hinzufügen (optional):** + +Fügen Sie die folgenden Zeilen zu Ihrer `.env` oder `config/prod.env` hinzu: + +```env +# --- WP-24c v4.2.0: Konfigurierbare Markdown-Header für Edge-Zonen --- +MINDNET_LLM_VALIDATION_HEADERS=Unzugeordnete Kanten,Edge Pool,Candidates +MINDNET_LLM_VALIDATION_HEADER_LEVEL=3 +MINDNET_NOTE_SCOPE_ZONE_HEADERS=Smart Edges,Relationen,Global Links,Note-Level Relations,Globale Verbindungen +MINDNET_NOTE_SCOPE_HEADER_LEVEL=2 +``` + +**Hinweis:** Falls diese Variablen nicht gesetzt sind, werden die Default-Werte verwendet. Das System funktioniert ohne explizite Konfiguration. + +**2. LLM-Profil prüfen:** + +Stellen Sie sicher, dass das `ingest_validator` Profil in `config/llm_profiles.yaml` existiert: + +```yaml +ingest_validator: + provider: ollama + model: phi3:mini + temperature: 0.0 + fallback_profile: null +``` + +**3. Prompt prüfen:** + +Stellen Sie sicher, dass der `edge_validation` Prompt in `config/prompts.yaml` existiert. + +**4. System neu starten:** + +Nach dem Hinzufügen der ENV-Variablen: + +```bash +systemctl restart mindnet-prod +systemctl restart mindnet-ui-prod +``` + +### Für Entwickler + +**Keine Code-Änderungen erforderlich!** Die neuen Features sind vollständig rückwärtskompatibel. + +**Optionale Integration:** + +* **Phase 3 Validierung:** Nutzen Sie `validate_edge_candidate()` aus `ingestion_validation.py` +* **Note-Scope Zonen:** Nutzen Sie `extract_note_scope_zones()` aus `graph_derive_edges.py` +* **Spiegelkanten:** Werden automatisch erzeugt, keine manuelle Integration erforderlich + +--- + +## 🚀 Deployment-Anweisungen + +### Pre-Deployment Checkliste + +- [ ] **Backup:** Vollständiges Backup von Qdrant und Vault durchführen +- [ ] **ENV-Variablen:** Neue ENV-Variablen zu `.env` hinzufügen (optional) +- [ ] **LLM-Profil:** `ingest_validator` Profil in `llm_profiles.yaml` prüfen +- [ ] **Prompt:** `edge_validation` Prompt in `prompts.yaml` prüfen +- [ ] **Dependencies:** `requirements.txt` aktualisieren (falls neue Abhängigkeiten) +- [ ] **Tests:** Unit Tests und Integration Tests ausführen + +### Deployment-Schritte + +**1. Code aktualisieren:** + +```bash +git pull origin main +# oder +git checkout WP24c +git merge main +``` + +**2. Dependencies aktualisieren:** + +```bash +source .venv/bin/activate +pip install -r requirements.txt +``` + +**3. ENV-Variablen konfigurieren (optional):** + +```bash +# Fügen Sie die neuen Variablen zu .env hinzu +nano .env +# oder +nano config/prod.env +``` + +**4. Services neu starten:** + +```bash +systemctl restart mindnet-prod +systemctl restart mindnet-ui-prod +``` + +**5. Health Check:** + +```bash +curl http://localhost:8001/healthz +curl http://localhost:8501/healthz +``` + +**6. Logs prüfen:** + +```bash +journalctl -u mindnet-prod -n 50 --no-pager +journalctl -u mindnet-ui-prod -n 50 --no-pager +``` + +### Post-Deployment Validierung + +**1. Phase 3 Validierung testen:** + +Erstellen Sie eine Test-Notiz mit `### Unzugeordnete Kanten`: + +```markdown +--- +type: concept +title: Test-Notiz +--- + +# Test-Notiz + +Hier ist der Inhalt... + +### Unzugeordnete Kanten + +related_to:Test-Ziel +``` + +**Erwartetes Verhalten:** +* Log zeigt `🚀 [PHASE 3] Validierung: ...` +* Log zeigt `✅ [PHASE 3] VERIFIED:` oder `🚫 [PHASE 3] REJECTED:` +* Kante wird nur bei VERIFIED persistiert + +**2. Note-Scope Zonen testen:** + +Erstellen Sie eine Test-Notiz mit `## Smart Edges`: + +```markdown +--- +type: decision +title: Test-Entscheidung +--- + +# Test-Entscheidung + +Hier ist der Inhalt... + +## Smart Edges + +[[rel:depends_on|Test-Projekt]] +``` + +**Erwartetes Verhalten:** +* Link wird als `scope: note` behandelt +* `provenance: explicit:note_zone` +* Höchste Priorität bei Duplikaten + +**3. Automatische Spiegelkanten testen:** + +Erstellen Sie eine explizite Kante: + +```markdown +[[rel:depends_on Projekt Alpha]] +``` + +**Erwartetes Verhalten:** +* Log zeigt `🔄 [SYMMETRY] Add inverse: ...` +* Beide Richtungen sind durchsuchbar +* Explizite Kante hat höhere Priorität + +--- + +## 🐛 Bekannte Probleme & Einschränkungen + +**Keine bekannten Probleme.** + +**Hinweise:** + +* **Phase 3 Validierung:** Erfordert LLM-Verfügbarkeit. Bei transienten Fehlern wird die Kante erlaubt (Datenintegrität vor Präzision). +* **Spiegelkanten:** Werden nur für explizite Kanten erzeugt. Validierte Kanten erhalten keine Spiegelkanten, bis sie VERIFIED sind. +* **Note-Scope:** Header-Namen müssen exakt (case-insensitive) übereinstimmen. + +--- + +## 📚 Dokumentation + +**Aktualisierte Dokumente:** + +* `docs/01_User_Manual/01_knowledge_design.md` - Automatische Spiegelkanten, Phase 3 Validierung, Note-Scope Zonen +* `docs/01_User_Manual/NOTE_SCOPE_ZONEN.md` - Phase 3 Validierung integriert +* `docs/01_User_Manual/LLM_VALIDIERUNG_VON_LINKS.md` - Phase 3 statt global_pool +* `docs/02_concepts/02_concept_graph_logic.md` - Phase 3 Validierung, automatische Spiegelkanten, Note-Scope vs. Chunk-Scope +* `docs/03_Technical_References/03_tech_data_model.md` - candidate: Präfix, verified Status, virtual Flag +* `docs/03_Technical_References/03_tech_configuration.md` - Neue ENV-Variablen dokumentiert +* `docs/04_Operations/04_admin_operations.md` - Troubleshooting für Phase 3 Validierung +* `docs/05_Development/05_testing_guide.md` - WP-24c Test-Szenarien + +**Neue Dokumente:** + +* Keine neuen Dokumente (alle Features in bestehenden Dokumenten integriert) + +--- + +## ✅ Breaking Changes + +**Keine Breaking Changes!** + +Das System ist vollständig rückwärtskompatibel. Bestehende Notizen funktionieren ohne Änderungen. + +--- + +## 🎉 Danksagungen + +Diese Version wurde entwickelt, um die Graph-Integrität zu sichern und die Benutzerfreundlichkeit durch automatische Spiegelkanten zu verbessern. + +--- + +**Status:** ✅ WP-24c ist zu 100% implementiert und audit-geprüft. +**Nächster Schritt:** WP-25c (Kontext-Budgeting & Erweiterte Prompt-Optimierung). diff --git a/docs/README.md b/docs/README.md index f90b8f6..a8e1ab9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,13 +2,13 @@ doc_type: documentation_index audience: all status: active -version: 3.1.1 -context: "Zentraler Einstiegspunkt für die Mindnet-Dokumentation" +version: 4.5.8 +context: "Zentraler Einstiegspunkt für die Mindnet-Dokumentation. Inkludiert WP-24c Phase 3 Agentic Edge Validation, automatische Spiegelkanten und Chunk-Aware Multigraph-System." --- # Mindnet Dokumentation -Willkommen in der Dokumentation von Mindnet v3.1.1! Diese Dokumentation hilft dir dabei, das System zu verstehen, zu nutzen und weiterzuentwickeln. +Willkommen in der Dokumentation von Mindnet v4.5.8! Diese Dokumentation hilft dir dabei, das System zu verstehen, zu nutzen und weiterzuentwickeln. ## 🚀 Schnellstart @@ -98,6 +98,10 @@ Historische Dokumentation: | Frage | Dokument | |-------|----------| | Wie starte ich mit Mindnet? | [Schnellstart](00_General/00_quickstart.md) | +| Wie verknüpfe ich Notizen? | [Knowledge Design - Edges](01_User_Manual/01_knowledge_design.md#4-edges--verlinkung) | +| Was sind automatische Spiegelkanten? | [Knowledge Design - Spiegelkanten](01_User_Manual/01_knowledge_design.md#43-automatische-spiegelkanten-invers-logik---wp-24c-v458) | +| Was ist Phase 3 Validierung? | [Knowledge Design - Phase 3](01_User_Manual/01_knowledge_design.md#44-explizite-vs-validierte-kanten-phase-3-validierung---wp-24c-v458) | +| Was sind Note-Scope Zonen? | [Note-Scope Zonen](01_User_Manual/NOTE_SCOPE_ZONEN.md) | | Wie nutze ich den Chat? | [Chat Usage Guide](01_User_Manual/01_chat_usage_guide.md) | | Wie strukturiere ich meine Notizen? | [Knowledge Design](01_User_Manual/01_knowledge_design.md) | | Wie schreibe ich für den Digitalen Zwilling? | [Authoring Guidelines](01_User_Manual/01_authoring_guidelines.md) | @@ -150,5 +154,5 @@ Falls du Verbesserungsvorschläge für die Dokumentation hast oder Fehler findes --- **Letzte Aktualisierung:** 2025-01-XX -**Version:** 2.9.1 +**Version:** 4.5.8 (WP-24c: Phase 3 Agentic Edge Validation - Integrity Baseline) 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/import_markdown.py b/scripts/import_markdown.py index ebf4914..b29098f 100644 --- a/scripts/import_markdown.py +++ b/scripts/import_markdown.py @@ -2,190 +2,264 @@ # -*- coding: utf-8 -*- """ FILE: scripts/import_markdown.py -VERSION: 2.4.1 (2025-12-15) +VERSION: 2.6.2 (WP-24c: Gold-Standard v4.1.0) STATUS: Active (Core) -COMPATIBILITY: v2.9.1 (Post-WP14/WP-15b) +COMPATIBILITY: IngestionProcessor v4.0.0+, graph_utils v4.1.0+ Zweck: ------- -Hauptwerkzeug zum Importieren von Markdown-Dateien aus einem Vault in Qdrant. -Implementiert den Two-Pass Workflow (WP-15b) für robuste Edge-Validierung. +Hauptwerkzeug zum Importieren von Markdown-Dateien aus einem lokalen Obsidian-Vault in die +Qdrant Vektor-Datenbank. Das Script ist darauf optimiert, die strukturelle Integrität des +Wissensgraphen zu wahren und die manuelle Nutzer-Autorität vor automatisierten System-Eingriffen +zu schützen. -Funktionsweise: ---------------- -1. PASS 1: Global Pre-Scan - - Scannt alle Markdown-Dateien im Vault - - Extrahiert Note-Kontext (ID, Titel, Dateiname) - - Füllt LocalBatchCache für semantische Edge-Validierung - - Indiziert nach ID, Titel und Dateiname für Link-Auflösung +Hintergrund der 2-Phasen-Schreibstrategie (Authority-First): +------------------------------------------------------------ +Um das Problem der "Ghost-IDs" (Links auf Titel statt IDs) und der asynchronen Überschreibungen +(Symmetrien löschen manuelle Kanten) zu lösen, implementiert dieses Script eine strikte +Trennung der Arbeitsabläufe: -2. PASS 2: Semantic Processing - - Verarbeitet Dateien in Batches (20 Dateien, max. 5 parallel) - - Nutzt gefüllten Cache für binäre Edge-Validierung - - Erzeugt Notes, Chunks und Edges in Qdrant - - Respektiert Hash-basierte Change Detection +1. PASS 1: Global Context Discovery (Pre-Scan) + - Scannt den gesamten Vault, um ein Mapping von Titeln/Dateinamen zu Note-IDs aufzubauen. + - Dieser Cache wird dem IngestionService übergeben, damit Wikilinks wie [[Klaus]] + während der Verarbeitung sofort in die korrekte Zeitstempel-ID (z.B. 202601031726-klaus) + aufgelöst werden können. + - Dies verhindert die Erzeugung falscher UUIDs durch unaufgelöste Bezeichnungen. + +2. PHASE 1: Authority Processing (Schreib-Durchlauf) + - Alle validen Dateien werden in Batches verarbeitet. + - Notizen, Chunks und explizite (vom Nutzer manuell gesetzte) Kanten werden sofort geschrieben. + - Durch die Verwendung von 'wait=True' in der Datenbank-Layer (qdrant_points) wird + sichergestellt, dass diese Informationen physisch indiziert sind, bevor Phase 2 startet. + - Symmetrische Gegenkanten werden während dieser Phase lediglich im Speicher gepuffert. + +3. PHASE 2: Global Symmetry Commitment (Integritäts-Sicherung) + - Erst nach Abschluss aller Batches wird die Methode commit_vault_symmetries() aufgerufen. + - Diese prüft die gepufferten Symmetrie-Vorschläge gegen die bereits existierende + Nutzer-Autorität in der Datenbank. + - Dank der in graph_utils v1.6.2 zentralisierten ID-Logik (_mk_edge_id) erkennt das + System Kollisionen hunderprozentig: Existiert bereits eine manuelle Kante für dieselbe + Verbindung, wird die automatische Symmetrie unterdrückt. + +Detaillierte Funktionsweise: +---------------------------- +- Ordner-Filter: Schließt System-Ordner wie .trash, .obsidian, .sync sowie Vorlagen konsequent aus. +- Cloud-Resilienz: Implementiert Semaphoren zur Begrenzung paralleler API-Zugriffe (max. 5). +- Mixture of Experts (MoE): Nutzt LLM-Validierung zur intelligenten Zuweisung von Kanten. +- Change Detection: Vergleicht Hashes, um redundante Schreibvorgänge zu vermeiden. Ergebnis-Interpretation: ------------------------ -- Log-Ausgabe: Fortschritt und Statistiken -- Stats: processed, skipped, errors -- Exit-Code 0: Erfolgreich (auch wenn einzelne Dateien Fehler haben) -- Ohne --apply: Dry-Run (keine DB-Änderungen) +- Log-Ausgabe: Zeigt detailliert den Fortschritt, LLM-Entscheidungen (✅ OK / ❌ SKIP) + und den Status der Symmetrie-Injektion. +- Statistiken: Gibt am Ende eine Zusammenfassung über Erfolg, Übersprungene (Hash identisch) + und Fehler (z.B. fehlendes Frontmatter). Verwendung: ----------- -- Regelmäßiger Import nach Vault-Änderungen -- Initial-Import eines neuen Vaults -- Re-Indexierung mit --force - -Hinweise: ---------- -- Two-Pass Workflow sorgt für robuste Edge-Validierung -- Change Detection verhindert unnötige Re-Indexierung -- Parallele Verarbeitung für Performance (max. 5 gleichzeitig) -- Cloud-Resilienz durch Semaphore-Limits - -Aufruf: -------- -python3 -m scripts.import_markdown --vault ./vault --apply -python3 -m scripts.import_markdown --vault ./vault --prefix mindnet_dev --force --apply - -Parameter: ----------- ---vault PATH Pfad zum Vault-Verzeichnis (Default: ./vault) ---prefix TEXT Collection-Präfix (Default: ENV COLLECTION_PREFIX oder mindnet) ---force Erzwingt Re-Indexierung aller Dateien (ignoriert Hashes) ---apply Führt tatsächliche DB-Schreibvorgänge durch (sonst Dry-Run) - -Änderungen: ------------ -v2.4.1 (2025-12-15): WP-15b Two-Pass Workflow - - Implementiert Pre-Scan für LocalBatchCache - - Indizierung nach ID, Titel und Dateiname - - Batch-Verarbeitung mit Semaphore-Limits -v2.0.0: Initial Release +- Initialer Aufbau: python3 -m scripts.import_markdown --vault /pfad/zum/vault --apply +- Update-Lauf: Das Script erkennt Änderungen automatisch via Change Detection. +- Erzwingung: Mit --force wird die Hash-Prüfung ignoriert und alles neu indiziert. """ + import asyncio import os import argparse import logging +import sys from pathlib import Path +from typing import List, Dict, Any from dotenv import load_dotenv -# Setzt das Level global auf INFO, damit der Fortschritt im Log sichtbar ist -logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s') +# WP-24c v4.5.9: Lade .env VOR dem Logging-Setup, damit DEBUG=true korrekt gelesen wird +load_dotenv() -# Importiere den neuen Async Service und stelle Python-Pfad sicher -import sys +# Root Logger Setup: Nutzt zentrale setup_logging() Funktion +# WP-24c v4.4.0-DEBUG: Aktiviert DEBUG-Level für End-to-End Tracing +# Kann auch über Umgebungsvariable DEBUG=true gesteuert werden +from app.core.logging_setup import setup_logging + +# Bestimme Log-Level basierend auf DEBUG Umgebungsvariable (nach load_dotenv!) +debug_mode = os.getenv("DEBUG", "false").lower() == "true" +log_level = logging.DEBUG if debug_mode else logging.INFO + +# Nutze zentrale Logging-Konfiguration (File + Console) +setup_logging(log_level=log_level) + +# Sicherstellung, dass das Root-Verzeichnis im Python-Pfad liegt sys.path.append(os.getcwd()) +# App-spezifische Imports from app.core.ingestion import IngestionService from app.core.parser import pre_scan_markdown logger = logging.getLogger("importer") async def main_async(args): + """ + Haupt-Workflow der Ingestion. Koordiniert die zwei Durchläufe (Pass 1/2) + und die zwei Schreibphasen (Phase 1/2). + """ vault_path = Path(args.vault).resolve() if not vault_path.exists(): - logger.error(f"Vault path does not exist: {vault_path}") + logger.error(f"Vault-Pfad existiert nicht: {vault_path}") return - # 1. Service initialisieren + # 1. Initialisierung des zentralen Ingestion-Services + # Nutzt IngestionProcessor v3.4.2 (initialisiert Registry mit .env Pfaden) logger.info(f"Initializing IngestionService (Prefix: {args.prefix})") service = IngestionService(collection_prefix=args.prefix) logger.info(f"Scanning {vault_path}...") - files = list(vault_path.rglob("*.md")) - # Exclude .obsidian folder if present - files = [f for f in files if ".obsidian" not in str(f)] - files.sort() + all_files_raw = list(vault_path.rglob("*.md")) - logger.info(f"Found {len(files)} markdown files.") + # --- GLOBALER ORDNER-FILTER --- + # Diese Liste stellt sicher, dass keine System-Leichen oder temporäre Dateien + # den Graphen korrumpieren oder zu ID-Kollisionen führen. + files = [] + + # WP-24c v4.1.0: MINDNET_IGNORE_FOLDERS aus Umgebungsvariable + # Format: Komma-separierte Liste von Ordnernamen (z.B. "trash,temp,archive") + env_ignore = os.getenv("MINDNET_IGNORE_FOLDERS", "") + env_ignore_list = [f.strip() for f in env_ignore.split(",") if f.strip()] if env_ignore else [] + + # Standard-Ignore-Liste (System-Ordner) + default_ignore_list = [".trash", ".obsidian", ".sync", "templates", "_system", ".git"] + + # Kombinierte Ignore-Liste (Umgebungsvariable hat Priorität, wird mit Defaults kombiniert) + ignore_list = list(set(default_ignore_list + env_ignore_list)) + + logger.info(f"📁 Ignore-Liste: {ignore_list}") + + for f in all_files_raw: + f_str = str(f) + # Filtert Ordner aus der ignore_list und versteckte Verzeichnisse + if not any(folder in f_str for folder in ignore_list) and not "/." in f_str: + files.append(f) + + files.sort() + logger.info(f"Found {len(files)} relevant markdown files (filtered trash/system/hidden).") # ========================================================================= - # PASS 1: Global Pre-Scan (WP-15b Harvester) - # Füllt den LocalBatchCache für die semantische Kanten-Validierung. - # Nutzt ID, Titel und Filename für robusten Look-up. + # PASS 1: Global Pre-Scan + # Ziel: Aufbau eines vollständigen Mappings von Bezeichnungen zu stabilen IDs. + # WICHTIG: Dies ist die Voraussetzung für die korrekte ID-Generierung in Phase 1. # ========================================================================= - logger.info(f"🔍 [Pass 1] Pre-scanning {len(files)} files for global context cache...") + logger.info(f"🔍 [Pass 1] Global Pre-Scan: Building context cache for {len(files)} files...") for f_path in files: try: - ctx = pre_scan_markdown(str(f_path)) + # Extrahiert Frontmatter und Metadaten ohne DB-Last + # Nutzt service.registry zur Typ-Auflösung + ctx = pre_scan_markdown(str(f_path), registry=service.registry) if ctx: - # 1. Look-up via Note ID (UUID oder Frontmatter ID) + # Mehrfache Indizierung für maximale Trefferrate bei Wikilinks service.batch_cache[ctx.note_id] = ctx - - # 2. Look-up via Titel (Wichtig für Wikilinks [[Titel]]) service.batch_cache[ctx.title] = ctx - - # 3. Look-up via Dateiname (Wichtig für Wikilinks [[Filename]]) - fname = os.path.splitext(f_path.name)[0] - service.batch_cache[fname] = ctx - + # Auch den Dateinamen ohne Endung als Alias hinterlegen + service.batch_cache[os.path.splitext(f_path.name)[0]] = ctx except Exception as e: - logger.warning(f"⚠️ Could not pre-scan {f_path.name}: {e}") - - logger.info(f"✅ Context Cache populated for {len(files)} notes.") + logger.warning(f"⚠️ Pre-scan fehlgeschlagen für {f_path.name}: {e}") # ========================================================================= - # PASS 2: Processing (Semantic Batch-Verarbeitung) - # Nutzt den gefüllten Cache zur binären Validierung semantischer Kanten. + # PHASE 1: Authority Processing (Batch-Lauf) + # Ziel: Verarbeitung der Dateiinhalte und Speicherung der Nutzer-Autorität. # ========================================================================= stats = {"processed": 0, "skipped": 0, "errors": 0} - sem = asyncio.Semaphore(5) # Max 5 parallele Dateien für Cloud-Stabilität + # Semaphore begrenzt die Parallelität zum Schutz der lokalen oder Cloud-API + sem = asyncio.Semaphore(5) async def process_with_limit(f_path): + """Kapselt den Prozess-Aufruf mit Ressourcen-Limitierung.""" async with sem: try: - # Nutzt den nun gefüllten Batch-Cache in der process_file Logik - res = await service.process_file( + # Verwendet process_file (v3.4.2), das explizite Kanten sofort schreibt. + # Symmetrien werden im Service-Puffer gesammelt und NICHT sofort geschrieben. + return await service.process_file( file_path=str(f_path), vault_root=str(vault_path), force_replace=args.force, apply=args.apply, purge_before=True ) - return res except Exception as e: return {"status": "error", "error": str(e), "path": str(f_path)} - logger.info(f"🚀 [Pass 2] Starting semantic processing in batches...") + logger.info(f"🚀 [Phase 1] Starting semantic processing in batches...") batch_size = 20 for i in range(0, len(files), batch_size): batch = files[i:i+batch_size] - logger.info(f"Processing batch {i} to {i+len(batch)}...") + logger.info(f"--- Processing Batch {i//batch_size + 1} ({len(batch)} files) ---") + # Parallelisierung innerhalb des Batches (begrenzt durch sem) tasks = [process_with_limit(f) for f in batch] results = await asyncio.gather(*tasks) for res in results: - if res.get("status") == "success": - stats["processed"] += 1 - elif res.get("status") == "error": + # Robuste Auswertung der Rückgabe-Dictionaries + if not isinstance(res, dict): stats["errors"] += 1 - logger.error(f"Error in {res.get('path')}: {res.get('error')}") + continue + + status = res.get("status") + if status == "success": + stats["processed"] += 1 + elif status == "error": + stats["errors"] += 1 + logger.error(f"❌ Fehler in {res.get('path')}: {res.get('error')}") + elif status == "unchanged": + stats["skipped"] += 1 else: stats["skipped"] += 1 - logger.info(f"Done. Stats: {stats}") - if not args.apply: - logger.info("DRY RUN. Use --apply to write to DB.") + # ========================================================================= + # PHASE 2: Global Symmetry Commitment + # Ziel: Finale Integrität. Triggert erst, wenn Phase 1 komplett indiziert ist. + # Verwendet die identische ID-Logik aus graph_utils v1.6.2. + # ========================================================================= + if args.apply: + logger.info(f"🔄 [Phase 2] Starting global symmetry injection for the entire vault...") + try: + # Diese Methode prüft den Puffer gegen die nun vollständige Datenbank. + # Verhindert Duplikate bei der 'Steinzeitaxt' durch Authority-Lookup. + sym_res = await service.commit_vault_symmetries() + if sym_res.get("status") == "success": + logger.info(f"✅ Phase 2 abgeschlossen. Hinzugefügt: {sym_res.get('added', 0)} geschützte Symmetrien.") + else: + logger.info(f"⏭️ Phase 2 übersprungen: {sym_res.get('reason', 'Keine Daten oder bereits vorhanden')}") + except Exception as e: + logger.error(f"❌ Fehler in Phase 2: {e}") + else: + logger.info("⏭️ [Phase 2] Dry-Run: Keine Symmetrie-Injektion durchgeführt.") + + logger.info(f"--- Import beendet ---") + logger.info(f"Statistiken: {stats}") def main(): - load_dotenv() + """Einstiegspunkt und Argument-Parsing.""" + # WP-24c v4.5.9: load_dotenv() wurde bereits beim Modul-Import aufgerufen + # (oben, vor dem Logging-Setup, damit DEBUG=true korrekt gelesen wird) + + # Standard-Präfix aus Umgebungsvariable oder Fallback default_prefix = os.getenv("COLLECTION_PREFIX", "mindnet") + # Optionaler Vault-Root aus .env + default_vault = os.getenv("MINDNET_VAULT_ROOT", "./vault") - parser = argparse.ArgumentParser(description="Two-Pass Markdown Ingestion for Mindnet") - parser.add_argument("--vault", default="./vault", help="Path to vault root") - parser.add_argument("--prefix", default=default_prefix, help="Collection prefix") - parser.add_argument("--force", action="store_true", help="Force re-index all files") - parser.add_argument("--apply", action="store_true", help="Perform writes to Qdrant") + parser = argparse.ArgumentParser(description="Mindnet Ingester: Two-Phase Markdown Import") + parser.add_argument("--vault", default=default_vault, help="Pfad zum Obsidian Vault") + parser.add_argument("--prefix", default=default_prefix, help="Qdrant Collection Präfix") + parser.add_argument("--force", action="store_true", help="Erzwingt Neu-Indizierung aller Dateien") + parser.add_argument("--apply", action="store_true", help="Schreibt physisch in die Datenbank") args = parser.parse_args() - # Starte den asynchronen Haupt-Loop - asyncio.run(main_async(args)) + try: + asyncio.run(main_async(args)) + except KeyboardInterrupt: + logger.info("Import durch Nutzer abgebrochen.") + except Exception as e: + logger.critical(f"FATALER FEHLER: {e}", exc_info=True) + sys.exit(1) if __name__ == "__main__": main() \ No newline at end of file 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}"