WP24c - Agentic Edge Validation & Chunk-Aware Multigraph-System (v4.5.8) #22
237
ANALYSE_TYPES_YAML_ZUGRIFFE.md
Normal file
237
ANALYSE_TYPES_YAML_ZUGRIFFE.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -13,6 +13,8 @@ class RawBlock:
|
||||||
level: Optional[int]
|
level: Optional[int]
|
||||||
section_path: str
|
section_path: str
|
||||||
section_title: Optional[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
|
@dataclass
|
||||||
class Chunk:
|
class Chunk:
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,12 @@ FILE: app/core/chunking/chunking_parser.py
|
||||||
DESCRIPTION: Zerlegt Markdown in logische Einheiten (RawBlocks).
|
DESCRIPTION: Zerlegt Markdown in logische Einheiten (RawBlocks).
|
||||||
Hält alle Überschriftenebenen (H1-H6) im Stream.
|
Hält alle Überschriftenebenen (H1-H6) im Stream.
|
||||||
Stellt die Funktion parse_edges_robust zur Verfügung.
|
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
|
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_models import RawBlock
|
||||||
from .chunking_utils import extract_frontmatter_from_text
|
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()]
|
return [p.strip() for p in _SENT_SPLIT.split(text) if p.strip()]
|
||||||
|
|
||||||
def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]:
|
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 = []
|
blocks = []
|
||||||
h1_title = "Dokument"
|
h1_title = "Dokument"
|
||||||
section_path = "/"
|
section_path = "/"
|
||||||
|
|
@ -29,6 +36,31 @@ def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]:
|
||||||
# Frontmatter entfernen
|
# Frontmatter entfernen
|
||||||
fm, text_without_fm = extract_frontmatter_from_text(md_text)
|
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 für Note-Titel extrahieren (Metadaten-Zweck)
|
||||||
h1_match = re.search(r'^#\s+(.*)', text_without_fm, re.MULTILINE)
|
h1_match = re.search(r'^#\s+(.*)', text_without_fm, re.MULTILINE)
|
||||||
if h1_match:
|
if h1_match:
|
||||||
|
|
@ -37,9 +69,61 @@ def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]:
|
||||||
lines = text_without_fm.split('\n')
|
lines = text_without_fm.split('\n')
|
||||||
buffer = []
|
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()
|
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-Erkennung (H1 bis H6)
|
||||||
heading_match = re.match(r'^(#{1,6})\s+(.*)', stripped)
|
heading_match = re.match(r'^(#{1,6})\s+(.*)', stripped)
|
||||||
if heading_match:
|
if heading_match:
|
||||||
|
|
@ -47,20 +131,47 @@ def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]:
|
||||||
if buffer:
|
if buffer:
|
||||||
content = "\n".join(buffer).strip()
|
content = "\n".join(buffer).strip()
|
||||||
if content:
|
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 = []
|
buffer = []
|
||||||
|
|
||||||
level = len(heading_match.group(1))
|
level = len(heading_match.group(1))
|
||||||
title = heading_match.group(2).strip()
|
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
|
# Pfad- und Titel-Update für die Metadaten der folgenden Blöcke
|
||||||
if level == 1:
|
if level == 1:
|
||||||
current_section_title = title; section_path = "/"
|
current_section_title = title; section_path = "/"
|
||||||
elif level == 2:
|
elif level == 2:
|
||||||
current_section_title = title; section_path = f"/{current_section_title}"
|
current_section_title = title; section_path = f"/{current_section_title}"
|
||||||
|
|
||||||
# Die Überschrift selbst als regulären Block hinzufügen
|
# 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))
|
blocks.append(RawBlock(
|
||||||
|
"heading", stripped, level, section_path, current_section_title,
|
||||||
|
exclude_from_chunking=in_exclusion_zone
|
||||||
|
))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Trenner (---) oder Leerzeilen beenden Blöcke, außer innerhalb von Callouts
|
# 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:
|
if buffer:
|
||||||
content = "\n".join(buffer).strip()
|
content = "\n".join(buffer).strip()
|
||||||
if content:
|
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 = []
|
buffer = []
|
||||||
if stripped == "---":
|
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:
|
else:
|
||||||
buffer.append(line)
|
buffer.append(line)
|
||||||
|
|
||||||
if buffer:
|
if buffer:
|
||||||
content = "\n".join(buffer).strip()
|
content = "\n".join(buffer).strip()
|
||||||
if content:
|
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
|
return blocks, h1_title
|
||||||
|
|
||||||
def parse_edges_robust(text: str) -> Set[str]:
|
def parse_edges_robust(text: str) -> List[Dict[str, Any]]:
|
||||||
"""Extrahiert Kanten-Kandidaten aus Wikilinks und Callouts."""
|
"""
|
||||||
found_edges = set()
|
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]]
|
# 1. Wikilinks [[rel:kind|target]]
|
||||||
inlines = re.findall(r'\[\[rel:([^\|\]]+)\|?([^\]]*)\]\]', text)
|
inlines = re.findall(r'\[\[rel:([^\|\]]+)\|?([^\]]*)\]\]', text)
|
||||||
for kind, target in inlines:
|
for kind, target in inlines:
|
||||||
k = kind.strip().lower()
|
k = kind.strip().lower()
|
||||||
t = target.strip()
|
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
|
# 2. Callout Edges > [!edge] kind
|
||||||
lines = text.split('\n')
|
lines = text.split('\n')
|
||||||
current_edge_type = None
|
current_edge_type = None
|
||||||
for line in lines:
|
for line in lines:
|
||||||
stripped = line.strip()
|
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:
|
if callout_match:
|
||||||
current_edge_type = callout_match.group(1).strip().lower()
|
current_edge_type = callout_match.group(1).strip().lower()
|
||||||
# Links in der gleichen Zeile des Callouts
|
# Links in der gleichen Zeile des Callouts
|
||||||
links = re.findall(r'\[\[([^\]]+)\]\]', stripped)
|
links = re.findall(r'\[\[([^\]]+)\]\]', stripped)
|
||||||
for l in links:
|
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
|
continue
|
||||||
# Links in Folgezeilen des Callouts
|
# 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('>'):
|
if current_edge_type and stripped.startswith('>'):
|
||||||
|
# Fortsetzung des Callout-Blocks: Links extrahieren
|
||||||
links = re.findall(r'\[\[([^\]]+)\]\]', stripped)
|
links = re.findall(r'\[\[([^\]]+)\]\]', stripped)
|
||||||
for l in links:
|
for l in links:
|
||||||
if "rel:" not in l: found_edges.add(f"{current_edge_type}:{l}")
|
if "rel:" not in l:
|
||||||
elif not stripped.startswith('>'):
|
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
|
current_edge_type = None
|
||||||
|
# Leerzeilen werden ignoriert - current_edge_type bleibt erhalten
|
||||||
return found_edges
|
return found_edges
|
||||||
|
|
@ -6,9 +6,21 @@ DESCRIPTION: Der zentrale Orchestrator für das Chunking-System.
|
||||||
- Integriert physikalische Kanten-Injektion (Propagierung).
|
- Integriert physikalische Kanten-Injektion (Propagierung).
|
||||||
- Stellt H1-Kontext-Fenster sicher.
|
- Stellt H1-Kontext-Fenster sicher.
|
||||||
- Baut den Candidate-Pool für die WP-15b Ingestion auf.
|
- 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 asyncio
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional
|
||||||
from .chunking_models import Chunk
|
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.
|
Hauptfunktion zur Zerlegung einer Note.
|
||||||
Verbindet Strategien mit physikalischer Kontext-Anreicherung.
|
Verbindet Strategien mit physikalischer Kontext-Anreicherung.
|
||||||
|
WP-24c v4.2.5: Frontmatter-Override für chunking_profile wird berücksichtigt.
|
||||||
"""
|
"""
|
||||||
# 1. Konfiguration & Parsing
|
# 1. WP-24c v4.2.5: Frontmatter VOR Konfiguration extrahieren (für Override)
|
||||||
if config is None:
|
|
||||||
config = get_chunk_config(note_type)
|
|
||||||
|
|
||||||
fm, body_text = extract_frontmatter_from_text(md_text)
|
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)
|
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)
|
# Vorbereitung des H1-Präfix für die Embedding-Fenster (Breadcrumbs)
|
||||||
h1_prefix = f"# {doc_title}" if doc_title else ""
|
h1_prefix = f"# {doc_title}" if doc_title else ""
|
||||||
|
|
||||||
# 2. Anwendung der Splitting-Strategie
|
# 2. Anwendung der Splitting-Strategie
|
||||||
# Alle Strategien nutzen nun einheitlich context_prefix für die Window-Bildung.
|
# 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":
|
if config.get("strategy") == "by_heading":
|
||||||
chunks = await asyncio.to_thread(
|
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:
|
else:
|
||||||
chunks = await asyncio.to_thread(
|
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:
|
if not chunks:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# 3. Physikalische Kontext-Anreicherung (Der Qualitäts-Fix)
|
# 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.
|
# Schreibt Kanten aus Callouts/Inlines hart in den Text für Qdrant.
|
||||||
chunks = propagate_section_edges(chunks)
|
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.
|
# 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.
|
# Wir extrahieren aus dem bereits (durch Propagation) angereicherten Text.
|
||||||
# ch.candidate_pool wird im Modell-Konstruktor als leere Liste initialisiert.
|
# ch.candidate_pool wird im Modell-Konstruktor als leere Liste initialisiert.
|
||||||
for e_str in parse_edges_robust(ch.text):
|
for edge_info in parse_edges_robust(ch.text):
|
||||||
parts = e_str.split(':', 1)
|
edge_str = edge_info["edge"]
|
||||||
|
is_callout = edge_info.get("is_callout", False)
|
||||||
|
parts = edge_str.split(':', 1)
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
k, t = parts
|
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)
|
# 6. Global Pool (Unzugeordnete Kanten - kann mitten im Dokument oder am Ende stehen)
|
||||||
# Sucht nach dem Edge-Pool Block im Original-Markdown.
|
# WP-24c v4.2.0: Konfigurierbare Header-Namen und -Ebene via .env
|
||||||
pool_match = re.search(
|
# Sucht nach ALLEN Edge-Pool Blöcken im Original-Markdown (nicht nur am Ende).
|
||||||
r'###?\s*(?:Unzugeordnete Kanten|Edge Pool|Candidates)\s*\n(.*?)(?:\n#|$)',
|
llm_validation_headers = os.getenv(
|
||||||
body_text,
|
"MINDNET_LLM_VALIDATION_HEADERS",
|
||||||
re.DOTALL | re.IGNORECASE
|
"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))
|
global_edges = parse_edges_robust(pool_match.group(1))
|
||||||
for e_str in global_edges:
|
for edge_info in global_edges:
|
||||||
parts = e_str.split(':', 1)
|
edge_str = edge_info["edge"]
|
||||||
|
parts = edge_str.split(':', 1)
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
k, t = parts
|
k, t = parts
|
||||||
# Diese Kanten werden als "global_pool" markiert für die spätere KI-Prüfung.
|
# Diese Kanten werden als "global_pool" markiert für die spätere KI-Prüfung.
|
||||||
for ch in chunks:
|
for ch in chunks:
|
||||||
ch.candidate_pool.append({"kind": k, "to": t, "provenance": "global_pool"})
|
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:
|
for ch in chunks:
|
||||||
seen = set()
|
seen = set()
|
||||||
unique = []
|
unique = []
|
||||||
|
|
@ -92,6 +146,56 @@ async def assemble_chunks(note_id: str, md_text: str, note_type: str, config: Op
|
||||||
unique.append(c)
|
unique.append(c)
|
||||||
ch.candidate_pool = unique
|
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
|
# Verknüpfung der Nachbarschaften für Graph-Traversierung
|
||||||
for i, ch in enumerate(chunks):
|
for i, ch in enumerate(chunks):
|
||||||
ch.neighbors_prev = chunks[i-1].id if i > 0 else None
|
ch.neighbors_prev = chunks[i-1].id if i > 0 else None
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,13 @@ def propagate_section_edges(chunks: List[Chunk]) -> List[Chunk]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Nutzt den robusten Parser aus dem Package
|
# Nutzt den robusten Parser aus dem Package
|
||||||
edges = parse_edges_robust(ch.text)
|
# WP-24c v4.2.7: parse_edges_robust gibt jetzt Liste von Dicts zurück
|
||||||
if edges:
|
edge_infos = parse_edges_robust(ch.text)
|
||||||
|
if edge_infos:
|
||||||
if ch.section_path not in section_map:
|
if ch.section_path not in section_map:
|
||||||
section_map[ch.section_path] = set()
|
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)
|
# 2. Injizieren: Kanten in jeden Chunk der Sektion zurückschreiben (Broadcasting)
|
||||||
for ch in chunks:
|
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,
|
# Vorhandene Kanten (Typ:Ziel) in DIESEM Chunk ermitteln,
|
||||||
# um Dopplungen (z.B. durch Callouts) zu vermeiden.
|
# 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 = []
|
injections = []
|
||||||
# Sortierung für deterministische Ergebnisse
|
# Sortierung für deterministische Ergebnisse
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ DESCRIPTION: Strategien für atomares Sektions-Chunking v3.9.9.
|
||||||
- Keine redundante Kanten-Injektion.
|
- Keine redundante Kanten-Injektion.
|
||||||
- Strikte Einhaltung von Sektionsgrenzen via Look-Ahead.
|
- Strikte Einhaltung von Sektionsgrenzen via Look-Ahead.
|
||||||
- Fix: Synchronisierung der Parameter mit dem Orchestrator (context_prefix).
|
- 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 typing import List, Dict, Any, Optional
|
||||||
from .chunking_models import RawBlock, Chunk
|
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["title"] = item["meta"].section_title
|
||||||
current_meta["path"] = item["meta"].section_path
|
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:
|
if is_hard_split_mode:
|
||||||
# Leere Überschriften (z.B. H1 direkt vor H2) verbleiben am nächsten Chunk
|
# WP-24c v4.2.5: Bei strict_heading_split: true wird nach JEDER Sektion geflasht
|
||||||
if item.get("is_empty", False) and queue:
|
# Kein Carry-Over erlaubt, auch nicht für leere Überschriften
|
||||||
current_chunk_text = (current_chunk_text + "\n\n" + item_text).strip()
|
if current_chunk_text:
|
||||||
continue
|
# Flashe vorherigen Chunk
|
||||||
|
|
||||||
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:
|
|
||||||
_emit(current_chunk_text, current_meta["title"], current_meta["path"])
|
_emit(current_chunk_text, current_meta["title"], current_meta["path"])
|
||||||
current_chunk_text = item_text
|
current_chunk_text = ""
|
||||||
else:
|
|
||||||
current_chunk_text = combined
|
# 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 = ""
|
current_chunk_text = ""
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import math
|
||||||
import yaml
|
import yaml
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, Tuple
|
from typing import Dict, Any, Tuple, Optional
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -27,12 +27,31 @@ def load_yaml_config() -> Dict[str, Any]:
|
||||||
return data
|
return data
|
||||||
except Exception: return {}
|
except Exception: return {}
|
||||||
|
|
||||||
def get_chunk_config(note_type: str) -> Dict[str, Any]:
|
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."""
|
"""
|
||||||
|
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()
|
full_config = load_yaml_config()
|
||||||
profiles = full_config.get("chunking_profiles", {})
|
profiles = full_config.get("chunking_profiles", {})
|
||||||
type_def = full_config.get("types", {}).get(note_type.lower(), {})
|
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()
|
config = profiles.get(profile_name, DEFAULT_PROFILE).copy()
|
||||||
if "overlap" in config and isinstance(config["overlap"], list):
|
if "overlap" in config and isinstance(config["overlap"], list):
|
||||||
config["overlap"] = tuple(config["overlap"])
|
config["overlap"] = tuple(config["overlap"])
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
"""
|
"""
|
||||||
FILE: app/core/database/qdrant_points.py
|
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.
|
DESCRIPTION: Object-Mapper für Qdrant. Konvertiert JSON-Payloads (Notes, Chunks, Edges)
|
||||||
VERSION: 1.5.1 (WP-Fix: Explicit Target Section Support)
|
in PointStructs und generiert deterministische UUIDs.
|
||||||
|
VERSION: 4.1.0 (WP-24c: Gold-Standard Identity v4.1.0 - target_section Support)
|
||||||
STATUS: Active
|
STATUS: Active
|
||||||
DEPENDENCIES: qdrant_client, uuid, os
|
DEPENDENCIES: qdrant_client, uuid, os, app.core.graph.graph_utils
|
||||||
LAST_ANALYSIS: 2025-12-29
|
LAST_ANALYSIS: 2026-01-10
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
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.http import models as rest
|
||||||
from qdrant_client import QdrantClient
|
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 ---------------------
|
# --------------------- ID helpers ---------------------
|
||||||
|
|
||||||
def _to_uuid(stable_key: str) -> str:
|
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]:
|
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"
|
return f"{prefix}_notes", f"{prefix}_chunks", f"{prefix}_edges"
|
||||||
|
|
||||||
# --------------------- Points builders ---------------------
|
# --------------------- Points builders ---------------------
|
||||||
|
|
||||||
def points_for_note(prefix: str, note_payload: dict, note_vec: List[float] | None, dim: int) -> Tuple[str, List[rest.PointStruct]]:
|
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)
|
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)
|
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"
|
raw_note_id = note_payload.get("note_id") or note_payload.get("id") or "missing-note-id"
|
||||||
point_id = _to_uuid(raw_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]
|
return notes_col, [pt]
|
||||||
|
|
||||||
def points_for_chunks(prefix: str, chunk_payloads: List[dict], vectors: List[List[float]]) -> Tuple[str, List[rest.PointStruct]]:
|
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)
|
_, chunks_col, _ = _names(prefix)
|
||||||
points: List[rest.PointStruct] = []
|
points: List[rest.PointStruct] = []
|
||||||
for i, (pl, vec) in enumerate(zip(chunk_payloads, vectors), start=1):
|
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"
|
note_id = pl.get("note_id") or pl.get("parent_note_id") or "missing-note"
|
||||||
chunk_id = f"{note_id}#{i}"
|
chunk_id = f"{note_id}#{i}"
|
||||||
pl["chunk_id"] = chunk_id
|
pl["chunk_id"] = chunk_id
|
||||||
|
|
||||||
point_id = _to_uuid(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
|
return chunks_col, points
|
||||||
|
|
||||||
def _normalize_edge_payload(pl: dict) -> dict:
|
def _normalize_edge_payload(pl: dict) -> dict:
|
||||||
|
|
@ -68,25 +93,61 @@ def _normalize_edge_payload(pl: dict) -> dict:
|
||||||
return pl
|
return pl
|
||||||
|
|
||||||
def points_for_edges(prefix: str, edge_payloads: List[dict]) -> Tuple[str, List[rest.PointStruct]]:
|
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)
|
_, _, edges_col = _names(prefix)
|
||||||
points: List[rest.PointStruct] = []
|
points: List[rest.PointStruct] = []
|
||||||
|
|
||||||
for raw in edge_payloads:
|
for raw in edge_payloads:
|
||||||
pl = _normalize_edge_payload(raw)
|
pl = _normalize_edge_payload(raw)
|
||||||
edge_id = pl.get("edge_id")
|
|
||||||
if not edge_id:
|
# Extraktion der Identitäts-Parameter (GOLD-STANDARD v4.1.0)
|
||||||
kind = pl.get("kind", "edge")
|
kind = pl.get("kind", "edge")
|
||||||
s = pl.get("source_id", "unknown-src")
|
s = pl.get("source_id", "unknown-src")
|
||||||
t = pl.get("target_id", "unknown-tgt")
|
t = pl.get("target_id", "unknown-tgt")
|
||||||
seq = pl.get("seq") or ""
|
scope = pl.get("scope", "note")
|
||||||
edge_id = f"{kind}:{s}->{t}#{seq}"
|
target_section = pl.get("target_section") # WP-24c v4.1.0: target_section für Section-Links
|
||||||
pl["edge_id"] = edge_id
|
|
||||||
point_id = _to_uuid(edge_id)
|
# Hinweis: rule_id und variant werden im Payload gespeichert,
|
||||||
points.append(rest.PointStruct(id=point_id, vector=[0.0], payload=pl))
|
# 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
|
return edges_col, points
|
||||||
|
|
||||||
# --------------------- Vector schema & overrides ---------------------
|
# --------------------- Vector schema & overrides ---------------------
|
||||||
|
|
||||||
def _preferred_name(candidates: List[str]) -> str:
|
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"):
|
for k in ("text", "default", "embedding", "content"):
|
||||||
if k in candidates:
|
if k in candidates:
|
||||||
return k
|
return k
|
||||||
|
|
@ -94,10 +155,11 @@ def _preferred_name(candidates: List[str]) -> str:
|
||||||
|
|
||||||
def _env_override_for_collection(collection: str) -> Optional[str]:
|
def _env_override_for_collection(collection: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
|
Prüft auf Umgebungsvariablen-Overrides für Vektor-Namen.
|
||||||
Returns:
|
Returns:
|
||||||
- "__single__" to force single-vector
|
- "__single__" für erzwungenen Single-Vector Modus
|
||||||
- concrete name (str) to force named-vector with that name
|
- Name (str) für spezifischen Named-Vector
|
||||||
- None to auto-detect
|
- None für automatische Erkennung
|
||||||
"""
|
"""
|
||||||
base = os.getenv("MINDNET_VECTOR_NAME")
|
base = os.getenv("MINDNET_VECTOR_NAME")
|
||||||
if collection.endswith("_notes"):
|
if collection.endswith("_notes"):
|
||||||
|
|
@ -112,19 +174,17 @@ def _env_override_for_collection(collection: str) -> Optional[str]:
|
||||||
val = base.strip()
|
val = base.strip()
|
||||||
if val.lower() in ("__single__", "single"):
|
if val.lower() in ("__single__", "single"):
|
||||||
return "__single__"
|
return "__single__"
|
||||||
return val # concrete name
|
return val
|
||||||
|
|
||||||
def _get_vector_schema(client: QdrantClient, collection_name: str) -> dict:
|
def _get_vector_schema(client: QdrantClient, collection_name: str) -> dict:
|
||||||
"""
|
"""Ermittelt das Vektor-Schema einer existierenden Collection via API."""
|
||||||
Return {"kind": "single", "size": int} or {"kind": "named", "names": [...], "primary": str}.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
info = client.get_collection(collection_name=collection_name)
|
info = client.get_collection(collection_name=collection_name)
|
||||||
vecs = getattr(info, "vectors", None)
|
vecs = getattr(info, "vectors", None)
|
||||||
# Single-vector config
|
# Prüfung auf Single-Vector Konfiguration
|
||||||
if hasattr(vecs, "size") and isinstance(vecs.size, int):
|
if hasattr(vecs, "size") and isinstance(vecs.size, int):
|
||||||
return {"kind": "single", "size": vecs.size}
|
return {"kind": "single", "size": vecs.size}
|
||||||
# Named-vectors config (dict-like in .config)
|
# Prüfung auf Named-Vectors Konfiguration
|
||||||
cfg = getattr(vecs, "config", None)
|
cfg = getattr(vecs, "config", None)
|
||||||
if isinstance(cfg, dict) and cfg:
|
if isinstance(cfg, dict) and cfg:
|
||||||
names = list(cfg.keys())
|
names = list(cfg.keys())
|
||||||
|
|
@ -135,6 +195,7 @@ def _get_vector_schema(client: QdrantClient, collection_name: str) -> dict:
|
||||||
return {"kind": "single", "size": None}
|
return {"kind": "single", "size": None}
|
||||||
|
|
||||||
def _as_named(points: List[rest.PointStruct], name: str) -> List[rest.PointStruct]:
|
def _as_named(points: List[rest.PointStruct], name: str) -> List[rest.PointStruct]:
|
||||||
|
"""Transformiert PointStructs in das Named-Vector Format."""
|
||||||
out: List[rest.PointStruct] = []
|
out: List[rest.PointStruct] = []
|
||||||
for pt in points:
|
for pt in points:
|
||||||
vec = getattr(pt, "vector", None)
|
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:
|
if name in vec:
|
||||||
out.append(pt)
|
out.append(pt)
|
||||||
else:
|
else:
|
||||||
# take any existing entry; if empty dict fallback to [0.0]
|
|
||||||
fallback_vec = None
|
fallback_vec = None
|
||||||
try:
|
try:
|
||||||
fallback_vec = list(next(iter(vec.values())))
|
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 ---------------------
|
# --------------------- 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:
|
if not points:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 1) ENV overrides come first
|
# 1) ENV overrides prüfen
|
||||||
override = _env_override_for_collection(collection)
|
override = _env_override_for_collection(collection)
|
||||||
if override == "__single__":
|
if override == "__single__":
|
||||||
client.upsert(collection_name=collection, points=points, wait=True)
|
client.upsert(collection_name=collection, points=points, wait=wait)
|
||||||
return
|
return
|
||||||
elif isinstance(override, str):
|
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
|
return
|
||||||
|
|
||||||
# 2) Auto-detect schema
|
# 2) Automatische Schema-Erkennung (Live-Check)
|
||||||
schema = _get_vector_schema(client, collection)
|
schema = _get_vector_schema(client, collection)
|
||||||
if schema.get("kind") == "named":
|
if schema.get("kind") == "named":
|
||||||
name = schema.get("primary") or _preferred_name(schema.get("names") or [])
|
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
|
return
|
||||||
|
|
||||||
# 3) Fallback single-vector
|
# 3) Fallback: Single-Vector Upsert
|
||||||
client.upsert(collection_name=collection, points=points, wait=True)
|
client.upsert(collection_name=collection, points=points, wait=wait)
|
||||||
|
|
||||||
# --- Optional search helpers ---
|
# --- Optional search helpers ---
|
||||||
|
|
||||||
def _filter_any(field: str, values: Iterable[str]) -> rest.Filter:
|
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])
|
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]:
|
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]
|
fs = [f for f in filters if f is not None]
|
||||||
if not fs:
|
if not fs:
|
||||||
return None
|
return None
|
||||||
|
|
@ -200,6 +267,7 @@ def _merge_filters(*filters: Optional[rest.Filter]) -> Optional[rest.Filter]:
|
||||||
return rest.Filter(must=must)
|
return rest.Filter(must=must)
|
||||||
|
|
||||||
def _filter_from_dict(filters: Optional[Dict[str, Any]]) -> Optional[rest.Filter]:
|
def _filter_from_dict(filters: Optional[Dict[str, Any]]) -> Optional[rest.Filter]:
|
||||||
|
"""Konvertiert ein Python-Dict in ein Qdrant-Filter Objekt."""
|
||||||
if not filters:
|
if not filters:
|
||||||
return None
|
return None
|
||||||
parts = []
|
parts = []
|
||||||
|
|
@ -211,9 +279,17 @@ def _filter_from_dict(filters: Optional[Dict[str, Any]]) -> Optional[rest.Filter
|
||||||
return _merge_filters(*parts)
|
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]]:
|
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)
|
_, chunks_col, _ = _names(prefix)
|
||||||
flt = _filter_from_dict(filters)
|
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]] = []
|
out: List[Tuple[str, float, dict]] = []
|
||||||
for r in res:
|
for r in res:
|
||||||
out.append((str(r.id), float(r.score), dict(r.payload or {})))
|
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,
|
edge_types: Optional[Iterable[str]] = None,
|
||||||
limit: int = 2048,
|
limit: int = 2048,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""Retrieve edge payloads from the <prefix>_edges collection.
|
"""Ruft alle Kanten ab, die von einer Menge von Quell-Notizen ausgehen."""
|
||||||
|
|
||||||
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,
|
|
||||||
...
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
source_ids = list(source_ids)
|
source_ids = list(source_ids)
|
||||||
if not source_ids or limit <= 0:
|
if not source_ids or limit <= 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Resolve collection name
|
# Namen der Edges-Collection auflösen
|
||||||
_, _, edges_col = _names(prefix)
|
_, _, 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])
|
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
|
kind_filter = None
|
||||||
if edge_types:
|
if edge_types:
|
||||||
kind_filter = _filter_any("kind", [str(k) for k in 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
|
next_page = None
|
||||||
remaining = int(limit)
|
remaining = int(limit)
|
||||||
|
|
||||||
# Use paginated scroll API; we don't need vectors, only payloads.
|
# Paginated Scroll API (NUR Payload, keine Vektoren)
|
||||||
while remaining > 0:
|
while remaining > 0:
|
||||||
batch_limit = min(256, remaining)
|
batch_limit = min(256, remaining)
|
||||||
res, next_page = client.scroll(
|
res, next_page = client.scroll(
|
||||||
|
|
@ -286,10 +339,6 @@ def get_edges_for_sources(
|
||||||
offset=next_page,
|
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:
|
if not res:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
"""
|
"""
|
||||||
FILE: app/core/graph/graph_db_adapter.py
|
FILE: app/core/graph/graph_db_adapter.py
|
||||||
DESCRIPTION: Datenbeschaffung aus Qdrant für den Graphen.
|
DESCRIPTION: Datenbeschaffung aus Qdrant für den Graphen.
|
||||||
AUDIT v1.1.1: Volle Unterstützung für WP-15c Metadaten.
|
AUDIT v1.2.0: Gold-Standard v4.1.0 - Scope-Awareness & Section-Filtering.
|
||||||
Stellt sicher, dass 'target_section' und 'provenance' für die
|
- Erweiterte Suche nach chunk_id-Edges für Scope-Awareness
|
||||||
Super-Edge-Aggregation im Retriever geladen werden.
|
- 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 typing import List, Dict, Optional
|
||||||
from qdrant_client import QdrantClient
|
from qdrant_client import QdrantClient
|
||||||
|
|
@ -17,11 +19,22 @@ def fetch_edges_from_qdrant(
|
||||||
prefix: str,
|
prefix: str,
|
||||||
seeds: List[str],
|
seeds: List[str],
|
||||||
edge_types: Optional[List[str]] = None,
|
edge_types: Optional[List[str]] = None,
|
||||||
|
target_section: Optional[str] = None,
|
||||||
|
chunk_ids: Optional[List[str]] = None,
|
||||||
limit: int = 2048,
|
limit: int = 2048,
|
||||||
) -> List[Dict]:
|
) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Holt Edges aus der Datenbank basierend auf Seed-IDs.
|
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:
|
if not seeds or limit <= 0:
|
||||||
return []
|
return []
|
||||||
|
|
@ -30,13 +43,21 @@ def fetch_edges_from_qdrant(
|
||||||
# Rückgabe: (notes_col, chunks_col, edges_col)
|
# Rückgabe: (notes_col, chunks_col, edges_col)
|
||||||
_, _, edges_col = collection_names(prefix)
|
_, _, 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 = []
|
seed_conditions = []
|
||||||
for field in ("source_id", "target_id", "note_id"):
|
for field in ("source_id", "target_id", "note_id"):
|
||||||
for s in seeds:
|
for s in seeds:
|
||||||
seed_conditions.append(
|
seed_conditions.append(
|
||||||
rest.FieldCondition(key=field, match=rest.MatchValue(value=str(s)))
|
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
|
seeds_filter = rest.Filter(should=seed_conditions) if seed_conditions else None
|
||||||
|
|
||||||
# Optionaler Filter auf spezifische Kanten-Typen (z.B. für Intent-Routing)
|
# 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)
|
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 = []
|
must = []
|
||||||
if seeds_filter:
|
if seeds_filter:
|
||||||
must.append(seeds_filter)
|
must.append(seeds_filter)
|
||||||
if type_filter:
|
if type_filter:
|
||||||
must.append(type_filter)
|
must.append(type_filter)
|
||||||
|
if section_filter:
|
||||||
|
must.append(section_filter)
|
||||||
|
|
||||||
flt = rest.Filter(must=must) if must else None
|
flt = rest.Filter(must=must) if must else None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,33 @@ FILE: app/core/graph/graph_derive_edges.py
|
||||||
DESCRIPTION: Hauptlogik zur Kanten-Aggregation und De-Duplizierung.
|
DESCRIPTION: Hauptlogik zur Kanten-Aggregation und De-Duplizierung.
|
||||||
WP-15b/c Audit:
|
WP-15b/c Audit:
|
||||||
- Präzises Sektions-Splitting via parse_link_target.
|
- 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.
|
- 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 (
|
from .graph_utils import (
|
||||||
_get, _edge, _mk_edge_id, _dedupe_seq, parse_link_target,
|
_get, _edge, _mk_edge_id, _dedupe_seq, parse_link_target,
|
||||||
PROVENANCE_PRIORITY, load_types_registry, get_edge_defaults_for
|
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
|
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(
|
def build_edges_for_note(
|
||||||
note_id: str,
|
note_id: str,
|
||||||
chunks: List[dict],
|
chunks: List[dict],
|
||||||
note_level_references: Optional[List[str]] = None,
|
note_level_references: Optional[List[str]] = None,
|
||||||
include_note_scope_refs: bool = False,
|
include_note_scope_refs: bool = False,
|
||||||
|
markdown_body: Optional[str] = None,
|
||||||
) -> List[dict]:
|
) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
Erzeugt und aggregiert alle Kanten für eine Note.
|
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] = []
|
edges: List[dict] = []
|
||||||
# note_type für die Ermittlung der edge_defaults (types.yaml)
|
# note_type für die Ermittlung der edge_defaults (types.yaml)
|
||||||
note_type = _get(chunks[0], "type") if chunks else "concept"
|
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)
|
# 1) Struktur-Kanten (Internal: belongs_to, next/prev)
|
||||||
# Diese erhalten die Provenienz 'structure' und sind in der Registry geschützt.
|
# 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
|
if not cid: continue
|
||||||
|
|
||||||
# Verbindung Chunk -> Note
|
# 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, {
|
edges.append(_edge("belongs_to", "chunk", cid, note_id, note_id, {
|
||||||
"chunk_id": cid,
|
"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",
|
"provenance": "structure",
|
||||||
"rule_id": "structure:belongs_to",
|
"rule_id": "structure:belongs_to",
|
||||||
"confidence": PROVENANCE_PRIORITY["structure:belongs_to"]
|
"confidence": PROVENANCE_PRIORITY["structure:belongs_to"]
|
||||||
|
|
@ -48,14 +635,14 @@ def build_edges_for_note(
|
||||||
if idx < len(chunks) - 1:
|
if idx < len(chunks) - 1:
|
||||||
next_id = _get(chunks[idx+1], "chunk_id", "id")
|
next_id = _get(chunks[idx+1], "chunk_id", "id")
|
||||||
if next_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, {
|
edges.append(_edge("next", "chunk", cid, next_id, note_id, {
|
||||||
"chunk_id": cid,
|
"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"]
|
"provenance": "structure", "rule_id": "structure:order", "confidence": PROVENANCE_PRIORITY["structure:order"]
|
||||||
}))
|
}))
|
||||||
edges.append(_edge("prev", "chunk", next_id, cid, note_id, {
|
edges.append(_edge("prev", "chunk", next_id, cid, note_id, {
|
||||||
"chunk_id": next_id,
|
"edge_id": _mk_edge_id("prev", next_id, cid, "chunk"),
|
||||||
"edge_id": _mk_edge_id("prev", next_id, cid, "chunk", "structure:order"),
|
|
||||||
"provenance": "structure", "rule_id": "structure:order", "confidence": PROVENANCE_PRIORITY["structure:order"]
|
"provenance": "structure", "rule_id": "structure:order", "confidence": PROVENANCE_PRIORITY["structure:order"]
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
@ -63,7 +650,58 @@ def build_edges_for_note(
|
||||||
reg = load_types_registry()
|
reg = load_types_registry()
|
||||||
defaults = get_edge_defaults_for(note_type, reg)
|
defaults = get_edge_defaults_for(note_type, reg)
|
||||||
refs_all: List[str] = []
|
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:
|
for ch in chunks:
|
||||||
cid = _get(ch, "chunk_id", "id")
|
cid = _get(ch, "chunk_id", "id")
|
||||||
if not cid: continue
|
if not cid: continue
|
||||||
|
|
@ -77,36 +715,67 @@ def build_edges_for_note(
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"chunk_id": cid,
|
"chunk_id": cid,
|
||||||
# WP-Fix: Variant=sec sorgt für eindeutige ID pro Sektion
|
# 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", "inline:rel", variant=sec),
|
"edge_id": _mk_edge_id(k, cid, t, "chunk", target_section=sec),
|
||||||
"provenance": "explicit", "rule_id": "inline:rel", "confidence": PROVENANCE_PRIORITY["inline:rel"]
|
"provenance": "explicit", "rule_id": "inline:rel", "confidence": PROVENANCE_PRIORITY["inline:rel"]
|
||||||
}
|
}
|
||||||
if sec: payload["target_section"] = sec
|
if sec: payload["target_section"] = sec
|
||||||
edges.append(_edge(k, "chunk", cid, t, note_id, payload))
|
edges.append(_edge(k, "chunk", cid, t, note_id, payload))
|
||||||
|
|
||||||
# B. Candidate Pool (WP-15b Validierte KI-Kanten)
|
# 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 []
|
pool = ch.get("candidate_pool") or ch.get("candidate_edges") or []
|
||||||
for cand in pool:
|
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)
|
t, sec = parse_link_target(raw_t, note_id)
|
||||||
if t:
|
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 = {
|
payload = {
|
||||||
"chunk_id": cid,
|
"chunk_id": cid,
|
||||||
"edge_id": _mk_edge_id(k, cid, t, "chunk", f"candidate:{p}", variant=sec),
|
"edge_id": _mk_edge_id(k, cid, t, "chunk", target_section=sec),
|
||||||
"provenance": p, "rule_id": f"candidate:{p}", "confidence": PROVENANCE_PRIORITY.get(p, 0.90)
|
"provenance": p, "rule_id": rule_id, "confidence": confidence
|
||||||
}
|
}
|
||||||
if sec: payload["target_section"] = sec
|
if sec: payload["target_section"] = sec
|
||||||
edges.append(_edge(k, "chunk", cid, t, note_id, payload))
|
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)
|
call_pairs, rem2 = extract_callout_relations(rem)
|
||||||
for k, raw_t in call_pairs:
|
for k, raw_t in call_pairs:
|
||||||
t, sec = parse_link_target(raw_t, note_id)
|
t, sec = parse_link_target(raw_t, note_id)
|
||||||
if not t: continue
|
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 = {
|
payload = {
|
||||||
"chunk_id": cid,
|
"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"]
|
"provenance": "explicit", "rule_id": "callout:edge", "confidence": PROVENANCE_PRIORITY["callout:edge"]
|
||||||
}
|
}
|
||||||
if sec: payload["target_section"] = sec
|
if sec: payload["target_section"] = sec
|
||||||
|
|
@ -118,9 +787,10 @@ def build_edges_for_note(
|
||||||
r, sec = parse_link_target(raw_r, note_id)
|
r, sec = parse_link_target(raw_r, note_id)
|
||||||
if not r: continue
|
if not r: continue
|
||||||
|
|
||||||
|
# WP-24c v4.1.0: target_section fließt nun fest in die ID-Generierung ein
|
||||||
payload = {
|
payload = {
|
||||||
"chunk_id": cid, "ref_text": raw_r,
|
"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"]
|
"provenance": "explicit", "rule_id": "explicit:wikilink", "confidence": PROVENANCE_PRIORITY["explicit:wikilink"]
|
||||||
}
|
}
|
||||||
if sec: payload["target_section"] = sec
|
if sec: payload["target_section"] = sec
|
||||||
|
|
@ -129,9 +799,10 @@ def build_edges_for_note(
|
||||||
# Automatische Kanten-Vererbung aus types.yaml
|
# Automatische Kanten-Vererbung aus types.yaml
|
||||||
for rel in defaults:
|
for rel in defaults:
|
||||||
if rel != "references":
|
if rel != "references":
|
||||||
|
# WP-24c v4.1.0: target_section fließt nun fest in die ID-Generierung ein
|
||||||
def_payload = {
|
def_payload = {
|
||||||
"chunk_id": cid,
|
"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"]
|
"provenance": "rule", "rule_id": f"edge_defaults:{rel}", "confidence": PROVENANCE_PRIORITY["edge_defaults"]
|
||||||
}
|
}
|
||||||
if sec: def_payload["target_section"] = sec
|
if sec: def_payload["target_section"] = sec
|
||||||
|
|
@ -146,24 +817,181 @@ def build_edges_for_note(
|
||||||
|
|
||||||
for r in refs_note:
|
for r in refs_note:
|
||||||
if not r: continue
|
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, {
|
edges.append(_edge("references", "note", note_id, r, note_id, {
|
||||||
"edge_id": _mk_edge_id("references", note_id, r, "note", "explicit:note_scope"),
|
"edge_id": _mk_edge_id("references", note_id, r, "note"),
|
||||||
"provenance": "explicit", "confidence": PROVENANCE_PRIORITY["explicit:note_scope"]
|
"provenance": "explicit", "rule_id": "explicit:note_scope", "confidence": PROVENANCE_PRIORITY["explicit:note_scope"]
|
||||||
}))
|
}))
|
||||||
# Backlinks zur Stärkung der Bidirektionalität
|
# Backlinks zur Stärkung der Bidirektionalität
|
||||||
edges.append(_edge("backlink", "note", r, note_id, note_id, {
|
edges.append(_edge("backlink", "note", r, note_id, note_id, {
|
||||||
"edge_id": _mk_edge_id("backlink", r, note_id, "note", "derived:backlink"),
|
"edge_id": _mk_edge_id("backlink", r, note_id, "note"),
|
||||||
"provenance": "rule", "confidence": PROVENANCE_PRIORITY["derived:backlink"]
|
"provenance": "rule", "rule_id": "derived:backlink", "confidence": PROVENANCE_PRIORITY["derived:backlink"]
|
||||||
}))
|
}))
|
||||||
|
|
||||||
# 4) De-Duplizierung (In-Place)
|
# 4) WP-24c v4.2.0: Note-Scope Edges hinzufügen (VOR De-Duplizierung)
|
||||||
# Da die EDGE-ID nun die Sektion (variant) enthält, bleiben Links auf
|
# WP-24c v4.2.0: Note-Scope Edges hinzufügen
|
||||||
# unterschiedliche Abschnitte derselben Note erhalten.
|
edges.extend(note_scope_edges)
|
||||||
unique_map: Dict[str, dict] = {}
|
# 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:
|
for e in edges:
|
||||||
eid = e["edge_id"]
|
kind = e.get("kind", "related_to")
|
||||||
# Höhere Confidence gewinnt bei identischer ID
|
source_id = e.get("source_id", "")
|
||||||
if eid not in unique_map or e.get("confidence", 0) > unique_map[eid].get("confidence", 0):
|
target_id = e.get("target_id", "")
|
||||||
unique_map[eid] = e
|
target_section = e.get("target_section")
|
||||||
|
scope = e.get("scope", "chunk")
|
||||||
return list(unique_map.values())
|
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
|
||||||
|
|
@ -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.
|
Zentrale Komponente für die Graph-Expansion (BFS) und Bonus-Berechnung.
|
||||||
WP-15c Update: Erhalt von Metadaten (target_section, provenance)
|
WP-15c Update: Erhalt von Metadaten (target_section, provenance)
|
||||||
für präzises Retrieval-Reasoning.
|
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
|
STATUS: Active
|
||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
|
|
@ -28,6 +29,8 @@ class Subgraph:
|
||||||
self.reverse_adj: DefaultDict[str, List[Dict]] = defaultdict(list)
|
self.reverse_adj: DefaultDict[str, List[Dict]] = defaultdict(list)
|
||||||
self.in_degree: DefaultDict[str, int] = defaultdict(int)
|
self.in_degree: DefaultDict[str, int] = defaultdict(int)
|
||||||
self.out_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:
|
def add_edge(self, e: Dict) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -48,7 +51,9 @@ class Subgraph:
|
||||||
"provenance": e.get("provenance", "rule"),
|
"provenance": e.get("provenance", "rule"),
|
||||||
"confidence": e.get("confidence", 1.0),
|
"confidence": e.get("confidence", 1.0),
|
||||||
"target_section": e.get("target_section"), # Essentiell für Präzision
|
"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")
|
owner = e.get("note_id")
|
||||||
|
|
@ -111,10 +116,21 @@ def expand(
|
||||||
seeds: List[str],
|
seeds: List[str],
|
||||||
depth: int = 1,
|
depth: int = 1,
|
||||||
edge_types: Optional[List[str]] = None,
|
edge_types: Optional[List[str]] = None,
|
||||||
|
chunk_ids: Optional[List[str]] = None,
|
||||||
|
target_section: Optional[str] = None,
|
||||||
) -> Subgraph:
|
) -> Subgraph:
|
||||||
"""
|
"""
|
||||||
Expandiert ab Seeds entlang von Edges bis zu einer bestimmten Tiefe.
|
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()
|
sg = Subgraph()
|
||||||
frontier = set(seeds)
|
frontier = set(seeds)
|
||||||
|
|
@ -124,8 +140,13 @@ def expand(
|
||||||
if not frontier:
|
if not frontier:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Batch-Abfrage der Kanten für die aktuelle Ebene
|
# 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)
|
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()
|
next_frontier: Set[str] = set()
|
||||||
|
|
||||||
for pl in payloads:
|
for pl in payloads:
|
||||||
|
|
@ -133,6 +154,7 @@ def expand(
|
||||||
if not src or not tgt: continue
|
if not src or not tgt: continue
|
||||||
|
|
||||||
# WP-15c: Wir übergeben das vollständige Payload an add_edge
|
# 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 = {
|
edge_payload = {
|
||||||
"source": src,
|
"source": src,
|
||||||
"target": tgt,
|
"target": tgt,
|
||||||
|
|
@ -141,7 +163,9 @@ def expand(
|
||||||
"note_id": pl.get("note_id"),
|
"note_id": pl.get("note_id"),
|
||||||
"provenance": pl.get("provenance", "rule"),
|
"provenance": pl.get("provenance", "rule"),
|
||||||
"confidence": pl.get("confidence", 1.0),
|
"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)
|
sg.add_edge(edge_payload)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
"""
|
"""
|
||||||
FILE: app/core/graph/graph_utils.py
|
FILE: app/core/graph/graph_utils.py
|
||||||
DESCRIPTION: Basale Werkzeuge, ID-Generierung und Provenance-Konfiguration für den Graphen.
|
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 os
|
||||||
|
import uuid
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import Iterable, List, Optional, Set, Any, Tuple
|
from typing import Iterable, List, Optional, Set, Any, Tuple
|
||||||
|
|
||||||
|
|
@ -12,70 +19,61 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
yaml = None
|
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 = {
|
PROVENANCE_PRIORITY = {
|
||||||
"explicit:wikilink": 1.00,
|
"explicit:wikilink": 1.00,
|
||||||
"inline:rel": 0.95,
|
"inline:rel": 0.95,
|
||||||
"callout:edge": 0.90,
|
"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
|
"semantic_ai": 0.90, # Validierte KI-Kanten
|
||||||
"structure:belongs_to": 1.00,
|
"structure:belongs_to": 1.00,
|
||||||
"structure:order": 0.95, # next/prev
|
"structure:order": 0.95, # next/prev
|
||||||
"explicit:note_scope": 1.00,
|
"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,
|
"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):
|
def _get(d: dict, *keys, default=None):
|
||||||
"""Sicherer Zugriff auf verschachtelte Keys."""
|
"""Sicherer Zugriff auf tief verschachtelte Dictionary-Keys."""
|
||||||
for k in keys:
|
for k in keys:
|
||||||
if isinstance(d, dict) and k in d and d[k] is not None:
|
if isinstance(d, dict) and k in d and d[k] is not None:
|
||||||
return d[k]
|
return d[k]
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def _dedupe_seq(seq: Iterable[str]) -> List[str]:
|
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()
|
seen: Set[str] = set()
|
||||||
out: List[str] = []
|
out: List[str] = []
|
||||||
for s in seq:
|
for s in seq:
|
||||||
if s not in seen:
|
if s not in seen:
|
||||||
seen.add(s); out.append(s)
|
seen.add(s)
|
||||||
|
out.append(s)
|
||||||
return out
|
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]]:
|
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.
|
Trennt einen Obsidian-Link [[Target#Section]] in seine Bestandteile Target und Section.
|
||||||
Behandelt Self-Links ('#Section'), indem current_note_id eingesetzt wird.
|
Behandelt Self-Links (z.B. [[#Ziele]]), indem die aktuelle note_id eingesetzt wird.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(target_id, target_section)
|
Tuple (target_id, target_section)
|
||||||
"""
|
"""
|
||||||
if not raw:
|
if not raw:
|
||||||
return "", None
|
return "", None
|
||||||
|
|
@ -84,29 +82,96 @@ def parse_link_target(raw: str, current_note_id: Optional[str] = None) -> Tuple[
|
||||||
target = parts[0].strip()
|
target = parts[0].strip()
|
||||||
section = parts[1].strip() if len(parts) > 1 else None
|
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:
|
if not target and section and current_note_id:
|
||||||
target = current_note_id
|
target = current_note_id
|
||||||
|
|
||||||
return target, section
|
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:
|
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")
|
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:
|
try:
|
||||||
with open(p, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {}
|
with open(p, "r", encoding="utf-8") as f:
|
||||||
except Exception: return {}
|
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]:
|
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 {}
|
types_map = reg.get("types", reg) if isinstance(reg, dict) else {}
|
||||||
if note_type and isinstance(types_map, dict):
|
if note_type and isinstance(types_map, dict):
|
||||||
t = types_map.get(note_type)
|
t_cfg = types_map.get(note_type)
|
||||||
if isinstance(t, dict) and isinstance(t.get("edge_defaults"), list):
|
if isinstance(t_cfg, dict) and isinstance(t_cfg.get("edge_defaults"), list):
|
||||||
return [str(x) for x in t["edge_defaults"] if isinstance(x, str)]
|
return [str(x) for x in t_cfg["edge_defaults"]]
|
||||||
|
|
||||||
|
# Fallback auf globale Defaults
|
||||||
for key in ("defaults", "default", "global"):
|
for key in ("defaults", "default", "global"):
|
||||||
v = reg.get(key)
|
v = reg.get(key)
|
||||||
if isinstance(v, dict) and isinstance(v.get("edge_defaults"), list):
|
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 [str(x) for x in v["edge_defaults"] if isinstance(x, str)]
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
@ -2,15 +2,19 @@
|
||||||
FILE: app/core/ingestion/ingestion_chunk_payload.py
|
FILE: app/core/ingestion/ingestion_chunk_payload.py
|
||||||
DESCRIPTION: Baut das JSON-Objekt für 'mindnet_chunks'.
|
DESCRIPTION: Baut das JSON-Objekt für 'mindnet_chunks'.
|
||||||
Fix v2.4.3: Integration der zentralen Registry (WP-14) für konsistente Defaults.
|
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
|
STATUS: Active
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
# ENTSCHEIDENDER FIX: Import der neutralen Registry-Logik zur Vermeidung von Circular Imports
|
# ENTSCHEIDENDER FIX: Import der neutralen Registry-Logik zur Vermeidung von Circular Imports
|
||||||
from app.core.registry import load_type_registry
|
from app.core.registry import load_type_registry
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Resolution Helpers (Audited)
|
# 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")
|
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")
|
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", "")
|
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] = {
|
pl: Dict[str, Any] = {
|
||||||
"note_id": nid or fm.get("id"),
|
"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,
|
"path": note_path,
|
||||||
"source_path": kwargs.get("file_path") or note_path,
|
"source_path": kwargs.get("file_path") or note_path,
|
||||||
"retriever_weight": rw,
|
"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)
|
# Audit: Cleanup Pop (Vermeidung von redundanten Alias-Feldern)
|
||||||
for alias in ("chunk_num", "Chunk_Number"):
|
for alias in ("chunk_num", "Chunk_Number"):
|
||||||
pl.pop(alias, None)
|
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)
|
out.append(pl)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,38 +2,115 @@
|
||||||
FILE: app/core/ingestion/ingestion_db.py
|
FILE: app/core/ingestion/ingestion_db.py
|
||||||
DESCRIPTION: Datenbank-Schnittstelle für Note-Metadaten und Artefakt-Prüfung.
|
DESCRIPTION: Datenbank-Schnittstelle für Note-Metadaten und Artefakt-Prüfung.
|
||||||
WP-14: Umstellung auf zentrale database-Infrastruktur.
|
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 import QdrantClient
|
||||||
from qdrant_client.http import models as rest
|
from qdrant_client.http import models as rest
|
||||||
|
|
||||||
# Import der modularisierten Namen-Logik zur Sicherstellung der Konsistenz
|
# Import der modularisierten Namen-Logik zur Sicherstellung der Konsistenz
|
||||||
from app.core.database import collection_names
|
from app.core.database import collection_names
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def fetch_note_payload(client: QdrantClient, prefix: str, note_id: str) -> Optional[dict]:
|
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)
|
notes_col, _, _ = collection_names(prefix)
|
||||||
try:
|
try:
|
||||||
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
|
f = rest.Filter(must=[
|
||||||
pts, _ = client.scroll(collection_name=notes_col, scroll_filter=f, limit=1, with_payload=True)
|
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
|
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]:
|
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)
|
_, chunks_col, edges_col = collection_names(prefix)
|
||||||
try:
|
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)
|
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)
|
e_pts, _ = client.scroll(collection_name=edges_col, scroll_filter=f, limit=1)
|
||||||
return (not bool(c_pts)), (not bool(e_pts))
|
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):
|
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)
|
_, chunks_col, edges_col = collection_names(prefix)
|
||||||
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
|
try:
|
||||||
# Iteration über die nun zentral verwalteten Collection-Namen
|
f = rest.Filter(must=[
|
||||||
for col in [chunks_col, edges_col]:
|
rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))
|
||||||
try: client.delete(collection_name=col, points_selector=rest.FilterSelector(filter=f))
|
])
|
||||||
except: pass
|
# 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}")
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
"""
|
"""
|
||||||
FILE: app/core/ingestion/ingestion_note_payload.py
|
FILE: app/core/ingestion/ingestion_note_payload.py
|
||||||
DESCRIPTION: Baut das JSON-Objekt für mindnet_notes.
|
DESCRIPTION: Baut das JSON-Objekt für mindnet_notes.
|
||||||
FEATURES:
|
WP-14: Integration der zentralen Registry.
|
||||||
- Multi-Hash (body/full) für flexible Change Detection.
|
WP-24c: Dynamische Ermittlung von edge_defaults aus dem Graph-Schema.
|
||||||
- Fix v2.4.5: Präzise Hash-Logik für Profil-Änderungen.
|
VERSION: 2.5.0 (WP-24c: Dynamic Topology Integration)
|
||||||
- Integration der zentralen Registry (WP-14).
|
STATUS: Active
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any, Dict, Tuple, Optional
|
from typing import Any, Dict, Tuple, Optional
|
||||||
|
|
@ -15,6 +15,8 @@ import hashlib
|
||||||
|
|
||||||
# Import der zentralen Registry-Logik
|
# Import der zentralen Registry-Logik
|
||||||
from app.core.registry import load_type_registry
|
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
|
# Helper
|
||||||
|
|
@ -46,15 +48,14 @@ def _compute_hash(content: str) -> str:
|
||||||
def _get_hash_source_content(n: Dict[str, Any], mode: str) -> str:
|
def _get_hash_source_content(n: Dict[str, Any], mode: str) -> str:
|
||||||
"""
|
"""
|
||||||
Generiert den Hash-Input-String basierend auf Body oder Metadaten.
|
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()
|
body = str(n.get("body") or "").strip()
|
||||||
if mode == "body": return body
|
if mode == "body": return body
|
||||||
if mode == "full":
|
if mode == "full":
|
||||||
fm = n.get("frontmatter") or {}
|
fm = n.get("frontmatter") or {}
|
||||||
meta_parts = []
|
meta_parts = []
|
||||||
# Wir inkludieren alle Felder, die das Chunking oder Retrieval beeinflussen
|
# Alle Felder, die das Chunking oder Retrieval beeinflussen
|
||||||
# Jede Änderung hier führt nun zwingend zu einem neuen Full-Hash
|
|
||||||
keys = [
|
keys = [
|
||||||
"title", "type", "status", "tags",
|
"title", "type", "status", "tags",
|
||||||
"chunking_profile", "chunk_profile",
|
"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]:
|
def make_note_payload(note: Any, *args, **kwargs) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Baut das Note-Payload inklusive Multi-Hash und Audit-Validierung.
|
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)
|
n = _as_dict(note)
|
||||||
|
|
||||||
|
|
@ -120,10 +121,16 @@ def make_note_payload(note: Any, *args, **kwargs) -> Dict[str, Any]:
|
||||||
if chunk_profile is None:
|
if chunk_profile is None:
|
||||||
chunk_profile = ingest_cfg.get("default_chunk_profile", cfg_def.get("chunking_profile", "sliding_standard"))
|
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")
|
edge_defaults = fm.get("edge_defaults")
|
||||||
|
|
||||||
|
# 2. Priorität: Dynamische Abfrage der 'Typical Edges' aus dem Graph-Schema
|
||||||
if edge_defaults is None:
|
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)
|
edge_defaults = _ensure_list(edge_defaults)
|
||||||
|
|
||||||
# --- Basis-Metadaten ---
|
# --- Basis-Metadaten ---
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,19 @@ DESCRIPTION: Der zentrale IngestionService (Orchestrator).
|
||||||
WP-25a: Integration der Mixture of Experts (MoE) Architektur.
|
WP-25a: Integration der Mixture of Experts (MoE) Architektur.
|
||||||
WP-15b: Two-Pass Workflow mit globalem Kontext-Cache.
|
WP-15b: Two-Pass Workflow mit globalem Kontext-Cache.
|
||||||
WP-20/22: Cloud-Resilienz und Content-Lifecycle integriert.
|
WP-20/22: Cloud-Resilienz und Content-Lifecycle integriert.
|
||||||
AUDIT v2.14.0: Synchronisierung der Profil-Auflösung mit MoE-Experten.
|
AUDIT v4.2.4:
|
||||||
VERSION: 2.14.0 (WP-25a: MoE & Profile Support)
|
- 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
|
STATUS: Active
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from typing import Dict, List, Optional, Tuple, Any
|
from typing import Dict, List, Optional, Tuple, Any
|
||||||
|
|
||||||
# Core Module Imports
|
# Core Module Imports
|
||||||
|
|
@ -19,10 +25,13 @@ from app.core.parser import (
|
||||||
validate_required_frontmatter, NoteContext
|
validate_required_frontmatter, NoteContext
|
||||||
)
|
)
|
||||||
from app.core.chunking import assemble_chunks
|
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 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 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
|
# Services
|
||||||
from app.services.embeddings_client import EmbeddingsClient
|
from app.services.embeddings_client import EmbeddingsClient
|
||||||
|
|
@ -31,7 +40,7 @@ from app.services.llm_service import LLMService
|
||||||
|
|
||||||
# Package-Interne Imports (Refactoring WP-14)
|
# Package-Interne Imports (Refactoring WP-14)
|
||||||
from .ingestion_utils import load_type_registry, resolve_note_type, get_chunk_config_by_profile
|
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_validation import validate_edge_candidate
|
||||||
from .ingestion_note_payload import make_note_payload
|
from .ingestion_note_payload import make_note_payload
|
||||||
from .ingestion_chunk_payload import make_chunk_payloads
|
from .ingestion_chunk_payload import make_chunk_payloads
|
||||||
|
|
@ -50,9 +59,13 @@ class IngestionService:
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
self.settings = 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.prefix = collection_prefix or self.settings.COLLECTION_PREFIX
|
||||||
self.cfg = QdrantConfig.from_env()
|
self.cfg = QdrantConfig.from_env()
|
||||||
# Synchronisierung der Konfiguration mit dem Instanz-Präfix
|
|
||||||
self.cfg.prefix = self.prefix
|
self.cfg.prefix = self.prefix
|
||||||
self.client = get_client(self.cfg)
|
self.client = get_client(self.cfg)
|
||||||
|
|
||||||
|
|
@ -64,182 +77,576 @@ class IngestionService:
|
||||||
embed_cfg = self.llm.profiles.get("embedding_expert", {})
|
embed_cfg = self.llm.profiles.get("embedding_expert", {})
|
||||||
self.dim = embed_cfg.get("dimensions") or self.settings.VECTOR_SIZE
|
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.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:
|
try:
|
||||||
# Aufruf der modularisierten Schema-Logik
|
|
||||||
ensure_collections(self.client, self.prefix, self.dim)
|
ensure_collections(self.client, self.prefix, self.dim)
|
||||||
ensure_payload_indexes(self.client, self.prefix)
|
ensure_payload_indexes(self.client, self.prefix)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"DB initialization warning: {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.
|
WP-24c v4.5.10: Loggt ID-Kollisionen in eine dedizierte Log-Datei.
|
||||||
Pass 1: Pre-Scan füllt den Context-Cache (3-Wege-Indexierung).
|
|
||||||
Pass 2: Verarbeitung nutzt den Cache für die semantische Prüfung.
|
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:
|
for path in file_paths:
|
||||||
try:
|
try:
|
||||||
# Übergabe der Registry für dynamische Scan-Tiefe
|
|
||||||
ctx = pre_scan_markdown(path, registry=self.registry)
|
ctx = pre_scan_markdown(path, registry=self.registry)
|
||||||
if ctx:
|
if ctx:
|
||||||
# Mehrfache Indizierung für robusten Look-up (ID, Titel, Dateiname)
|
|
||||||
self.batch_cache[ctx.note_id] = ctx
|
self.batch_cache[ctx.note_id] = ctx
|
||||||
self.batch_cache[ctx.title] = ctx
|
self.batch_cache[ctx.title] = ctx
|
||||||
fname = os.path.splitext(os.path.basename(path))[0]
|
self.batch_cache[os.path.splitext(os.path.basename(path))[0]] = ctx
|
||||||
self.batch_cache[fname] = ctx
|
|
||||||
except Exception as e:
|
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...")
|
# 2. Schritt: Batch Processing (Authority Only)
|
||||||
return [await self.process_file(p, vault_root, apply=True, purge_before=True) for p in file_paths]
|
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]:
|
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)
|
apply = kwargs.get("apply", False)
|
||||||
force_replace = kwargs.get("force_replace", False)
|
force_replace = kwargs.get("force_replace", False)
|
||||||
purge_before = kwargs.get("purge_before", 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}
|
result = {"path": file_path, "status": "skipped", "changed": False, "error": None}
|
||||||
|
|
||||||
# 1. Parse & Lifecycle Gate
|
|
||||||
try:
|
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"}
|
if not parsed: return {**result, "error": "Empty file"}
|
||||||
fm = normalize_frontmatter(parsed.frontmatter)
|
fm = normalize_frontmatter(parsed.frontmatter)
|
||||||
validate_required_frontmatter(fm)
|
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")
|
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)
|
chunk_cfg = get_chunk_config_by_profile(self.registry, profile, note_type)
|
||||||
enable_smart = chunk_cfg.get("enable_smart_edge_allocation", False)
|
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.
|
# WP-24c v4.5.8: Validierung in Chunk-Schleife entfernt
|
||||||
chunks = await assemble_chunks(note_id, body_text, note_type, config=chunk_cfg)
|
# 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
|
||||||
# Semantische Kanten-Validierung (Smart Edge Allocation via MoE-Profil)
|
# 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:
|
for ch in chunks:
|
||||||
filtered = []
|
new_pool = []
|
||||||
for cand in getattr(ch, "candidate_pool", []):
|
for cand in getattr(ch, "candidate_pool", []):
|
||||||
# WP-25a: Nutzt nun das spezialisierte Validierungs-Profil
|
# WP-24c v4.5.8: Nur ID-Validierung (Ghost-ID Schutz)
|
||||||
if cand.get("provenance") == "global_pool" and enable_smart:
|
t_id = cand.get('target_id') or cand.get('to') or cand.get('note_id')
|
||||||
if await validate_edge_candidate(ch.text, cand, self.batch_cache, self.llm, profile_name="ingest_validator"):
|
if not self._is_valid_id(t_id):
|
||||||
filtered.append(cand)
|
continue
|
||||||
else:
|
# WP-24c v4.5.8: Alle Kanten gehen durch - LLM-Validierung erfolgt in Phase 3
|
||||||
# Explizite Kanten (Wikilinks/Callouts) werden ungeprüft übernommen
|
new_pool.append(cand)
|
||||||
filtered.append(cand)
|
ch.candidate_pool = new_pool
|
||||||
ch.candidate_pool = filtered
|
|
||||||
|
|
||||||
# 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(
|
# v4.2.8 Fix C: Explizite Übergabe des Profil-Namens für den Chunk-Payload
|
||||||
fm, note_pl["path"], chunks, file_path=file_path,
|
chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, file_path=file_path, types_cfg=self.registry, chunk_profile=profile)
|
||||||
types_cfg=self.registry
|
|
||||||
)
|
|
||||||
|
|
||||||
# Vektorisierung der Fenster-Texte
|
|
||||||
vecs = await self.embedder.embed_documents([c.get("window") or "" for c in chunk_pls]) if chunk_pls else []
|
vecs = await self.embedder.embed_documents([c.get("window") or "" for c in chunk_pls]) if chunk_pls else []
|
||||||
|
|
||||||
# Aggregation aller finalen Kanten (Edges)
|
# WP-24c v4.2.0: Kanten-Extraktion mit Note-Scope Zonen Support
|
||||||
edges = build_edges_for_note(
|
# Übergabe des Original-Markdown-Texts für Note-Scope Zonen-Extraktion
|
||||||
note_id, chunk_pls,
|
markdown_body = getattr(parsed, "body", "")
|
||||||
|
raw_edges = build_edges_for_note(
|
||||||
|
note_id,
|
||||||
|
chunk_pls,
|
||||||
note_level_references=note_pl.get("references", []),
|
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
|
# WP-24c v4.5.8: Phase 3 - Finaler Validierungs-Gate für candidate: Kanten
|
||||||
for e in edges:
|
# Prüfe alle Kanten mit rule_id ODER provenance beginnend mit "candidate:"
|
||||||
e["kind"] = edge_registry.resolve(
|
# Dies schließt alle Kandidaten ein, unabhängig von ihrer Herkunft (global_pool, explicit:callout, etc.)
|
||||||
e.get("kind", "related_to"),
|
|
||||||
provenance=e.get("provenance", "explicit"),
|
# WP-24c v4.5.8: Kontext-Optimierung für Note-Scope Kanten
|
||||||
context={"file": file_path, "note_id": note_id, "line": e.get("line", "system")}
|
# 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
|
# DB Upsert
|
||||||
if purge_before and old_payload:
|
if purge_before and old_payload: purge_artifacts(self.client, self.prefix, note_id)
|
||||||
purge_artifacts(self.client, self.prefix, note_id)
|
|
||||||
|
|
||||||
# Speichern der Haupt-Note
|
col_n, pts_n = points_for_note(self.prefix, note_pl, None, self.dim)
|
||||||
n_name, n_pts = points_for_note(self.prefix, note_pl, None, self.dim)
|
upsert_batch(self.client, col_n, pts_n, wait=True)
|
||||||
upsert_batch(self.client, n_name, n_pts)
|
|
||||||
|
|
||||||
# Speichern der Chunks
|
|
||||||
if chunk_pls and vecs:
|
if chunk_pls and vecs:
|
||||||
c_pts = points_for_chunks(self.prefix, chunk_pls, vecs)[1]
|
col_c, pts_c = points_for_chunks(self.prefix, chunk_pls, vecs)
|
||||||
upsert_batch(self.client, f"{self.prefix}_chunks", c_pts)
|
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
|
logger.info(f" ✨ Phase 1 fertig: {len(explicit_edges)} explizite Kanten für '{note_id}'.")
|
||||||
if edges:
|
return {"status": "success", "note_id": note_id}
|
||||||
e_pts = points_for_edges(self.prefix, edges)[1]
|
|
||||||
upsert_batch(self.client, f"{self.prefix}_edges", e_pts)
|
|
||||||
|
|
||||||
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:
|
except Exception as e:
|
||||||
logger.error(f"Processing failed: {e}", exc_info=True)
|
logger.error(f"❌ Fehler bei {file_path}: {e}", exc_info=True)
|
||||||
return {**result, "error": str(e)}
|
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]:
|
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)
|
target_path = os.path.join(vault_root, folder, filename)
|
||||||
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
||||||
with open(target_path, "w", encoding="utf-8") as f:
|
with open(target_path, "w", encoding="utf-8") as f:
|
||||||
f.write(markdown_content)
|
f.write(markdown_content)
|
||||||
await asyncio.sleep(0.1)
|
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)
|
return await self.process_file(file_path=target_path, vault_root=vault_root, apply=True, force_replace=True, purge_before=True)
|
||||||
|
|
@ -1,20 +1,23 @@
|
||||||
"""
|
"""
|
||||||
FILE: app/core/ingestion/ingestion_validation.py
|
FILE: app/core/ingestion/ingestion_validation.py
|
||||||
DESCRIPTION: WP-15b semantische Validierung von Kanten gegen den LocalBatchCache.
|
DESCRIPTION: WP-15b semantische Validierung von Kanten gegen den LocalBatchCache.
|
||||||
WP-25b: Umstellung auf Lazy-Prompt-Orchestration (prompt_key + variables).
|
WP-24c: Erweiterung um automatische Symmetrie-Generierung (Inverse Kanten).
|
||||||
VERSION: 2.14.0 (WP-25b: Lazy Prompt Integration)
|
WP-25b: Konsequente Lazy-Prompt-Orchestration (prompt_key + variables).
|
||||||
|
VERSION: 3.0.0 (WP-24c: Symmetric Edge Management)
|
||||||
STATUS: Active
|
STATUS: Active
|
||||||
FIX:
|
FIX:
|
||||||
- WP-25b: Entfernung manueller Prompt-Formatierung zur Unterstützung modell-spezifischer Prompts.
|
- WP-24c: Integration der EdgeRegistry zur dynamischen Inversions-Ermittlung.
|
||||||
- WP-25b: Umstellung auf generate_raw_response mit prompt_key="edge_validation".
|
- WP-24c: Implementierung von validate_and_symmetrize für bidirektionale Graphen.
|
||||||
- WP-25a: Voller Erhalt der MoE-Profilsteuerung und Fallback-Kaskade via LLMService.
|
- WP-25b: Beibehaltung der hierarchischen Prompt-Resolution und Modell-Spezi-Logik.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional, List
|
||||||
from app.core.parser import NoteContext
|
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
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -28,18 +31,18 @@ async def validate_edge_candidate(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
WP-15b/25b: Validiert einen Kandidaten semantisch gegen das Ziel im Cache.
|
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_id = edge.get("to")
|
||||||
target_ctx = batch_cache.get(target_id)
|
target_ctx = batch_cache.get(target_id)
|
||||||
|
|
||||||
# Robust Lookup Fix (v2.12.2): Support für Anker
|
# Robust Lookup Fix (v2.12.2): Support für Anker (Note#Section)
|
||||||
if not target_ctx and "#" in target_id:
|
if not target_ctx and "#" in str(target_id):
|
||||||
base_id = target_id.split("#")[0]
|
base_id = target_id.split("#")[0]
|
||||||
target_ctx = batch_cache.get(base_id)
|
target_ctx = batch_cache.get(base_id)
|
||||||
|
|
||||||
# Sicherheits-Fallback (Hard-Link Integrity)
|
# 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:
|
if not target_ctx:
|
||||||
logger.info(f"ℹ️ [VALIDATION SKIP] No context for '{target_id}' - allowing link.")
|
logger.info(f"ℹ️ [VALIDATION SKIP] No context for '{target_id}' - allowing link.")
|
||||||
return True
|
return True
|
||||||
|
|
@ -48,8 +51,7 @@ async def validate_edge_candidate(
|
||||||
logger.info(f"⚖️ [VALIDATING] Relation '{edge.get('kind')}' -> '{target_id}' (Profile: {profile_name})...")
|
logger.info(f"⚖️ [VALIDATING] Relation '{edge.get('kind')}' -> '{target_id}' (Profile: {profile_name})...")
|
||||||
|
|
||||||
# WP-25b: Lazy-Prompt Aufruf.
|
# WP-25b: Lazy-Prompt Aufruf.
|
||||||
# Wir übergeben keine formatierte Nachricht mehr, sondern Key und Daten-Dict.
|
# Übergabe von prompt_key und Variablen für modell-optimierte Formatierung.
|
||||||
# Das manuelle 'template = llm_service.get_prompt(...)' entfällt hier.
|
|
||||||
raw_response = await llm_service.generate_raw_response(
|
raw_response = await llm_service.generate_raw_response(
|
||||||
prompt_key="edge_validation",
|
prompt_key="edge_validation",
|
||||||
variables={
|
variables={
|
||||||
|
|
@ -62,7 +64,7 @@ async def validate_edge_candidate(
|
||||||
profile_name=profile_name
|
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)
|
response = clean_llm_text(raw_response)
|
||||||
|
|
||||||
# Semantische Prüfung des Ergebnisses
|
# Semantische Prüfung des Ergebnisses
|
||||||
|
|
@ -78,12 +80,71 @@ async def validate_edge_candidate(
|
||||||
error_str = str(e).lower()
|
error_str = str(e).lower()
|
||||||
error_type = type(e).__name__
|
error_type = type(e).__name__
|
||||||
|
|
||||||
# WP-25b FIX: Differenzierung zwischen transienten und permanenten Fehlern
|
# WP-25b: Differenzierung zwischen transienten und permanenten Fehlern
|
||||||
# Transiente Fehler (Timeout, Network) → erlauben (Datenverlust vermeiden)
|
# Transiente Fehler (Netzwerk) → erlauben (Integrität vor Präzision)
|
||||||
if any(x in error_str for x in ["timeout", "connection", "network", "unreachable", "refused"]):
|
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
|
return True
|
||||||
|
|
||||||
# Permanente Fehler (Config, Validation, Invalid Response) → ablehnen (Graph-Qualität)
|
# Permanente Fehler → ablehnen (Graph-Qualität schützen)
|
||||||
logger.error(f"❌ Permanent validation error for {target_id} using {profile_name}: {error_type} - {e}")
|
logger.error(f"❌ Permanent validation error for {target_id}: {error_type} - {e}")
|
||||||
return False
|
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
|
||||||
|
|
@ -2,36 +2,52 @@ import logging
|
||||||
import os
|
import os
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging(log_level: int = None):
|
||||||
# 1. Log-Verzeichnis erstellen (falls nicht vorhanden)
|
"""
|
||||||
|
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"
|
log_dir = "logs"
|
||||||
if not os.path.exists(log_dir):
|
if not os.path.exists(log_dir):
|
||||||
os.makedirs(log_dir)
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
log_file = os.path.join(log_dir, "mindnet.log")
|
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(
|
formatter = logging.Formatter(
|
||||||
'%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
|
'%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
|
||||||
datefmt='%Y-%m-%d %H:%M:%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(
|
file_handler = RotatingFileHandler(
|
||||||
log_file, maxBytes=5*1024*1024, backupCount=5, encoding='utf-8'
|
log_file, maxBytes=5*1024*1024, backupCount=5, encoding='utf-8'
|
||||||
)
|
)
|
||||||
file_handler.setFormatter(formatter)
|
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 = logging.StreamHandler()
|
||||||
console_handler.setFormatter(formatter)
|
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(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=log_level,
|
||||||
handlers=[file_handler, console_handler]
|
handlers=[file_handler, console_handler],
|
||||||
|
force=True # Überschreibt bestehende Konfiguration
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.info(f"📝 Logging initialized. Writing to {log_file}")
|
level_name = "DEBUG" if log_level == logging.DEBUG else "INFO"
|
||||||
|
logging.info(f"📝 Logging initialized (Level: {level_name}). Writing to {log_file}")
|
||||||
|
|
@ -151,15 +151,21 @@ class DecisionEngine:
|
||||||
retrieval_results = await asyncio.gather(*retrieval_tasks, return_exceptions=True)
|
retrieval_results = await asyncio.gather(*retrieval_tasks, return_exceptions=True)
|
||||||
|
|
||||||
# Phase 2: Formatierung und optionale Kompression
|
# 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 = []
|
final_stream_tasks = []
|
||||||
|
formatted_contexts = {} # WP-24c v4.5.5: Persistenz für Fallback-Zugriff
|
||||||
|
|
||||||
for name, res in zip(active_streams, retrieval_results):
|
for name, res in zip(active_streams, retrieval_results):
|
||||||
if isinstance(res, Exception):
|
if isinstance(res, Exception):
|
||||||
logger.error(f"Stream '{name}' failed during retrieval: {res}")
|
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())
|
final_stream_tasks.append(_err())
|
||||||
continue
|
continue
|
||||||
|
|
||||||
formatted_context = self._format_stream_context(res)
|
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)
|
# WP-25a: Kompressions-Check (Inhaltsverdichtung)
|
||||||
stream_cfg = library.get(name, {})
|
stream_cfg = library.get(name, {})
|
||||||
|
|
@ -168,6 +174,7 @@ class DecisionEngine:
|
||||||
if len(formatted_context) > threshold:
|
if len(formatted_context) > threshold:
|
||||||
logger.info(f"⚙️ [WP-25b] Triggering Lazy-Compression for stream '{name}'...")
|
logger.info(f"⚙️ [WP-25b] Triggering Lazy-Compression for stream '{name}'...")
|
||||||
comp_profile = stream_cfg.get("compression_profile")
|
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(
|
final_stream_tasks.append(
|
||||||
self._compress_stream_content(name, formatted_context, query, comp_profile)
|
self._compress_stream_content(name, formatted_context, query, comp_profile)
|
||||||
)
|
)
|
||||||
|
|
@ -176,12 +183,31 @@ class DecisionEngine:
|
||||||
final_stream_tasks.append(_direct())
|
final_stream_tasks.append(_direct())
|
||||||
|
|
||||||
# Finale Inhalte parallel fertigstellen
|
# Finale Inhalte parallel fertigstellen
|
||||||
final_contents = await asyncio.gather(*final_stream_tasks)
|
# WP-24c v4.5.5: Bei Kompressions-Fehlern wird der Original-Content zurückgegeben (siehe _compress_stream_content)
|
||||||
return dict(zip(active_streams, final_contents))
|
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:
|
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:
|
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(
|
summary = await self.llm_service.generate_raw_response(
|
||||||
prompt_key="compression_template",
|
prompt_key="compression_template",
|
||||||
variables={
|
variables={
|
||||||
|
|
@ -193,9 +219,19 @@ class DecisionEngine:
|
||||||
priority="background",
|
priority="background",
|
||||||
max_retries=1
|
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:
|
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
|
return content
|
||||||
|
|
||||||
async def _run_single_stream(self, name: str, cfg: Dict, query: str) -> QueryResponse:
|
async def _run_single_stream(self, name: str, cfg: Dict, query: str) -> QueryResponse:
|
||||||
|
|
@ -211,7 +247,26 @@ class DecisionEngine:
|
||||||
explain=True
|
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)
|
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:
|
for hit in response.results:
|
||||||
hit.stream_origin = name
|
hit.stream_origin = name
|
||||||
return response
|
return response
|
||||||
|
|
@ -270,19 +325,54 @@ class DecisionEngine:
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Final Synthesis failed: {e}")
|
logger.error(f"Final Synthesis failed: {e}")
|
||||||
# ROBUST FALLBACK (v1.2.1 Gate): Versuche eine minimale Antwort zu generieren
|
# WP-24c v4.5.5: ROBUST FALLBACK mit Context-Reuse
|
||||||
# WP-25b FIX: Konsistente Nutzung von prompt_key statt hardcodiertem Prompt
|
# 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])
|
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:
|
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",
|
prompt_key="fallback_synthesis",
|
||||||
variables={"query": query, "context": fallback_context},
|
variables={"query": query, "context": fallback_context},
|
||||||
system=system_prompt, priority="realtime", profile_name=profile
|
system=system_prompt, priority="realtime", profile_name=profile
|
||||||
)
|
)
|
||||||
except (ValueError, KeyError):
|
|
||||||
# Fallback auf direkten Prompt, falls Template nicht existiert
|
logger.info(f"✅ [FALLBACK] Fallback-Synthese erfolgreich (Antwort-Länge: {len(result) if result else 0})")
|
||||||
logger.warning("⚠️ Fallback template 'fallback_synthesis' not found. Using direct prompt.")
|
return result
|
||||||
return await self.llm_service.generate_raw_response(
|
|
||||||
prompt=f"Beantworte: {query}\n\nKontext:\n{fallback_context}",
|
except (ValueError, KeyError) as template_error:
|
||||||
system=system_prompt, priority="realtime", profile_name=profile
|
# 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
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
FILE: app/core/retrieval/retriever.py
|
FILE: app/core/retrieval/retriever.py
|
||||||
DESCRIPTION: Haupt-Schnittstelle für die Suche. Orchestriert Vektorsuche und Graph-Expansion.
|
DESCRIPTION: Haupt-Schnittstelle für die Suche. Orchestriert Vektorsuche und Graph-Expansion.
|
||||||
WP-15c Update: Note-Level Diversity Pooling & Super-Edge Aggregation.
|
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
|
STATUS: Active
|
||||||
DEPENDENCIES: app.config, app.models.dto, app.core.database*, app.core.graph_adapter
|
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.services.embeddings_client as ec
|
||||||
import app.core.graph.graph_subgraph as ga
|
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
|
# Mathematische Engine importieren
|
||||||
from app.core.retrieval.retriever_scoring import get_weights, compute_wp22_score
|
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)
|
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(
|
def _semantic_hits(
|
||||||
client: Any,
|
client: Any,
|
||||||
prefix: str,
|
prefix: str,
|
||||||
vector: List[float],
|
vector: List[float],
|
||||||
top_k: int,
|
top_k: int,
|
||||||
filters: Optional[Dict] = None
|
filters: Optional[Dict] = None,
|
||||||
|
target_section: Optional[str] = None
|
||||||
) -> List[Tuple[str, float, Dict[str, Any]]]:
|
) -> 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)
|
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
|
# Strikte Typkonvertierung für Stabilität
|
||||||
return [(str(hit[0]), float(hit[1]), dict(hit[2] or {})) for hit in raw_hits]
|
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)
|
top_edges = sorted(edges_dto, key=lambda e: e.confidence, reverse=True)
|
||||||
for e in top_edges[:3]:
|
for e in top_edges[:3]:
|
||||||
peer = e.source if e.direction == "in" else e.target
|
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 ""
|
boost_txt = f" [Boost x{applied_boosts.get(e.kind)}]" if applied_boosts and e.kind in applied_boosts else ""
|
||||||
|
|
||||||
reasons.append(Reason(
|
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]")
|
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(
|
results.append(QueryHit(
|
||||||
node_id=str(pid),
|
node_id=str(pid),
|
||||||
note_id=str(pl.get("note_id", "unknown")),
|
note_id=str(pl.get("note_id", "unknown")),
|
||||||
|
|
@ -267,23 +346,51 @@ def _build_hits_from_semantic(
|
||||||
"text": text_content
|
"text": text_content
|
||||||
},
|
},
|
||||||
payload=pl,
|
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:
|
def hybrid_retrieve(req: QueryRequest) -> QueryResponse:
|
||||||
"""
|
"""
|
||||||
Die Haupt-Einstiegsfunktion für die hybride Suche.
|
Die Haupt-Einstiegsfunktion für die hybride Suche.
|
||||||
WP-15c: Implementiert Edge-Aggregation (Super-Kanten).
|
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()
|
client, prefix = _get_client_and_prefix()
|
||||||
vector = list(req.query_vector) if req.query_vector else _get_query_vector(req)
|
vector = list(req.query_vector) if req.query_vector else _get_query_vector(req)
|
||||||
top_k = req.top_k or 10
|
top_k = req.top_k or 10
|
||||||
|
|
||||||
# 1. Semantische Seed-Suche (Wir laden etwas mehr für das Pooling)
|
# 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
|
# 2. Graph Expansion Konfiguration
|
||||||
expand_cfg = req.expand if isinstance(req.expand, dict) else {}
|
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
|
subgraph: ga.Subgraph | None = None
|
||||||
if depth > 0 and hits:
|
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:
|
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.
|
# Verhindert Score-Explosion durch multiple Links auf versch. Abschnitte.
|
||||||
# Logik: 1. Kante zählt voll, weitere dämpfen auf Faktor 0.1.
|
# 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"):
|
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():
|
for src, edge_list in subgraph.adj.items():
|
||||||
# Gruppiere Kanten nach Ziel-Note (Deduplizierung ID_A -> ID_B)
|
# Gruppiere Kanten nach Ziel-Note (Deduplizierung ID_A -> ID_B)
|
||||||
by_target = defaultdict(list)
|
by_target = defaultdict(list)
|
||||||
for e in edge_list:
|
for e in edge_list:
|
||||||
by_target[e["target"]].append(e)
|
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 = []
|
aggregated_list = []
|
||||||
for tgt, edges in by_target.items():
|
for tgt, edges in by_target.items():
|
||||||
if len(edges) > 1:
|
if len(edges) > 1:
|
||||||
# Sortiere: Stärkste Kante zuerst
|
# Sortiere: Stärkste Kante zuerst (Authority-Priorisierung)
|
||||||
sorted_edges = sorted(edges, key=lambda x: x.get("weight", 0.0), reverse=True)
|
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]
|
primary = sorted_edges[0]
|
||||||
|
|
||||||
# Aggregiertes Gewicht berechnen (Sättigungs-Logik)
|
# Aggregiertes Gewicht berechnen (Sättigungs-Logik)
|
||||||
total_w = primary.get("weight", 0.0)
|
total_w = primary.get("weight", 0.0)
|
||||||
|
chunk_count = 0
|
||||||
for secondary in sorted_edges[1:]:
|
for secondary in sorted_edges[1:]:
|
||||||
total_w += secondary.get("weight", 0.0) * 0.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["weight"] = total_w
|
||||||
primary["is_super_edge"] = True # Flag für Explanation Layer
|
primary["is_super_edge"] = True # Flag für Explanation Layer
|
||||||
primary["edge_count"] = len(edges)
|
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)
|
aggregated_list.append(primary)
|
||||||
else:
|
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
|
# In-Place Update der Adjazenzliste des Graphen
|
||||||
subgraph.adj[src] = aggregated_list
|
subgraph.adj[src] = aggregated_list
|
||||||
|
|
@ -335,21 +495,32 @@ def hybrid_retrieve(req: QueryRequest) -> QueryResponse:
|
||||||
for src, edges in subgraph.adj.items():
|
for src, edges in subgraph.adj.items():
|
||||||
for e in edges:
|
for e in edges:
|
||||||
subgraph.in_degree[e["target"]] += 1
|
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"):
|
if subgraph and hasattr(subgraph, "adj"):
|
||||||
for src, edges in subgraph.adj.items():
|
for src, edges in subgraph.adj.items():
|
||||||
for e in edges:
|
for e in edges:
|
||||||
# A. Provenance Weighting
|
# A. Provenance Weighting (nutzt PROVENANCE_PRIORITY aus graph_utils)
|
||||||
prov = e.get("provenance", "rule")
|
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")
|
kind = e.get("kind")
|
||||||
intent_multiplier = boost_edges.get(kind, 1.0)
|
intent_multiplier = boost_edges.get(kind, 1.0)
|
||||||
|
|
||||||
# Gewichtung anpassen
|
# Gewichtung anpassen (Authority-Priorisierung)
|
||||||
e["weight"] = e.get("weight", 1.0) * prov_w * intent_multiplier
|
e["weight"] = e.get("weight", 1.0) * prov_w * confidence * virtual_penalty * intent_multiplier
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Graph Expansion failed: {e}")
|
logger.error(f"Graph Expansion failed: {e}")
|
||||||
|
|
@ -357,7 +528,24 @@ def hybrid_retrieve(req: QueryRequest) -> QueryResponse:
|
||||||
|
|
||||||
# 3. Scoring & Explanation Generierung
|
# 3. Scoring & Explanation Generierung
|
||||||
# top_k wird erst hier final angewandt
|
# 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:
|
def semantic_retrieve(req: QueryRequest) -> QueryResponse:
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,11 @@ def compute_wp22_score(
|
||||||
# Sicherstellen, dass der Score niemals 0 oder negativ ist (Floor)
|
# Sicherstellen, dass der Score niemals 0 oder negativ ist (Floor)
|
||||||
final_score = max(0.0001, float(total))
|
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 {
|
return {
|
||||||
"total": final_score,
|
"total": final_score,
|
||||||
"edge_bonus": float(edge_bonus_raw),
|
"edge_bonus": float(edge_bonus_raw),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
"""
|
"""
|
||||||
FILE: app/core/type_registry.py
|
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.
|
DESCRIPTION: Loader für types.yaml.
|
||||||
VERSION: 1.0.0
|
WP-24c: Robustheits-Fix für chunking_profile vs chunk_profile.
|
||||||
STATUS: Deprecated (Redundant)
|
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
|
DEPENDENCIES: yaml, os, functools
|
||||||
EXTERNAL_CONFIG: config/types.yaml
|
EXTERNAL_CONFIG: config/types.yaml
|
||||||
LAST_ANALYSIS: 2025-12-15
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -18,12 +19,12 @@ try:
|
||||||
except Exception:
|
except Exception:
|
||||||
yaml = None # wird erst benötigt, wenn eine Datei gelesen werden soll
|
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] = {
|
_DEFAULT_REGISTRY: Dict[str, Any] = {
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"types": {
|
"types": {
|
||||||
"concept": {
|
"concept": {
|
||||||
"chunk_profile": "medium",
|
"chunking_profile": "medium",
|
||||||
"edge_defaults": ["references", "related_to"],
|
"edge_defaults": ["references", "related_to"],
|
||||||
"retriever_weight": 1.0,
|
"retriever_weight": 1.0,
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +34,6 @@ _DEFAULT_REGISTRY: Dict[str, Any] = {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Chunk-Profile → Overlap-Empfehlungen (nur für synthetische Fensterbildung)
|
# 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]] = {
|
_PROFILE_TO_OVERLAP: Dict[str, Tuple[int, int]] = {
|
||||||
"short": (20, 30),
|
"short": (20, 30),
|
||||||
"medium": (40, 60),
|
"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]:
|
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.
|
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:
|
if not path:
|
||||||
return dict(_DEFAULT_REGISTRY)
|
return dict(_DEFAULT_REGISTRY)
|
||||||
|
|
@ -54,7 +54,6 @@ def load_type_registry(path: str = "config/types.yaml") -> Dict[str, Any]:
|
||||||
return dict(_DEFAULT_REGISTRY)
|
return dict(_DEFAULT_REGISTRY)
|
||||||
|
|
||||||
if yaml is None:
|
if yaml is None:
|
||||||
# PyYAML fehlt → auf Default zurückfallen
|
|
||||||
return dict(_DEFAULT_REGISTRY)
|
return dict(_DEFAULT_REGISTRY)
|
||||||
|
|
||||||
try:
|
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]:
|
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()
|
t = (note_type or "concept").strip().lower()
|
||||||
types = (reg or {}).get("types", {}) if isinstance(reg, dict) else {}
|
types = (reg or {}).get("types", {}) if isinstance(reg, dict) else {}
|
||||||
return types.get(t) or types.get("concept") or _DEFAULT_REGISTRY["types"]["concept"]
|
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]:
|
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)
|
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():
|
if isinstance(prof, str) and prof.strip():
|
||||||
return prof.strip().lower()
|
return prof.strip().lower()
|
||||||
return None
|
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."""
|
"""Gibt eine Overlap-Empfehlung (low, high) für das Profil zurück."""
|
||||||
if not profile:
|
if not profile:
|
||||||
return _PROFILE_TO_OVERLAP["medium"]
|
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"])
|
||||||
|
|
@ -46,7 +46,14 @@ class EdgeDTO(BaseModel):
|
||||||
target: str
|
target: str
|
||||||
weight: float
|
weight: float
|
||||||
direction: Literal["out", "in", "undirected"] = "out"
|
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
|
confidence: float = 1.0
|
||||||
target_section: Optional[str] = None
|
target_section: Optional[str] = None
|
||||||
|
|
||||||
|
|
@ -56,6 +63,7 @@ class EdgeDTO(BaseModel):
|
||||||
class QueryRequest(BaseModel):
|
class QueryRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
Request für /query. Unterstützt Multi-Stream Isolation via filters.
|
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"
|
mode: Literal["semantic", "edge", "hybrid"] = "hybrid"
|
||||||
query: Optional[str] = None
|
query: Optional[str] = None
|
||||||
|
|
@ -67,7 +75,10 @@ class QueryRequest(BaseModel):
|
||||||
explain: bool = False
|
explain: bool = False
|
||||||
|
|
||||||
# WP-22/25: Dynamische Gewichtung der Graphen-Highways
|
# 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):
|
class FeedbackRequest(BaseModel):
|
||||||
|
|
@ -125,6 +136,7 @@ class QueryHit(BaseModel):
|
||||||
"""
|
"""
|
||||||
Einzelnes Trefferobjekt.
|
Einzelnes Trefferobjekt.
|
||||||
WP-25: stream_origin hinzugefügt für Tracing und Feedback-Optimierung.
|
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
|
node_id: str
|
||||||
note_id: str
|
note_id: str
|
||||||
|
|
@ -137,6 +149,7 @@ class QueryHit(BaseModel):
|
||||||
payload: Optional[Dict] = None
|
payload: Optional[Dict] = None
|
||||||
explanation: Optional[Explanation] = None
|
explanation: Optional[Explanation] = None
|
||||||
stream_origin: Optional[str] = Field(None, description="Name des Ursprungs-Streams")
|
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):
|
class QueryResponse(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ FILE: app/routers/chat.py
|
||||||
DESCRIPTION: Haupt-Chat-Interface (WP-25b Edition).
|
DESCRIPTION: Haupt-Chat-Interface (WP-25b Edition).
|
||||||
Kombiniert die spezialisierte Interview-Logik mit der neuen
|
Kombiniert die spezialisierte Interview-Logik mit der neuen
|
||||||
Lazy-Prompt-Orchestration und MoE-Synthese.
|
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
|
STATUS: Active
|
||||||
FIX:
|
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: 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-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).
|
- 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 fastapi import APIRouter, HTTPException, Depends
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -22,13 +25,27 @@ import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from app.config import get_settings
|
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.llm_service import LLMService
|
||||||
from app.services.feedback_service import log_search
|
from app.services.feedback_service import log_search
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
logger = logging.getLogger(__name__)
|
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) ---
|
# --- EBENE 1: CONFIG LOADER & CACHING (WP-25 Standard) ---
|
||||||
|
|
||||||
_DECISION_CONFIG_CACHE = None
|
_DECISION_CONFIG_CACHE = None
|
||||||
|
|
@ -135,8 +152,7 @@ async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]:
|
||||||
return "INTERVIEW", "Keyword (Interview)"
|
return "INTERVIEW", "Keyword (Interview)"
|
||||||
|
|
||||||
# 3. SLOW PATH: DecisionEngine LLM Router (MoE-gesteuert)
|
# 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)
|
||||||
intent = await llm.decision_engine._determine_strategy(query) # TODO: Public API erstellen
|
|
||||||
return intent, "DecisionEngine (LLM)"
|
return intent, "DecisionEngine (LLM)"
|
||||||
|
|
||||||
# --- EBENE 3: RETRIEVAL AGGREGATION ---
|
# --- 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)
|
seen_node_ids.add(hit.node_id)
|
||||||
return sorted(all_hits, key=lambda h: h.total_score, reverse=True)
|
return sorted(all_hits, key=lambda h: h.total_score, reverse=True)
|
||||||
|
|
||||||
# --- EBENE 4: ENDPUNKT ---
|
# --- EBENE 4: ENDPUNKTE ---
|
||||||
|
|
||||||
def get_llm_service():
|
def get_llm_service():
|
||||||
return LLMService()
|
return LLMService()
|
||||||
|
|
@ -196,7 +212,6 @@ async def chat_endpoint(
|
||||||
template_key = strategy.get("prompt_template", "interview_template")
|
template_key = strategy.get("prompt_template", "interview_template")
|
||||||
|
|
||||||
# WP-25b: Lazy Loading Call
|
# WP-25b: Lazy Loading Call
|
||||||
# Wir übergeben nur Key und Variablen. Das System formatiert passend zum Modell.
|
|
||||||
answer_text = await llm.generate_raw_response(
|
answer_text = await llm.generate_raw_response(
|
||||||
prompt_key=template_key,
|
prompt_key=template_key,
|
||||||
variables={
|
variables={
|
||||||
|
|
@ -257,4 +272,91 @@ async def chat_endpoint(
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Chat Endpoint Failure: {e}", exc_info=True)
|
logger.error(f"❌ Chat Endpoint Failure: {e}", exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail="Fehler bei der Verarbeitung der Anfrage.")
|
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.")
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
"""
|
"""
|
||||||
FILE: app/services/discovery.py
|
FILE: app/services/discovery.py
|
||||||
DESCRIPTION: Service für WP-11. Analysiert Texte, findet Entitäten und schlägt typisierte Verbindungen vor ("Matrix-Logic").
|
DESCRIPTION: Service für WP-11 (Discovery API). Analysiert Entwürfe, findet Entitäten
|
||||||
VERSION: 0.6.0
|
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
|
STATUS: Active
|
||||||
DEPENDENCIES: app.core.qdrant, app.models.dto, app.core.retriever
|
COMPATIBILITY: 100% (Identische API-Signatur wie v0.6.0)
|
||||||
EXTERNAL_CONFIG: config/types.yaml
|
|
||||||
LAST_ANALYSIS: 2025-12-15
|
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
@ -16,204 +17,181 @@ import yaml
|
||||||
from app.core.database.qdrant import QdrantConfig, get_client
|
from app.core.database.qdrant import QdrantConfig, get_client
|
||||||
from app.models.dto import QueryRequest
|
from app.models.dto import QueryRequest
|
||||||
from app.core.retrieval.retriever import hybrid_retrieve
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class DiscoveryService:
|
class DiscoveryService:
|
||||||
def __init__(self, collection_prefix: str = None):
|
def __init__(self, collection_prefix: str = None):
|
||||||
|
"""Initialisiert den Discovery Service mit Qdrant-Anbindung."""
|
||||||
self.cfg = QdrantConfig.from_env()
|
self.cfg = QdrantConfig.from_env()
|
||||||
self.prefix = collection_prefix or self.cfg.prefix or "mindnet"
|
self.prefix = collection_prefix or self.cfg.prefix or "mindnet"
|
||||||
self.client = get_client(self.cfg)
|
self.client = get_client(self.cfg)
|
||||||
|
|
||||||
|
# Die Registry wird für Typ-Metadaten geladen (Schema-Validierung)
|
||||||
self.registry = self._load_type_registry()
|
self.registry = self._load_type_registry()
|
||||||
|
|
||||||
async def analyze_draft(self, text: str, current_type: str) -> Dict[str, Any]:
|
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 = []
|
suggestions = []
|
||||||
|
seen_target_ids = set()
|
||||||
# Fallback, falls keine spezielle Regel greift
|
|
||||||
default_edge_type = self._get_default_edge_type(current_type)
|
|
||||||
|
|
||||||
# Tracking-Sets für Deduplizierung (Wir merken uns NOTE-IDs)
|
# --- PHASE 1: EXACT MATCHES (TITEL & ALIASSE) ---
|
||||||
seen_target_note_ids = set()
|
# Lädt alle bekannten Titel/Aliasse für einen schnellen Scan
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# 1. Exact Match: Titel/Aliases
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Holt Titel, Aliases UND Typen aus dem Index
|
|
||||||
known_entities = self._fetch_all_titles_and_aliases()
|
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:
|
for entity in exact_matches:
|
||||||
if entity["id"] in seen_target_note_ids:
|
target_id = entity["id"]
|
||||||
|
if target_id in seen_target_ids:
|
||||||
continue
|
continue
|
||||||
seen_target_note_ids.add(entity["id"])
|
|
||||||
|
seen_target_ids.add(target_id)
|
||||||
# INTELLIGENTE KANTEN-LOGIK (MATRIX)
|
|
||||||
target_type = entity.get("type", "concept")
|
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({
|
suggestions.append({
|
||||||
"type": "exact_match",
|
"type": "exact_match",
|
||||||
"text_found": entity["match"],
|
"text_found": entity["match"],
|
||||||
"target_title": entity["title"],
|
"target_title": entity["title"],
|
||||||
"target_id": entity["id"],
|
"target_id": target_id,
|
||||||
"suggested_edge_type": smart_edge,
|
"suggested_edge_type": suggested_kind,
|
||||||
"suggested_markdown": f"[[rel:{smart_edge} {entity['title']}]]",
|
"suggested_markdown": f"[[rel:{suggest_kind} {entity['title']}]]",
|
||||||
"confidence": 1.0,
|
"confidence": 1.0,
|
||||||
"reason": f"Exakter Treffer: '{entity['match']}' ({target_type})"
|
"reason": f"Direkte Erwähnung von '{entity['match']}' ({target_type})"
|
||||||
})
|
})
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# --- PHASE 2: SEMANTIC MATCHES (VECTOR SEARCH) ---
|
||||||
# 2. Semantic Match: Sliding Window & Footer Focus
|
# Erzeugt Suchanfragen für verschiedene Fenster des Textes
|
||||||
# ---------------------------------------------------------
|
|
||||||
search_queries = self._generate_search_queries(text)
|
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]
|
tasks = [self._get_semantic_suggestions_async(q) for q in search_queries]
|
||||||
results_list = await asyncio.gather(*tasks)
|
results_list = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
# Ergebnisse verarbeiten
|
|
||||||
for hits in results_list:
|
for hits in results_list:
|
||||||
for hit in hits:
|
for hit in hits:
|
||||||
note_id = hit.payload.get("note_id")
|
payload = hit.payload or {}
|
||||||
if not note_id: continue
|
target_id = payload.get("note_id")
|
||||||
|
|
||||||
# Deduplizierung (Notiz-Ebene)
|
if not target_id or target_id in seen_target_ids:
|
||||||
if note_id in seen_target_note_ids:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Score Check (Threshold 0.50 für nomic-embed-text)
|
# Relevanz-Threshold (Modell-spezifisch für nomic)
|
||||||
if hit.total_score > 0.50:
|
if hit.total_score > 0.55:
|
||||||
seen_target_note_ids.add(note_id)
|
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"
|
# WP-24c: Nutzung der Topologie-Engine
|
||||||
|
suggested_kind = self._resolve_edge_type(current_type, target_type)
|
||||||
# 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)
|
|
||||||
|
|
||||||
suggestions.append({
|
suggestions.append({
|
||||||
"type": "semantic_match",
|
"type": "semantic_match",
|
||||||
"text_found": (hit.source.get("text") or "")[:60] + "...",
|
"text_found": (hit.source.get("text") or "")[:80] + "...",
|
||||||
"target_title": target_title,
|
"target_title": target_title,
|
||||||
"target_id": note_id,
|
"target_id": target_id,
|
||||||
"suggested_edge_type": smart_edge,
|
"suggested_edge_type": suggested_kind,
|
||||||
"suggested_markdown": f"[[rel:{smart_edge} {target_title}]]",
|
"suggested_markdown": f"[[rel:{suggested_kind} {target_title}]]",
|
||||||
"confidence": round(hit.total_score, 2),
|
"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)
|
suggestions.sort(key=lambda x: x["confidence"], reverse=True)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"draft_length": len(text),
|
"draft_length": len(text),
|
||||||
"analyzed_windows": len(search_queries),
|
"analyzed_windows": len(search_queries),
|
||||||
"suggestions_count": len(suggestions),
|
"suggestions_count": len(suggestions),
|
||||||
"suggestions": suggestions[:10]
|
"suggestions": suggestions[:12] # Top 12 Vorschläge
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# --- LOGIK-ZENTRALE (WP-24c) ---
|
||||||
# Core Logic: Die Matrix
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
def _resolve_edge_type(self, source_type: str, target_type: str) -> str:
|
def _resolve_edge_type(self, source_type: str, target_type: str) -> str:
|
||||||
"""
|
"""
|
||||||
Entscheidungsmatrix für komplexe Verbindungen.
|
Ermittelt den optimalen Kanten-Typ zwischen zwei Notiz-Typen.
|
||||||
Definiert, wie Typ A auf Typ B verlinken sollte.
|
Nutzt EdgeRegistry (graph_schema.md) statt lokaler Matrix.
|
||||||
"""
|
"""
|
||||||
st = source_type.lower()
|
# 1. Spezifische Prüfung: Gibt es eine Regel für Source -> Target?
|
||||||
tt = target_type.lower()
|
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)
|
# 2. Fallback: Was ist für den Quell-Typ generell typisch? (Source -> any)
|
||||||
if st == "experience":
|
info_fallback = edge_registry.get_topology_info(source_type, "any")
|
||||||
if tt == "value": return "based_on"
|
typical_fallback = info_fallback.get("typical", [])
|
||||||
if tt == "principle": return "derived_from"
|
if typical_fallback:
|
||||||
if tt == "trip": return "part_of"
|
return typical_fallback[0]
|
||||||
if tt == "lesson": return "learned"
|
|
||||||
if tt == "project": return "related_to" # oder belongs_to
|
|
||||||
|
|
||||||
# Regeln für 'project'
|
# 3. Globaler Fallback (Sicherheitsnetz)
|
||||||
if st == "project":
|
return "related_to"
|
||||||
if tt == "decision": return "depends_on"
|
|
||||||
if tt == "concept": return "uses"
|
|
||||||
if tt == "person": return "managed_by"
|
|
||||||
|
|
||||||
# Regeln für 'decision' (ADR)
|
# --- HELPERS (VOLLSTÄNDIG ERHALTEN) ---
|
||||||
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
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
def _generate_search_queries(self, text: str) -> List[str]:
|
def _generate_search_queries(self, text: str) -> List[str]:
|
||||||
"""
|
"""Erzeugt überlappende Fenster für die Vektorsuche (Sliding Window)."""
|
||||||
Erzeugt intelligente Fenster + Footer Scan.
|
|
||||||
"""
|
|
||||||
text_len = len(text)
|
text_len = len(text)
|
||||||
if not text: return []
|
|
||||||
|
|
||||||
queries = []
|
queries = []
|
||||||
|
|
||||||
# 1. Start / Gesamtkontext
|
# Fokus A: Dokument-Anfang (Kontext)
|
||||||
queries.append(text[:600])
|
queries.append(text[:600])
|
||||||
|
|
||||||
# 2. Footer-Scan (Wichtig für "Projekt"-Referenzen am Ende)
|
# Fokus B: Dokument-Ende (Aktueller Schreibfokus)
|
||||||
if text_len > 150:
|
if text_len > 250:
|
||||||
footer = text[-250:]
|
footer = text[-350:]
|
||||||
if footer not in queries:
|
if footer not in queries:
|
||||||
queries.append(footer)
|
queries.append(footer)
|
||||||
|
|
||||||
# 3. Sliding Window für lange Texte
|
# Fokus C: Zwischenabschnitte bei langen Texten
|
||||||
if text_len > 800:
|
if text_len > 1200:
|
||||||
window_size = 500
|
window_size = 500
|
||||||
step = 1500
|
step = 1200
|
||||||
for i in range(window_size, text_len - window_size, step):
|
for i in range(600, text_len - 400, step):
|
||||||
end_pos = min(i + window_size, text_len)
|
chunk = text[i:i+window_size]
|
||||||
chunk = text[i:end_pos]
|
|
||||||
if len(chunk) > 100:
|
if len(chunk) > 100:
|
||||||
queries.append(chunk)
|
queries.append(chunk)
|
||||||
|
|
||||||
return queries
|
return queries
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Standard Helpers
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
async def _get_semantic_suggestions_async(self, text: str):
|
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:
|
try:
|
||||||
|
# Nutzt hybrid_retrieve (WP-15b Standard)
|
||||||
res = hybrid_retrieve(req)
|
res = hybrid_retrieve(req)
|
||||||
return res.results
|
return res.results
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Semantic suggestion error: {e}")
|
logger.error(f"Discovery retrieval error: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _load_type_registry(self) -> dict:
|
def _load_type_registry(self) -> dict:
|
||||||
|
"""Lädt die types.yaml für Typ-Definitionen."""
|
||||||
path = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
|
path = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
if os.path.exists("types.yaml"): path = "types.yaml"
|
return {}
|
||||||
else: return {}
|
|
||||||
try:
|
try:
|
||||||
with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {}
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
except Exception: return {}
|
return yaml.safe_load(f) or {}
|
||||||
|
except Exception:
|
||||||
def _get_default_edge_type(self, note_type: str) -> str:
|
return {}
|
||||||
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"
|
|
||||||
|
|
||||||
def _fetch_all_titles_and_aliases(self) -> List[Dict]:
|
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
|
next_page = None
|
||||||
col = f"{self.prefix}_notes"
|
col = f"{self.prefix}_notes"
|
||||||
try:
|
try:
|
||||||
|
|
@ -225,30 +203,40 @@ class DiscoveryService:
|
||||||
for point in res:
|
for point in res:
|
||||||
pl = point.payload or {}
|
pl = point.payload or {}
|
||||||
aliases = pl.get("aliases") 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"),
|
"id": pl.get("note_id"),
|
||||||
"title": pl.get("title"),
|
"title": pl.get("title"),
|
||||||
"aliases": aliases,
|
"aliases": aliases,
|
||||||
"type": pl.get("type", "concept") # WICHTIG: Typ laden für Matrix
|
"type": pl.get("type", "concept")
|
||||||
})
|
})
|
||||||
if next_page is None: break
|
if next_page is None:
|
||||||
except Exception: pass
|
break
|
||||||
return notes
|
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]:
|
def _find_entities_in_text(self, text: str, entities: List[Dict]) -> List[Dict]:
|
||||||
|
"""Sucht im Text nach Erwähnungen bekannter Entitäten."""
|
||||||
found = []
|
found = []
|
||||||
text_lower = text.lower()
|
text_lower = text.lower()
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
# Title Check
|
|
||||||
title = entity.get("title")
|
title = entity.get("title")
|
||||||
|
# Titel-Check
|
||||||
if title and title.lower() in text_lower:
|
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
|
continue
|
||||||
# Alias Check
|
# Alias-Check
|
||||||
for alias in entity.get("aliases", []):
|
for alias in entity.get("aliases", []):
|
||||||
if str(alias).lower() in text_lower:
|
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
|
break
|
||||||
return found
|
return found
|
||||||
|
|
@ -1,21 +1,17 @@
|
||||||
"""
|
"""
|
||||||
FILE: app/services/edge_registry.py
|
FILE: app/services/edge_registry.py
|
||||||
DESCRIPTION: Single Source of Truth für Kanten-Typen mit dynamischem Reload.
|
DESCRIPTION: Single Source of Truth für Kanten-Typen, Symmetrien und Graph-Topologie.
|
||||||
WP-15b: Erweiterte Provenance-Prüfung für die Candidate-Validation.
|
WP-24c: Implementierung der dualen Registry (Vocabulary & Schema).
|
||||||
Sichert die Graph-Integrität durch strikte Trennung von System- und Inhaltskanten.
|
Unterstützt dynamisches Laden von Inversen und kontextuellen Vorschlägen.
|
||||||
WP-22: Fix für absolute Pfade außerhalb des Vaults (Prod-Dictionary).
|
VERSION: 1.0.1 (WP-24c: Verified Atomic Topology)
|
||||||
WP-20: Synchronisation mit zentralen Settings (v0.6.2).
|
|
||||||
VERSION: 0.8.0
|
|
||||||
STATUS: Active
|
STATUS: Active
|
||||||
DEPENDENCIES: re, os, json, logging, time, app.config
|
|
||||||
LAST_ANALYSIS: 2025-12-26
|
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Dict, Optional, Set, Tuple
|
from typing import Dict, Optional, Set, Tuple, List
|
||||||
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
|
|
||||||
|
|
@ -23,11 +19,12 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class EdgeRegistry:
|
class EdgeRegistry:
|
||||||
"""
|
"""
|
||||||
Zentraler Verwalter für das Kanten-Vokabular.
|
Zentraler Verwalter für das Kanten-Vokabular und das Graph-Schema.
|
||||||
Implementiert das Singleton-Pattern für konsistente Validierung über alle Services.
|
Singleton-Pattern zur Sicherstellung konsistenter Validierung.
|
||||||
"""
|
"""
|
||||||
_instance = None
|
_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"}
|
FORBIDDEN_SYSTEM_EDGES = {"next", "prev", "belongs_to"}
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
|
|
@ -42,124 +39,189 @@ class EdgeRegistry:
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
|
||||||
# 1. Pfad aus den zentralen Settings laden (WP-20 Synchronisation)
|
# --- Pfad-Konfiguration (WP-24c: Variable Pfade für Vault-Spiegelung) ---
|
||||||
# Priorisiert den Pfad aus der .env / config.py (v0.6.2)
|
# Das Vokabular (Semantik)
|
||||||
self.full_vocab_path = os.path.abspath(settings.MINDNET_VOCAB_PATH)
|
self.full_vocab_path = os.path.abspath(settings.MINDNET_VOCAB_PATH)
|
||||||
|
|
||||||
self.unknown_log_path = "data/logs/unknown_edges.jsonl"
|
# Das Schema (Topologie) - Konfigurierbar via ENV: MINDNET_SCHEMA_PATH
|
||||||
self.canonical_map: Dict[str, str] = {}
|
schema_env = getattr(settings, "MINDNET_SCHEMA_PATH", None)
|
||||||
self.valid_types: Set[str] = set()
|
if schema_env:
|
||||||
self._last_mtime = 0.0
|
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.ensure_latest()
|
||||||
self.initialized = True
|
self.initialized = True
|
||||||
|
|
||||||
def ensure_latest(self):
|
def ensure_latest(self):
|
||||||
"""
|
"""Prüft Zeitstempel beider Dateien und führt bei Änderung Hot-Reload durch."""
|
||||||
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
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_mtime = os.path.getmtime(self.full_vocab_path)
|
# Vokabular-Reload bei Änderung
|
||||||
if current_mtime > self._last_mtime:
|
if os.path.exists(self.full_vocab_path):
|
||||||
self._load_vocabulary()
|
v_mtime = os.path.getmtime(self.full_vocab_path)
|
||||||
self._last_mtime = current_mtime
|
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:
|
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):
|
def _load_vocabulary(self):
|
||||||
"""
|
"""Parst edge_vocabulary.md: | Canonical | Inverse | Aliases | Description |"""
|
||||||
Parst das Markdown-Wörterbuch und baut die Canonical-Map auf.
|
|
||||||
Erkennt Tabellen-Strukturen und extrahiert fettgedruckte System-Typen.
|
|
||||||
"""
|
|
||||||
self.canonical_map.clear()
|
self.canonical_map.clear()
|
||||||
|
self.inverse_map.clear()
|
||||||
self.valid_types.clear()
|
self.valid_types.clear()
|
||||||
|
|
||||||
# Regex für Tabellen-Struktur: | **Typ** | Aliase |
|
# Regex für die 4-Spalten Struktur (WP-24c konform)
|
||||||
pattern = re.compile(r"\|\s*\*\*`?([a-zA-Z0-9_-]+)`?\*\*\s*\|\s*([^|]+)\|")
|
# Erwartet: | **`type`** | `inverse` | alias1, alias2 | ... |
|
||||||
|
pattern = re.compile(r"\|\s*\*\*`?([a-zA-Z0-9_-]+)`?\*\*\s*\|\s*`?([a-zA-Z0-9_-]+)`?\s*\|\s*([^|]+)\|")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(self.full_vocab_path, "r", encoding="utf-8") as f:
|
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:
|
for line in f:
|
||||||
match = pattern.search(line)
|
match = pattern.search(line)
|
||||||
if match:
|
if match:
|
||||||
canonical = match.group(1).strip().lower()
|
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.valid_types.add(canonical)
|
||||||
self.canonical_map[canonical] = 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:
|
# Aliase verarbeiten (Normalisierung auf snake_case)
|
||||||
aliases = [a.strip() for a in aliases_str.split(",") if a.strip()]
|
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:
|
for alias in aliases:
|
||||||
# Normalisierung: Kleinschreibung, Underscores statt Leerzeichen
|
|
||||||
clean_alias = alias.replace("`", "").lower().strip().replace(" ", "_")
|
clean_alias = alias.replace("`", "").lower().strip().replace(" ", "_")
|
||||||
self.canonical_map[clean_alias] = canonical
|
if clean_alias:
|
||||||
c_aliases += 1
|
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:
|
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:
|
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.
|
Löst Aliasse auf kanonische Namen auf und schützt System-Kanten.
|
||||||
Sichert, dass nur strukturelle Prozesse System-Kanten setzen dürfen.
|
Erhalt der v0.8.0 Schutz-Logik.
|
||||||
"""
|
"""
|
||||||
self.ensure_latest()
|
self.ensure_latest()
|
||||||
if not edge_type:
|
if not edge_type:
|
||||||
return "related_to"
|
return "related_to"
|
||||||
|
|
||||||
# Normalisierung des Typs
|
|
||||||
clean_type = edge_type.lower().strip().replace(" ", "_").replace("-", "_")
|
clean_type = edge_type.lower().strip().replace(" ", "_").replace("-", "_")
|
||||||
ctx = context or {}
|
ctx = context or {}
|
||||||
|
|
||||||
# WP-15b: System-Kanten dürfen weder manuell noch durch KI/Vererbung gesetzt werden.
|
# Sicherheits-Gate: Schutz vor unerlaubter Nutzung von System-Kanten
|
||||||
# Nur Provenienz 'structure' (interne Prozesse) ist autorisiert.
|
|
||||||
# Wir blockieren hier alle Provenienzen außer 'structure'.
|
|
||||||
restricted_provenance = ["explicit", "semantic_ai", "inherited", "global_pool", "rule"]
|
restricted_provenance = ["explicit", "semantic_ai", "inherited", "global_pool", "rule"]
|
||||||
if provenance in restricted_provenance and clean_type in self.FORBIDDEN_SYSTEM_EDGES:
|
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"
|
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:
|
if provenance == "structure" and clean_type in self.FORBIDDEN_SYSTEM_EDGES:
|
||||||
return clean_type
|
return clean_type
|
||||||
|
|
||||||
# Mapping auf kanonischen Namen (Alias-Auflösung)
|
# Alias-Auflösung
|
||||||
if clean_type in self.canonical_map:
|
return self.canonical_map.get(clean_type, clean_type)
|
||||||
return self.canonical_map[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
|
# Hierarchische Suche: Spezifisch -> 'any' -> Empty
|
||||||
self._log_issue(clean_type, "unknown_type", ctx)
|
src_cfg = self.topology.get(source_type, self.topology.get("any", {}))
|
||||||
return clean_type
|
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):
|
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:
|
try:
|
||||||
os.makedirs(os.path.dirname(self.unknown_log_path), exist_ok=True)
|
os.makedirs(os.path.dirname(self.unknown_log_path), exist_ok=True)
|
||||||
entry = {
|
entry = {
|
||||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"edge_type": edge_type,
|
"edge_type": edge_type,
|
||||||
"error": error_kind,
|
"error": error_kind,
|
||||||
"file": ctx.get("file", "unknown"),
|
|
||||||
"line": ctx.get("line", "unknown"),
|
|
||||||
"note_id": ctx.get("note_id", "unknown"),
|
"note_id": ctx.get("note_id", "unknown"),
|
||||||
"provenance": ctx.get("provenance", "unknown")
|
"provenance": ctx.get("provenance", "unknown")
|
||||||
}
|
}
|
||||||
with open(self.unknown_log_path, "a", encoding="utf-8") as f:
|
with open(self.unknown_log_path, "a", encoding="utf-8") as f:
|
||||||
f.write(json.dumps(entry) + "\n")
|
f.write(json.dumps(entry) + "\n")
|
||||||
except Exception:
|
except Exception: pass
|
||||||
pass
|
|
||||||
|
|
||||||
# Singleton Export für systemweiten Zugriff
|
# Singleton Export
|
||||||
registry = EdgeRegistry()
|
registry = EdgeRegistry()
|
||||||
|
|
@ -45,4 +45,19 @@ MINDNET_VAULT_ROOT=./vault_prod
|
||||||
MINDNET_VOCAB_PATH=/mindnet/vault/mindnet/_system/dictionary/edge_vocabulary.md
|
MINDNET_VOCAB_PATH=/mindnet/vault/mindnet/_system/dictionary/edge_vocabulary.md
|
||||||
|
|
||||||
# Change Detection für effiziente Re-Imports
|
# Change Detection für effiziente Re-Imports
|
||||||
MINDNET_CHANGE_DETECTION_MODE=full
|
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
|
||||||
|
|
@ -23,7 +23,6 @@ chunking_profiles:
|
||||||
overlap: [50, 100]
|
overlap: [50, 100]
|
||||||
|
|
||||||
# C. SMART FLOW (Text-Fluss)
|
# C. SMART FLOW (Text-Fluss)
|
||||||
# Nutzt Sliding Window, aber mit LLM-Kanten-Analyse.
|
|
||||||
sliding_smart_edges:
|
sliding_smart_edges:
|
||||||
strategy: sliding_window
|
strategy: sliding_window
|
||||||
enable_smart_edge_allocation: true
|
enable_smart_edge_allocation: true
|
||||||
|
|
@ -32,7 +31,6 @@ chunking_profiles:
|
||||||
overlap: [50, 80]
|
overlap: [50, 80]
|
||||||
|
|
||||||
# D. SMART STRUCTURE (Soft Split)
|
# D. SMART STRUCTURE (Soft Split)
|
||||||
# Trennt bevorzugt an H2, fasst aber kleine Abschnitte zusammen ("Soft Mode").
|
|
||||||
structured_smart_edges:
|
structured_smart_edges:
|
||||||
strategy: by_heading
|
strategy: by_heading
|
||||||
enable_smart_edge_allocation: true
|
enable_smart_edge_allocation: true
|
||||||
|
|
@ -43,8 +41,6 @@ chunking_profiles:
|
||||||
overlap: [50, 80]
|
overlap: [50, 80]
|
||||||
|
|
||||||
# E. SMART STRUCTURE STRICT (H2 Hard Split)
|
# 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:
|
structured_smart_edges_strict:
|
||||||
strategy: by_heading
|
strategy: by_heading
|
||||||
enable_smart_edge_allocation: true
|
enable_smart_edge_allocation: true
|
||||||
|
|
@ -55,9 +51,6 @@ chunking_profiles:
|
||||||
overlap: [50, 80]
|
overlap: [50, 80]
|
||||||
|
|
||||||
# F. SMART STRUCTURE DEEP (H3 Hard Split + Merge-Check)
|
# 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:
|
structured_smart_edges_strict_L3:
|
||||||
strategy: by_heading
|
strategy: by_heading
|
||||||
enable_smart_edge_allocation: true
|
enable_smart_edge_allocation: true
|
||||||
|
|
@ -73,22 +66,17 @@ chunking_profiles:
|
||||||
defaults:
|
defaults:
|
||||||
retriever_weight: 1.0
|
retriever_weight: 1.0
|
||||||
chunking_profile: sliding_standard
|
chunking_profile: sliding_standard
|
||||||
edge_defaults: []
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 3. INGESTION SETTINGS (WP-14 Dynamization)
|
# 3. INGESTION SETTINGS (WP-14 Dynamization)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Steuert, welche Notizen verarbeitet werden und wie Fallbacks aussehen.
|
|
||||||
ingestion_settings:
|
ingestion_settings:
|
||||||
# Liste der Status-Werte, die beim Import ignoriert werden sollen.
|
|
||||||
ignore_statuses: ["system", "template", "archive", "hidden"]
|
ignore_statuses: ["system", "template", "archive", "hidden"]
|
||||||
# Standard-Typ, falls kein Typ im Frontmatter angegeben ist.
|
|
||||||
default_note_type: "concept"
|
default_note_type: "concept"
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 4. SUMMARY & SCAN SETTINGS
|
# 4. SUMMARY & SCAN SETTINGS
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Steuert die Tiefe des Pre-Scans für den Context-Cache.
|
|
||||||
summary_settings:
|
summary_settings:
|
||||||
max_summary_length: 500
|
max_summary_length: 500
|
||||||
pre_scan_depth: 600
|
pre_scan_depth: 600
|
||||||
|
|
@ -96,7 +84,6 @@ summary_settings:
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 5. LLM SETTINGS
|
# 5. LLM SETTINGS
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Steuerzeichen und Patterns zur Bereinigung der LLM-Antworten.
|
|
||||||
llm_settings:
|
llm_settings:
|
||||||
cleanup_patterns: ["<s>", "</s>", "[OUT]", "[/OUT]", "```json", "```"]
|
cleanup_patterns: ["<s>", "</s>", "[OUT]", "[/OUT]", "```json", "```"]
|
||||||
|
|
||||||
|
|
@ -108,8 +95,7 @@ types:
|
||||||
|
|
||||||
experience:
|
experience:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: sliding_smart_edges
|
||||||
retriever_weight: 1.10 # Erhöht für biografische Relevanz
|
retriever_weight: 1.10
|
||||||
edge_defaults: ["derived_from", "references"]
|
|
||||||
detection_keywords: ["erleben", "reagieren", "handeln", "prägen", "reflektieren"]
|
detection_keywords: ["erleben", "reagieren", "handeln", "prägen", "reflektieren"]
|
||||||
schema:
|
schema:
|
||||||
- "Situation (Was ist passiert?)"
|
- "Situation (Was ist passiert?)"
|
||||||
|
|
@ -119,8 +105,7 @@ types:
|
||||||
|
|
||||||
insight:
|
insight:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: sliding_smart_edges
|
||||||
retriever_weight: 1.20 # Hoch gewichtet für aktuelle Steuerung
|
retriever_weight: 1.20
|
||||||
edge_defaults: ["references", "based_on"]
|
|
||||||
detection_keywords: ["beobachten", "erkennen", "verstehen", "analysieren", "schlussfolgern"]
|
detection_keywords: ["beobachten", "erkennen", "verstehen", "analysieren", "schlussfolgern"]
|
||||||
schema:
|
schema:
|
||||||
- "Beobachtung (Was sehe ich?)"
|
- "Beobachtung (Was sehe ich?)"
|
||||||
|
|
@ -131,7 +116,6 @@ types:
|
||||||
project:
|
project:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: sliding_smart_edges
|
||||||
retriever_weight: 0.97
|
retriever_weight: 0.97
|
||||||
edge_defaults: ["references", "depends_on"]
|
|
||||||
detection_keywords: ["umsetzen", "planen", "starten", "bauen", "abschließen"]
|
detection_keywords: ["umsetzen", "planen", "starten", "bauen", "abschließen"]
|
||||||
schema:
|
schema:
|
||||||
- "Mission & Zielsetzung"
|
- "Mission & Zielsetzung"
|
||||||
|
|
@ -141,7 +125,6 @@ types:
|
||||||
decision:
|
decision:
|
||||||
chunking_profile: structured_smart_edges_strict
|
chunking_profile: structured_smart_edges_strict
|
||||||
retriever_weight: 1.00
|
retriever_weight: 1.00
|
||||||
edge_defaults: ["caused_by", "references"]
|
|
||||||
detection_keywords: ["entscheiden", "wählen", "abwägen", "priorisieren", "festlegen"]
|
detection_keywords: ["entscheiden", "wählen", "abwägen", "priorisieren", "festlegen"]
|
||||||
schema:
|
schema:
|
||||||
- "Kontext & Problemstellung"
|
- "Kontext & Problemstellung"
|
||||||
|
|
@ -149,12 +132,9 @@ types:
|
||||||
- "Die Entscheidung"
|
- "Die Entscheidung"
|
||||||
- "Begründung"
|
- "Begründung"
|
||||||
|
|
||||||
# --- PERSÖNLICHKEIT & IDENTITÄT ---
|
|
||||||
|
|
||||||
value:
|
value:
|
||||||
chunking_profile: structured_smart_edges_strict
|
chunking_profile: structured_smart_edges_strict
|
||||||
retriever_weight: 1.00
|
retriever_weight: 1.00
|
||||||
edge_defaults: ["related_to"]
|
|
||||||
detection_keywords: ["werten", "achten", "verpflichten", "bedeuten"]
|
detection_keywords: ["werten", "achten", "verpflichten", "bedeuten"]
|
||||||
schema:
|
schema:
|
||||||
- "Definition"
|
- "Definition"
|
||||||
|
|
@ -164,7 +144,6 @@ types:
|
||||||
principle:
|
principle:
|
||||||
chunking_profile: structured_smart_edges_strict_L3
|
chunking_profile: structured_smart_edges_strict_L3
|
||||||
retriever_weight: 0.95
|
retriever_weight: 0.95
|
||||||
edge_defaults: ["derived_from", "references"]
|
|
||||||
detection_keywords: ["leiten", "steuern", "ausrichten", "handhaben"]
|
detection_keywords: ["leiten", "steuern", "ausrichten", "handhaben"]
|
||||||
schema:
|
schema:
|
||||||
- "Das Prinzip"
|
- "Das Prinzip"
|
||||||
|
|
@ -173,7 +152,6 @@ types:
|
||||||
trait:
|
trait:
|
||||||
chunking_profile: structured_smart_edges_strict
|
chunking_profile: structured_smart_edges_strict
|
||||||
retriever_weight: 1.10
|
retriever_weight: 1.10
|
||||||
edge_defaults: ["related_to"]
|
|
||||||
detection_keywords: ["begeistern", "können", "auszeichnen", "befähigen", "stärken"]
|
detection_keywords: ["begeistern", "können", "auszeichnen", "befähigen", "stärken"]
|
||||||
schema:
|
schema:
|
||||||
- "Eigenschaft / Talent"
|
- "Eigenschaft / Talent"
|
||||||
|
|
@ -183,7 +161,6 @@ types:
|
||||||
obstacle:
|
obstacle:
|
||||||
chunking_profile: structured_smart_edges_strict
|
chunking_profile: structured_smart_edges_strict
|
||||||
retriever_weight: 1.00
|
retriever_weight: 1.00
|
||||||
edge_defaults: ["blocks", "related_to"]
|
|
||||||
detection_keywords: ["blockieren", "fürchten", "vermeiden", "hindern", "zweifeln"]
|
detection_keywords: ["blockieren", "fürchten", "vermeiden", "hindern", "zweifeln"]
|
||||||
schema:
|
schema:
|
||||||
- "Beschreibung der Hürde"
|
- "Beschreibung der Hürde"
|
||||||
|
|
@ -194,7 +171,6 @@ types:
|
||||||
belief:
|
belief:
|
||||||
chunking_profile: sliding_short
|
chunking_profile: sliding_short
|
||||||
retriever_weight: 0.90
|
retriever_weight: 0.90
|
||||||
edge_defaults: ["related_to"]
|
|
||||||
detection_keywords: ["glauben", "meinen", "annehmen", "überzeugen"]
|
detection_keywords: ["glauben", "meinen", "annehmen", "überzeugen"]
|
||||||
schema:
|
schema:
|
||||||
- "Der Glaubenssatz"
|
- "Der Glaubenssatz"
|
||||||
|
|
@ -203,18 +179,15 @@ types:
|
||||||
profile:
|
profile:
|
||||||
chunking_profile: structured_smart_edges_strict
|
chunking_profile: structured_smart_edges_strict
|
||||||
retriever_weight: 0.70
|
retriever_weight: 0.70
|
||||||
edge_defaults: ["references", "related_to"]
|
|
||||||
detection_keywords: ["verkörpern", "verantworten", "agieren", "repräsentieren"]
|
detection_keywords: ["verkörpern", "verantworten", "agieren", "repräsentieren"]
|
||||||
schema:
|
schema:
|
||||||
- "Rolle / Identität"
|
- "Rolle / Identität"
|
||||||
- "Fakten & Daten"
|
- "Fakten & Daten"
|
||||||
- "Historie"
|
- "Historie"
|
||||||
|
|
||||||
|
|
||||||
idea:
|
idea:
|
||||||
chunking_profile: sliding_short
|
chunking_profile: sliding_short
|
||||||
retriever_weight: 0.70
|
retriever_weight: 0.70
|
||||||
edge_defaults: ["leads_to", "references"]
|
|
||||||
detection_keywords: ["einfall", "gedanke", "potenzial", "möglichkeit"]
|
detection_keywords: ["einfall", "gedanke", "potenzial", "möglichkeit"]
|
||||||
schema:
|
schema:
|
||||||
- "Der Kerngedanke"
|
- "Der Kerngedanke"
|
||||||
|
|
@ -224,7 +197,6 @@ types:
|
||||||
skill:
|
skill:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: sliding_smart_edges
|
||||||
retriever_weight: 0.90
|
retriever_weight: 0.90
|
||||||
edge_defaults: ["references", "related_to"]
|
|
||||||
detection_keywords: ["lernen", "beherrschen", "üben", "fertigkeit", "kompetenz"]
|
detection_keywords: ["lernen", "beherrschen", "üben", "fertigkeit", "kompetenz"]
|
||||||
schema:
|
schema:
|
||||||
- "Definition der Fähigkeit"
|
- "Definition der Fähigkeit"
|
||||||
|
|
@ -234,7 +206,6 @@ types:
|
||||||
habit:
|
habit:
|
||||||
chunking_profile: sliding_short
|
chunking_profile: sliding_short
|
||||||
retriever_weight: 0.85
|
retriever_weight: 0.85
|
||||||
edge_defaults: ["related_to", "triggered_by"]
|
|
||||||
detection_keywords: ["gewohnheit", "routine", "automatismus", "immer wenn"]
|
detection_keywords: ["gewohnheit", "routine", "automatismus", "immer wenn"]
|
||||||
schema:
|
schema:
|
||||||
- "Auslöser (Trigger)"
|
- "Auslöser (Trigger)"
|
||||||
|
|
@ -245,7 +216,6 @@ types:
|
||||||
need:
|
need:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: sliding_smart_edges
|
||||||
retriever_weight: 1.05
|
retriever_weight: 1.05
|
||||||
edge_defaults: ["related_to", "impacts"]
|
|
||||||
detection_keywords: ["bedürfnis", "brauchen", "mangel", "erfüllung"]
|
detection_keywords: ["bedürfnis", "brauchen", "mangel", "erfüllung"]
|
||||||
schema:
|
schema:
|
||||||
- "Das Bedürfnis"
|
- "Das Bedürfnis"
|
||||||
|
|
@ -255,7 +225,6 @@ types:
|
||||||
motivation:
|
motivation:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: sliding_smart_edges
|
||||||
retriever_weight: 0.95
|
retriever_weight: 0.95
|
||||||
edge_defaults: ["drives", "references"]
|
|
||||||
detection_keywords: ["motivation", "antrieb", "warum", "energie"]
|
detection_keywords: ["motivation", "antrieb", "warum", "energie"]
|
||||||
schema:
|
schema:
|
||||||
- "Der Antrieb"
|
- "Der Antrieb"
|
||||||
|
|
@ -265,86 +234,68 @@ types:
|
||||||
bias:
|
bias:
|
||||||
chunking_profile: sliding_short
|
chunking_profile: sliding_short
|
||||||
retriever_weight: 0.80
|
retriever_weight: 0.80
|
||||||
edge_defaults: ["affects", "related_to"]
|
|
||||||
detection_keywords: ["denkfehler", "verzerrung", "vorurteil", "falle"]
|
detection_keywords: ["denkfehler", "verzerrung", "vorurteil", "falle"]
|
||||||
schema: ["Beschreibung der Verzerrung", "Typische Situationen", "Gegenstrategie"]
|
schema: ["Beschreibung der Verzerrung", "Typische Situationen", "Gegenstrategie"]
|
||||||
|
|
||||||
state:
|
state:
|
||||||
chunking_profile: sliding_short
|
chunking_profile: sliding_short
|
||||||
retriever_weight: 0.60
|
retriever_weight: 0.60
|
||||||
edge_defaults: ["impacts"]
|
|
||||||
detection_keywords: ["stimmung", "energie", "gefühl", "verfassung"]
|
detection_keywords: ["stimmung", "energie", "gefühl", "verfassung"]
|
||||||
schema: ["Aktueller Zustand", "Auslöser", "Auswirkung auf den Tag"]
|
schema: ["Aktueller Zustand", "Auslöser", "Auswirkung auf den Tag"]
|
||||||
|
|
||||||
boundary:
|
boundary:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: sliding_smart_edges
|
||||||
retriever_weight: 0.90
|
retriever_weight: 0.90
|
||||||
edge_defaults: ["protects", "related_to"]
|
|
||||||
detection_keywords: ["grenze", "nein sagen", "limit", "schutz"]
|
detection_keywords: ["grenze", "nein sagen", "limit", "schutz"]
|
||||||
schema: ["Die Grenze", "Warum sie wichtig ist", "Konsequenz bei Verletzung"]
|
schema: ["Die Grenze", "Warum sie wichtig ist", "Konsequenz bei Verletzung"]
|
||||||
# --- STRATEGIE & RISIKO ---
|
|
||||||
|
|
||||||
goal:
|
goal:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: sliding_smart_edges
|
||||||
retriever_weight: 0.95
|
retriever_weight: 0.95
|
||||||
edge_defaults: ["depends_on", "related_to"]
|
|
||||||
schema: ["Zielzustand", "Zeitrahmen & KPIs", "Motivation"]
|
schema: ["Zielzustand", "Zeitrahmen & KPIs", "Motivation"]
|
||||||
|
|
||||||
risk:
|
risk:
|
||||||
chunking_profile: sliding_short
|
chunking_profile: sliding_short
|
||||||
retriever_weight: 0.85
|
retriever_weight: 0.85
|
||||||
edge_defaults: ["related_to", "blocks"]
|
|
||||||
detection_keywords: ["risiko", "gefahr", "bedrohung"]
|
detection_keywords: ["risiko", "gefahr", "bedrohung"]
|
||||||
schema: ["Beschreibung des Risikos", "Auswirkungen", "Gegenmaßnahmen"]
|
schema: ["Beschreibung des Risikos", "Auswirkungen", "Gegenmaßnahmen"]
|
||||||
|
|
||||||
# --- BASIS & WISSEN ---
|
|
||||||
|
|
||||||
concept:
|
concept:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: sliding_smart_edges
|
||||||
retriever_weight: 0.60
|
retriever_weight: 0.60
|
||||||
edge_defaults: ["references", "related_to"]
|
|
||||||
schema: ["Definition", "Kontext", "Verwandte Konzepte"]
|
schema: ["Definition", "Kontext", "Verwandte Konzepte"]
|
||||||
|
|
||||||
task:
|
task:
|
||||||
chunking_profile: sliding_short
|
chunking_profile: sliding_short
|
||||||
retriever_weight: 0.80
|
retriever_weight: 0.80
|
||||||
edge_defaults: ["depends_on", "part_of"]
|
|
||||||
schema: ["Aufgabe", "Kontext", "Definition of Done"]
|
schema: ["Aufgabe", "Kontext", "Definition of Done"]
|
||||||
|
|
||||||
journal:
|
journal:
|
||||||
chunking_profile: sliding_standard
|
chunking_profile: sliding_standard
|
||||||
retriever_weight: 0.80
|
retriever_weight: 0.80
|
||||||
edge_defaults: ["references", "related_to"]
|
|
||||||
schema: ["Log-Eintrag", "Gedanken"]
|
schema: ["Log-Eintrag", "Gedanken"]
|
||||||
|
|
||||||
source:
|
source:
|
||||||
chunking_profile: sliding_standard
|
chunking_profile: sliding_standard
|
||||||
retriever_weight: 0.50
|
retriever_weight: 0.50
|
||||||
edge_defaults: []
|
|
||||||
schema: ["Metadaten", "Zusammenfassung", "Zitate"]
|
schema: ["Metadaten", "Zusammenfassung", "Zitate"]
|
||||||
|
|
||||||
glossary:
|
glossary:
|
||||||
chunking_profile: sliding_short
|
chunking_profile: sliding_short
|
||||||
retriever_weight: 0.40
|
retriever_weight: 0.40
|
||||||
edge_defaults: ["related_to"]
|
|
||||||
schema: ["Begriff", "Definition"]
|
schema: ["Begriff", "Definition"]
|
||||||
|
|
||||||
person:
|
person:
|
||||||
chunking_profile: sliding_standard
|
chunking_profile: sliding_standard
|
||||||
retriever_weight: 0.50
|
retriever_weight: 0.50
|
||||||
edge_defaults: ["related_to"]
|
|
||||||
schema: ["Rolle", "Beziehung", "Kontext"]
|
schema: ["Rolle", "Beziehung", "Kontext"]
|
||||||
|
|
||||||
event:
|
event:
|
||||||
chunking_profile: sliding_standard
|
chunking_profile: sliding_standard
|
||||||
retriever_weight: 0.60
|
retriever_weight: 0.60
|
||||||
edge_defaults: ["related_to"]
|
|
||||||
schema: ["Datum & Ort", "Teilnehmer", "Ergebnisse"]
|
schema: ["Datum & Ort", "Teilnehmer", "Ergebnisse"]
|
||||||
|
|
||||||
# --- FALLBACK ---
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
chunking_profile: sliding_standard
|
chunking_profile: sliding_standard
|
||||||
retriever_weight: 1.00
|
retriever_weight: 1.00
|
||||||
edge_defaults: ["references"]
|
|
||||||
schema: ["Inhalt"]
|
schema: ["Inhalt"]
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
doc_type: glossary
|
doc_type: glossary
|
||||||
audience: all
|
audience: all
|
||||||
status: active
|
status: active
|
||||||
version: 3.1.1
|
version: 4.5.8
|
||||||
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."
|
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
|
# 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.
|
* **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.
|
* **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]`, `</s>` oder Newlines zuverlässig bereinigt, um präzises Strategie-Routing zu gewährleisten.
|
* **Ultra-robustes Intent-Parsing (WP-25b):** Regex-basierter Intent-Parser in der DecisionEngine, der Modell-Artefakte wie `[/S]`, `</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).
|
* **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.
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
doc_type: quality_assurance
|
doc_type: quality_assurance
|
||||||
audience: all
|
audience: all
|
||||||
status: active
|
status: active
|
||||||
version: 2.9.1
|
version: 4.5.8
|
||||||
context: "Qualitätsprüfung der Dokumentation für alle Rollen: Vollständigkeit, Korrektheit und Anwendbarkeit."
|
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
|
# Dokumentations-Qualitätsprüfung
|
||||||
|
|
@ -59,6 +59,8 @@ Diese Checkliste dient zur systematischen Prüfung, ob die Dokumentation alle Fr
|
||||||
### Konfiguration
|
### Konfiguration
|
||||||
- [x] **ENV-Variablen:** [Configuration Reference](../03_Technical_References/03_tech_configuration.md#1-environment-variablen-env)
|
- [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] **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] **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] **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] **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
|
### Häufige Fragen
|
||||||
- [x] **Wie strukturiere ich Notizen?** → [Knowledge Design](../01_User_Manual/01_knowledge_design.md)
|
- [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] **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 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] **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
|
### Aktualisierte Dokumente
|
||||||
|
|
||||||
1. ✅ `00_documentation_map.md` - Alle neuen Dokumente aufgenommen
|
1. ✅ `00_documentation_map.md` - Alle neuen Dokumente aufgenommen
|
||||||
2. ✅ `04_admin_operations.md` - Troubleshooting erweitert
|
2. ✅ `04_admin_operations.md` - Troubleshooting erweitert, Phase 3 Validierung dokumentiert
|
||||||
3. ✅ `05_developer_guide.md` - Modulare Struktur ergänzt
|
3. ✅ `05_developer_guide.md` - Modulare Struktur ergänzt, WP-24c Phase 3 dokumentiert
|
||||||
4. ✅ `03_tech_ingestion_pipeline.md` - Background Tasks 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
|
5. ✅ `03_tech_configuration.md` - Fehlende ENV-Variablen ergänzt, WP-24c Konfiguration dokumentiert
|
||||||
6. ✅ `00_vision_and_strategy.md` - Design-Entscheidungen ergänzt
|
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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
doc_type: user_manual
|
doc_type: user_manual
|
||||||
audience: user, mindmaster
|
audience: user, mindmaster
|
||||||
scope: chat, ui, feedback, graph
|
scope: chat, ui, feedback, graph, agentic_validation
|
||||||
status: active
|
status: active
|
||||||
version: 2.9.3
|
version: 4.5.8
|
||||||
context: "Anleitung zur Nutzung der Web-Oberfläche, der Chat-Personas, Multi-Stream RAG und des Graph Explorers."
|
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
|
# 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.
|
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:
|
Wenn du nach "Projekt Alpha" suchst, findet Mindnet auch:
|
||||||
* **Abhängigkeiten:** "Technologie X wird benötigt".
|
* **Abhängigkeiten:** "Technologie X wird benötigt".
|
||||||
* **Entscheidungen:** "Warum nutzen wir X?".
|
* **Entscheidungen:** "Warum nutzen wir X?".
|
||||||
* **Ähnliches:** "Projekt Beta war ähnlich".
|
* **Ä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):**
|
**Der Zwilling (Die Personas):**
|
||||||
Mindnet passt seinen Charakter an: Mal ist es der neutrale Bibliothekar, mal der strategische Berater, mal der empathische Spiegel.
|
Mindnet passt seinen Charakter an: Mal ist es der neutrale Bibliothekar, mal der strategische Berater, mal der empathische Spiegel.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
doc_type: user_manual
|
doc_type: user_manual
|
||||||
audience: user, author
|
audience: user, author
|
||||||
scope: vault, markdown, schema
|
scope: vault, markdown, schema, agentic_validation, note_scope
|
||||||
status: active
|
status: active
|
||||||
version: 2.9.1
|
version: 4.5.8
|
||||||
context: "Regelwerk für das Erstellen von Notizen im Vault. Die 'Source of Truth' für Autoren."
|
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
|
# Knowledge Design Manual
|
||||||
|
|
@ -238,8 +238,14 @@ Callout-Blocks mit mehreren Zeilen werden korrekt verarbeitet. Das System erkenn
|
||||||
**Format-agnostische De-Duplizierung:**
|
**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).
|
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!]
|
### 4.3 Automatische Spiegelkanten (Invers-Logik) - WP-24c v4.5.8
|
||||||
In Mindnet musst du Kanten **nicht** manuell in beide Richtungen pflegen. Der **Edger** übernimmt die Paarbildung automatisch im Hintergrund.
|
|
||||||
|
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.
|
**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`.
|
* **Blick nach vorn (Vorwärtslink):** Wenn du einen Plan oder ein Protokoll schreibst, nutze `resulted_in`, `supports` oder `next`.
|
||||||
|
|
||||||
**System-Logik (Beispiele):**
|
**System-Logik (Beispiele):**
|
||||||
- Schreibst du in Note A: `next: [[B]]`, weiß das System automatisch: `B prev A`.
|
- Schreibst du in Note A: `[[rel:next Projekt B]]`, erzeugt das System automatisch: `Projekt B prev: Note A`
|
||||||
- Schreibst du in Note B: `derived_from: [[A]]`, weiß das System automatisch: `A resulted_in B`.
|
- 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))*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
282
docs/01_User_Manual/LLM_VALIDIERUNG_VON_LINKS.md
Normal file
282
docs/01_User_Manual/LLM_VALIDIERUNG_VON_LINKS.md
Normal file
|
|
@ -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)
|
||||||
275
docs/01_User_Manual/NOTE_SCOPE_ZONEN.md
Normal file
275
docs/01_User_Manual/NOTE_SCOPE_ZONEN.md
Normal file
|
|
@ -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)
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
doc_type: concept
|
doc_type: concept
|
||||||
audience: architect, product_owner
|
audience: architect, product_owner
|
||||||
scope: graph, logic, provenance
|
scope: graph, logic, provenance, agentic_validation, note_scope
|
||||||
status: active
|
status: active
|
||||||
version: 2.9.1
|
version: 4.5.8
|
||||||
context: "Fachliche Beschreibung des Wissensgraphen: Knoten, Kanten, Provenance, Matrix-Logik, WP-15c Multigraph-Support und WP-22 Scoring-Prinzipien."
|
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
|
# 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.
|
Das System garantiert fachliche Konsistenz auch bei mehrfachen Importen.
|
||||||
* **Stabile IDs:** Deterministische IDs verhindern Duplikate bei Re-Imports.
|
* **Stabile IDs:** Deterministische IDs verhindern Duplikate bei Re-Imports.
|
||||||
* **Deduplizierung:** Kanten werden anhand ihrer Identität (inkl. Section) erkannt. Die "stärkere" Provenance gewinnt.
|
* **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.
|
* **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.
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
doc_type: technical_reference
|
doc_type: technical_reference
|
||||||
audience: developer, admin
|
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
|
status: active
|
||||||
version: 3.1.1
|
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) und WP-25b Lazy-Prompt-Orchestration unter Berücksichtigung von WP-14."
|
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
|
# 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_LL_BACKGROUND_LIMIT`| `2` | **Traffic Control:** Max. parallele Hintergrund-Tasks (Semaphore). |
|
||||||
| `MINDNET_CHANGE_DETECTION_MODE` | `full` | `full` (Text + Meta) oder `body` (nur Text). |
|
| `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_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` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
doc_type: technical_reference
|
doc_type: technical_reference
|
||||||
audience: developer, architect
|
audience: developer, architect
|
||||||
scope: database, qdrant, schema
|
scope: database, qdrant, schema, agentic_validation
|
||||||
status: active
|
status: active
|
||||||
version: 2.9.1
|
version: 4.5.8
|
||||||
context: "Exakte Definition der Datenmodelle (Payloads) in Qdrant und Index-Anforderungen. Berücksichtigt WP-14 Modularisierung und WP-15b Multi-Hashes."
|
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)
|
# 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')
|
"scope": "string (keyword)", // Immer 'chunk' (Legacy-Support: 'note')
|
||||||
"note_id": "string (keyword)", // Owner Note ID (Ursprung der Kante)
|
"note_id": "string (keyword)", // Owner Note ID (Ursprung der Kante)
|
||||||
|
|
||||||
// Provenance & Quality (WP03/WP15)
|
// Provenance & Quality (WP03/WP15/WP-24c)
|
||||||
"provenance": "keyword", // 'explicit', 'rule', 'smart', 'structure'
|
"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', 'smart:llm'
|
"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)
|
"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.
|
* 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.
|
* **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:**
|
**Erforderliche Indizes:**
|
||||||
Es müssen Payload-Indizes für folgende Felder existieren:
|
Es müssen Payload-Indizes für folgende Felder existieren:
|
||||||
* `source_id`
|
* `source_id`
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
doc_type: technical_reference
|
doc_type: technical_reference
|
||||||
audience: developer, devops
|
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
|
status: active
|
||||||
version: 2.14.0
|
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 und WP-25b Lazy-Prompt-Orchestration. Integriert Mistral-safe Parsing und Deep Fallback."
|
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
|
# 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)
|
### Phase 1: Pre-Scan & Context (Pass 1)
|
||||||
1. **Trigger & Async Dispatch:**
|
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.
|
* 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.
|
* Die Namensauflösung erfolgt nun über das modularisierte `database`-Paket.
|
||||||
10. **Chunking anwenden:** Zerlegung des Textes basierend auf dem ermittelten Profil (siehe Kap. 3).
|
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.
|
* 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.
|
* **Kandidaten-Markierung:** Alle vorgeschlagenen Kanten erhalten `candidate:` Präfix in `rule_id` oder `provenance`.
|
||||||
* **Profilgesteuerte Validierung:** Nutzt das MoE-Profil `ingest_validator` (Temperature 0.0 für maximale Determinismus).
|
* **Hinweis:** Die eigentliche LLM-Validierung erfolgt erst in **Phase 3** (siehe Schritt 17).
|
||||||
* **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 (<s>, [OUT]).
|
|
||||||
* **Deep Fallback (v2.11.14):** Erkennt "Silent Refusals". Liefert die Cloud keine verwertbaren Kanten, wird ein lokaler Fallback via Ollama erzwungen.
|
|
||||||
12. **Inline-Kanten finden:** Parsing von `[[rel:...]]` und Callouts.
|
12. **Inline-Kanten finden:** Parsing von `[[rel:...]]` und Callouts.
|
||||||
13. **Alias-Auflösung & Kanonisierung (WP-22):**
|
13. **Alias-Auflösung & Kanonisierung (WP-22):**
|
||||||
* Jede Kante wird via `EdgeRegistry` normalisiert (z.B. `basiert_auf` -> `based_on`).
|
* 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`.
|
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).
|
* **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.
|
* **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.
|
**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.
|
**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.
|
||||||
265
docs/03_Technical_References/AUDIT_CLEAN_CONTEXT_V4.2.0.md
Normal file
265
docs/03_Technical_References/AUDIT_CLEAN_CONTEXT_V4.2.0.md
Normal file
|
|
@ -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
|
||||||
131
docs/03_Technical_References/AUDIT_RETRIEVER_V4.1.0.md
Normal file
131
docs/03_Technical_References/AUDIT_RETRIEVER_V4.1.0.md
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
# Audit: Retriever & Scoring (Gold-Standard v4.1.0)
|
||||||
|
|
||||||
|
**Datum:** 2026-01-10
|
||||||
|
**Version:** v4.1.0
|
||||||
|
**Status:** Audit abgeschlossen, Optimierungen implementiert
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Das Ingestion-System wurde auf den Gold-Standard v4.1.0 aktualisiert. Die Kanten-Identität ist nun deterministisch und hochpräzise mit strikter Trennung zwischen:
|
||||||
|
|
||||||
|
- **Chunk-Scope-Edges:** Präzise Links aus Textabsätzen (Source = `chunk_id`), oft mit `target_section`
|
||||||
|
- **Note-Scope-Edges:** Strukturelle Links und Symmetrien (Source = `note_id`)
|
||||||
|
- **Multigraph-Support:** Identische Note-Verbindungen bleiben als separate Points erhalten, wenn sie auf unterschiedliche Sektionen zeigen oder aus unterschiedlichen Chunks stammen
|
||||||
|
|
||||||
|
## Prüffragen & Ergebnisse
|
||||||
|
|
||||||
|
### 1. Scope-Awareness ❌ **KRITISCH**
|
||||||
|
|
||||||
|
**Frage:** Sucht der Retriever bei einer Note-Anfrage sowohl nach Abgangskanten der `note_id` als auch nach Abgangskanten aller zugehörigen `chunk_ids`?
|
||||||
|
|
||||||
|
**Aktueller Status:**
|
||||||
|
- ❌ **NEIN**: Der Retriever sucht nur nach Edges, die von `note_id` ausgehen
|
||||||
|
- Die Graph-Expansion in `graph_db_adapter.py` filtert nur nach `source_id`, `target_id` und `note_id`
|
||||||
|
- Chunk-Level Edges (`scope="chunk"`) werden nicht explizit berücksichtigt
|
||||||
|
- **Risiko:** Datenverlust bei präzisen Chunk-Links
|
||||||
|
|
||||||
|
**Empfehlung:**
|
||||||
|
- Erweitere `fetch_edges_from_qdrant` um explizite Suche nach `chunk_id`-Edges
|
||||||
|
- Bei Note-Anfragen: Lade alle Chunks der Note und suche nach deren Edges
|
||||||
|
- Aggregiere Chunk-Edges in Note-Level Scoring
|
||||||
|
|
||||||
|
### 2. Section-Filtering ❌ **FEHLT**
|
||||||
|
|
||||||
|
**Frage:** Kann der Retriever bei einem Sektions-Link (`[[Note#Sektion]]`) die Ergebnismenge in Qdrant gezielt auf Chunks filtern, die das entsprechende `section`-Attribut im Payload tragen?
|
||||||
|
|
||||||
|
**Aktueller Status:**
|
||||||
|
- ❌ **NEIN**: Es gibt keine Filterung nach `target_section`
|
||||||
|
- `target_section` wird zwar im Edge-Payload gespeichert, aber nicht für Filterung verwendet
|
||||||
|
- **Risiko:** Unpräzise Ergebnisse bei Section-Links
|
||||||
|
|
||||||
|
**Empfehlung:**
|
||||||
|
- Erweitere `QueryRequest` um optionales `target_section` Feld
|
||||||
|
- Implementiere Filterung in `_semantic_hits` und `fetch_edges_from_qdrant`
|
||||||
|
- Nutze `target_section` für präzise Chunk-Filterung
|
||||||
|
|
||||||
|
### 3. Scoring-Aggregation ⚠️ **TEILWEISE**
|
||||||
|
|
||||||
|
**Frage:** Wie geht das Scoring damit um, wenn ein Ziel von mehreren Chunks derselben Note referenziert wird? Wird die Relevanz (In-Degree) auf Chunk-Ebene korrekt akkumuliert?
|
||||||
|
|
||||||
|
**Aktueller Status:**
|
||||||
|
- ⚠️ **TEILWEISE**: Super-Edge-Aggregation existiert (WP-15c), aber:
|
||||||
|
- Aggregiert nur nach Ziel-Note (`target_id`), nicht nach Chunk-Level
|
||||||
|
- Mehrere Chunks derselben Note, die auf dasselbe Ziel zeigen, werden nicht korrekt akkumuliert
|
||||||
|
- Die "Beweislast" (In-Degree) wird nicht auf Chunk-Ebene berechnet
|
||||||
|
- **Risiko:** Unterbewertung von Zielen, die von mehreren Chunks referenziert werden
|
||||||
|
|
||||||
|
**Empfehlung:**
|
||||||
|
- Erweitere Super-Edge-Aggregation um Chunk-Level Tracking
|
||||||
|
- Berechne In-Degree sowohl auf Note- als auch auf Chunk-Ebene
|
||||||
|
- Nutze Chunk-Level In-Degree als zusätzlichen Boost-Faktor
|
||||||
|
|
||||||
|
### 4. Authority-Priorisierung ⚠️ **TEILWEISE**
|
||||||
|
|
||||||
|
**Frage:** Nutzt das Scoring das Feld `provenance_priority` oder `confidence`, um manuelle "Explicit"-Kanten gegenüber "Virtual"-Symmetrien bei der Sortierung zu bevorzugen?
|
||||||
|
|
||||||
|
**Aktueller Status:**
|
||||||
|
- ⚠️ **TEILWEISE**:
|
||||||
|
- Provenance-Weighting existiert (Zeile 344-345 in `retriever.py`)
|
||||||
|
- Nutzt aber nicht `confidence` oder `provenance_priority` aus dem Payload
|
||||||
|
- Hardcoded Gewichtung: `explicit=1.0`, `smart=0.9`, `rule=0.7`
|
||||||
|
- `virtual` Flag wird nicht berücksichtigt
|
||||||
|
- **Risiko:** Virtual-Symmetrien werden nicht korrekt de-priorisiert
|
||||||
|
|
||||||
|
**Empfehlung:**
|
||||||
|
- Nutze `confidence` aus dem Edge-Payload
|
||||||
|
- Berücksichtige `virtual` Flag für explizite De-Priorisierung
|
||||||
|
- Integriere `PROVENANCE_PRIORITY` aus `graph_utils.py` statt Hardcoding
|
||||||
|
|
||||||
|
### 5. RAG-Kontext ❌ **FEHLT**
|
||||||
|
|
||||||
|
**Frage:** Wird beim Retrieval einer Kante der `source_id` (Chunk) direkt mitgeliefert, damit das LLM den exakten Herkunfts-Kontext der Verbindung erhält?
|
||||||
|
|
||||||
|
**Aktueller Status:**
|
||||||
|
- ❌ **NEIN**: `source_id` (Chunk-ID) wird nicht explizit im `QueryHit` mitgeliefert
|
||||||
|
- Edge-Payload enthält `source_id`, aber es wird nicht in den RAG-Kontext übernommen
|
||||||
|
- **Risiko:** LLM erhält keinen Kontext über die Herkunft der Verbindung
|
||||||
|
|
||||||
|
**Empfehlung:**
|
||||||
|
- Erweitere `QueryHit` um `source_chunk_id` Feld
|
||||||
|
- Bei Chunk-Scope Edges: Lade den Quell-Chunk-Text für RAG-Kontext
|
||||||
|
- Integriere Chunk-Kontext in Explanation Layer
|
||||||
|
|
||||||
|
## Implementierte Optimierungen
|
||||||
|
|
||||||
|
Siehe: `app/core/retrieval/retriever.py` (v0.8.0) und `app/core/graph/graph_db_adapter.py` (v1.2.0)
|
||||||
|
|
||||||
|
### Änderungen
|
||||||
|
|
||||||
|
1. **Scope-Aware Edge Retrieval**
|
||||||
|
- `fetch_edges_from_qdrant` sucht nun explizit nach `chunk_id`-Edges
|
||||||
|
- Bei Note-Anfragen werden alle zugehörigen Chunks geladen
|
||||||
|
|
||||||
|
2. **Section-Filtering**
|
||||||
|
- `QueryRequest` unterstützt optionales `target_section` Feld
|
||||||
|
- Filterung in `_semantic_hits` und Edge-Retrieval implementiert
|
||||||
|
|
||||||
|
3. **Chunk-Level Aggregation**
|
||||||
|
- Super-Edge-Aggregation erweitert um Chunk-Level Tracking
|
||||||
|
- In-Degree wird sowohl auf Note- als auch Chunk-Ebene berechnet
|
||||||
|
|
||||||
|
4. **Authority-Priorisierung**
|
||||||
|
- Nutzung von `confidence` und `PROVENANCE_PRIORITY`
|
||||||
|
- `virtual` Flag wird für De-Priorisierung berücksichtigt
|
||||||
|
|
||||||
|
5. **RAG-Kontext**
|
||||||
|
- `QueryHit` erweitert um `source_chunk_id`
|
||||||
|
- Chunk-Kontext wird in Explanation Layer integriert
|
||||||
|
|
||||||
|
## Validierung
|
||||||
|
|
||||||
|
- ✅ Scope-Awareness: Note- und Chunk-Edges werden korrekt geladen
|
||||||
|
- ✅ Section-Filtering: Präzise Filterung nach `target_section` funktioniert
|
||||||
|
- ✅ Scoring-Aggregation: Chunk-Level In-Degree wird korrekt akkumuliert
|
||||||
|
- ✅ Authority-Priorisierung: Explicit-Kanten werden bevorzugt
|
||||||
|
- ✅ RAG-Kontext: `source_chunk_id` wird mitgeliefert
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
1. Performance-Tests mit großen Vaults
|
||||||
|
2. Integration in Decision Engine
|
||||||
|
3. Dokumentation der neuen Features
|
||||||
510
docs/03_Technical_References/AUDIT_SYSTEM_INTEGRITY_V4.5.8.md
Normal file
510
docs/03_Technical_References/AUDIT_SYSTEM_INTEGRITY_V4.5.8.md
Normal file
|
|
@ -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
|
||||||
242
docs/03_Technical_References/KONFIGURATION_EDGE_ZONEN.md
Normal file
242
docs/03_Technical_References/KONFIGURATION_EDGE_ZONEN.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
doc_type: operations_manual
|
doc_type: operations_manual
|
||||||
audience: admin, devops
|
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
|
status: active
|
||||||
version: 3.1.1
|
version: 4.5.8
|
||||||
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."
|
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
|
# 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
|
1. Füge fehlende Typen als Aliase in `01_edge_vocabulary.md` hinzu
|
||||||
2. Oder verwende kanonische Typen aus der Registry
|
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
|
#### Performance-Optimierung
|
||||||
|
|
||||||
**Problem: Langsame Chat-Antworten**
|
**Problem: Langsame Chat-Antworten**
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
doc_type: developer_guide
|
doc_type: developer_guide
|
||||||
audience: developer
|
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
|
status: active
|
||||||
version: 3.1.1
|
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, Modul-Interna, Setup und Git-Workflow."
|
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
|
# 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/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/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/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/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/retrieval/`** | Suche & Scoring | `retriever.py` (Orchestrator), `retriever_scoring.py` (Mathematik) |
|
||||||
| **`app/core/registry.py`** | SSOT & Utilities | Text-Bereinigung, Circular-Import-Fix |
|
| **`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.
|
*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
|
### Workflow B: Graph-Farben ändern
|
||||||
1. Öffne `app/frontend/ui_config.py`.
|
1. Öffne `app/frontend/ui_config.py`.
|
||||||
2. Bearbeite das Dictionary `GRAPH_COLORS`.
|
2. Bearbeite das Dictionary `GRAPH_COLORS`.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
doc_type: developer_guide
|
doc_type: developer_guide
|
||||||
audience: developer, tester
|
audience: developer, tester
|
||||||
scope: testing, quality_assurance, test_strategies
|
scope: testing, quality_assurance, test_strategies, agentic_validation
|
||||||
status: active
|
status: active
|
||||||
version: 2.9.3
|
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."
|
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
|
# Testing Guide
|
||||||
|
|
@ -272,16 +272,26 @@ class TestIngest(unittest.IsolatedAsyncioTestCase):
|
||||||
### 4.5 Ingestion-Tests
|
### 4.5 Ingestion-Tests
|
||||||
|
|
||||||
**Was wird getestet:**
|
**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)
|
- Change Detection (Hash-basiert)
|
||||||
- Background Tasks
|
- Background Tasks
|
||||||
- Smart Edge Allocation
|
- Smart Edge Allocation
|
||||||
|
- Automatische Spiegelkanten (Invers-Logik)
|
||||||
|
|
||||||
**Tests:**
|
**Tests:**
|
||||||
- `tests/test_dialog_full_flow.py`
|
- `tests/test_dialog_full_flow.py`
|
||||||
- `tests/test_WP22_intelligence.py`
|
- `tests/test_WP22_intelligence.py`
|
||||||
- `scripts/import_markdown.py` (mit `--dry-run`)
|
- `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
|
## 5. Continuous Integration
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
doc_type: roadmap
|
doc_type: roadmap
|
||||||
audience: product_owner, developer
|
audience: product_owner, developer
|
||||||
status: active
|
status: active
|
||||||
version: 3.1.1
|
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."
|
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
|
# Mindnet Active Roadmap
|
||||||
|
|
||||||
**Aktueller Stand:** v3.1.1 (Post-WP25b: Lazy-Prompt-Orchestration & Full Resilience)
|
**Aktueller Stand:** v4.5.8 (Post-WP24c: Phase 3 Agentic Edge Validation - Integrity Baseline)
|
||||||
**Fokus:** Hierarchische Prompt-Resolution, Modell-spezifisches Tuning & maximale Resilienz.
|
**Fokus:** Chunk-Aware Multigraph-System, Agentic Edge Validation, Graph-Qualitätssicherung.
|
||||||
|
|
||||||
| Phase | Fokus | Status |
|
| 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-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-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-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
|
### 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.
|
* **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
|
- Kontext-Budgeting: Intelligente Token-Verteilung
|
||||||
- Stream-specific Provider: Unterschiedliche KI-Modelle pro Wissensbereich
|
- Stream-specific Provider: Unterschiedliche KI-Modelle pro Wissensbereich
|
||||||
- Erweiterte Prompt-Optimierung: Dynamische Anpassung basierend auf Kontext und Historie
|
- 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
|
### WP-24 – Proactive Discovery & Agentic Knowledge Mining
|
||||||
|
|
|
||||||
113
docs/99_Archive/WP24c_merge_commit.md
Normal file
113
docs/99_Archive/WP24c_merge_commit.md
Normal file
|
|
@ -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
|
||||||
407
docs/99_Archive/WP24c_release_notes.md
Normal file
407
docs/99_Archive/WP24c_release_notes.md
Normal file
|
|
@ -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).
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
doc_type: documentation_index
|
doc_type: documentation_index
|
||||||
audience: all
|
audience: all
|
||||||
status: active
|
status: active
|
||||||
version: 3.1.1
|
version: 4.5.8
|
||||||
context: "Zentraler Einstiegspunkt für die Mindnet-Dokumentation"
|
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
|
# 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
|
## 🚀 Schnellstart
|
||||||
|
|
||||||
|
|
@ -98,6 +98,10 @@ Historische Dokumentation:
|
||||||
| Frage | Dokument |
|
| Frage | Dokument |
|
||||||
|-------|----------|
|
|-------|----------|
|
||||||
| Wie starte ich mit Mindnet? | [Schnellstart](00_General/00_quickstart.md) |
|
| 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 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 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) |
|
| 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
|
**Letzte Aktualisierung:** 2025-01-XX
|
||||||
**Version:** 2.9.1
|
**Version:** 4.5.8 (WP-24c: Phase 3 Agentic Edge Validation - Integrity Baseline)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,8 @@ async def analyze_file(file_path: str):
|
||||||
"chunk_id": chunk.id,
|
"chunk_id": chunk.id,
|
||||||
"type": "concept"
|
"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']]
|
found_explicitly = [f"{e['kind']}:{e.get('target_id')}" for e in edges if e['rule_id'] in ['callout:edge', 'inline:rel']]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,11 +129,13 @@ def main():
|
||||||
chunks = _simple_chunker(parsed.body, note_id, note_type)
|
chunks = _simple_chunker(parsed.body, note_id, note_type)
|
||||||
note_refs = _fm_note_refs(fm)
|
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(
|
edges = build_edges_for_note(
|
||||||
note_id=note_id,
|
note_id=note_id,
|
||||||
chunks=chunks,
|
chunks=chunks,
|
||||||
note_level_references=note_refs,
|
note_level_references=note_refs,
|
||||||
include_note_scope_refs=include_note_scope,
|
include_note_scope_refs=include_note_scope,
|
||||||
|
markdown_body=parsed.body if parsed else None,
|
||||||
)
|
)
|
||||||
kinds = {}
|
kinds = {}
|
||||||
for e in edges:
|
for e in edges:
|
||||||
|
|
|
||||||
|
|
@ -2,190 +2,264 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
FILE: scripts/import_markdown.py
|
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)
|
STATUS: Active (Core)
|
||||||
COMPATIBILITY: v2.9.1 (Post-WP14/WP-15b)
|
COMPATIBILITY: IngestionProcessor v4.0.0+, graph_utils v4.1.0+
|
||||||
|
|
||||||
Zweck:
|
Zweck:
|
||||||
-------
|
-------
|
||||||
Hauptwerkzeug zum Importieren von Markdown-Dateien aus einem Vault in Qdrant.
|
Hauptwerkzeug zum Importieren von Markdown-Dateien aus einem lokalen Obsidian-Vault in die
|
||||||
Implementiert den Two-Pass Workflow (WP-15b) für robuste Edge-Validierung.
|
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:
|
Hintergrund der 2-Phasen-Schreibstrategie (Authority-First):
|
||||||
---------------
|
------------------------------------------------------------
|
||||||
1. PASS 1: Global Pre-Scan
|
Um das Problem der "Ghost-IDs" (Links auf Titel statt IDs) und der asynchronen Überschreibungen
|
||||||
- Scannt alle Markdown-Dateien im Vault
|
(Symmetrien löschen manuelle Kanten) zu lösen, implementiert dieses Script eine strikte
|
||||||
- Extrahiert Note-Kontext (ID, Titel, Dateiname)
|
Trennung der Arbeitsabläufe:
|
||||||
- Füllt LocalBatchCache für semantische Edge-Validierung
|
|
||||||
- Indiziert nach ID, Titel und Dateiname für Link-Auflösung
|
|
||||||
|
|
||||||
2. PASS 2: Semantic Processing
|
1. PASS 1: Global Context Discovery (Pre-Scan)
|
||||||
- Verarbeitet Dateien in Batches (20 Dateien, max. 5 parallel)
|
- Scannt den gesamten Vault, um ein Mapping von Titeln/Dateinamen zu Note-IDs aufzubauen.
|
||||||
- Nutzt gefüllten Cache für binäre Edge-Validierung
|
- Dieser Cache wird dem IngestionService übergeben, damit Wikilinks wie [[Klaus]]
|
||||||
- Erzeugt Notes, Chunks und Edges in Qdrant
|
während der Verarbeitung sofort in die korrekte Zeitstempel-ID (z.B. 202601031726-klaus)
|
||||||
- Respektiert Hash-basierte Change Detection
|
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:
|
Ergebnis-Interpretation:
|
||||||
------------------------
|
------------------------
|
||||||
- Log-Ausgabe: Fortschritt und Statistiken
|
- Log-Ausgabe: Zeigt detailliert den Fortschritt, LLM-Entscheidungen (✅ OK / ❌ SKIP)
|
||||||
- Stats: processed, skipped, errors
|
und den Status der Symmetrie-Injektion.
|
||||||
- Exit-Code 0: Erfolgreich (auch wenn einzelne Dateien Fehler haben)
|
- Statistiken: Gibt am Ende eine Zusammenfassung über Erfolg, Übersprungene (Hash identisch)
|
||||||
- Ohne --apply: Dry-Run (keine DB-Änderungen)
|
und Fehler (z.B. fehlendes Frontmatter).
|
||||||
|
|
||||||
Verwendung:
|
Verwendung:
|
||||||
-----------
|
-----------
|
||||||
- Regelmäßiger Import nach Vault-Änderungen
|
- Initialer Aufbau: python3 -m scripts.import_markdown --vault /pfad/zum/vault --apply
|
||||||
- Initial-Import eines neuen Vaults
|
- Update-Lauf: Das Script erkennt Änderungen automatisch via Change Detection.
|
||||||
- Re-Indexierung mit --force
|
- Erzwingung: Mit --force wird die Hash-Prüfung ignoriert und alles neu indiziert.
|
||||||
|
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Any
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Setzt das Level global auf INFO, damit der Fortschritt im Log sichtbar ist
|
# WP-24c v4.5.9: Lade .env VOR dem Logging-Setup, damit DEBUG=true korrekt gelesen wird
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
load_dotenv()
|
||||||
|
|
||||||
# Importiere den neuen Async Service und stelle Python-Pfad sicher
|
# Root Logger Setup: Nutzt zentrale setup_logging() Funktion
|
||||||
import sys
|
# 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())
|
sys.path.append(os.getcwd())
|
||||||
|
|
||||||
|
# App-spezifische Imports
|
||||||
from app.core.ingestion import IngestionService
|
from app.core.ingestion import IngestionService
|
||||||
from app.core.parser import pre_scan_markdown
|
from app.core.parser import pre_scan_markdown
|
||||||
|
|
||||||
logger = logging.getLogger("importer")
|
logger = logging.getLogger("importer")
|
||||||
|
|
||||||
async def main_async(args):
|
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()
|
vault_path = Path(args.vault).resolve()
|
||||||
if not vault_path.exists():
|
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
|
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})")
|
logger.info(f"Initializing IngestionService (Prefix: {args.prefix})")
|
||||||
service = IngestionService(collection_prefix=args.prefix)
|
service = IngestionService(collection_prefix=args.prefix)
|
||||||
|
|
||||||
logger.info(f"Scanning {vault_path}...")
|
logger.info(f"Scanning {vault_path}...")
|
||||||
files = list(vault_path.rglob("*.md"))
|
all_files_raw = list(vault_path.rglob("*.md"))
|
||||||
# Exclude .obsidian folder if present
|
|
||||||
files = [f for f in files if ".obsidian" not in str(f)]
|
|
||||||
files.sort()
|
|
||||||
|
|
||||||
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)
|
# PASS 1: Global Pre-Scan
|
||||||
# Füllt den LocalBatchCache für die semantische Kanten-Validierung.
|
# Ziel: Aufbau eines vollständigen Mappings von Bezeichnungen zu stabilen IDs.
|
||||||
# Nutzt ID, Titel und Filename für robusten Look-up.
|
# 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:
|
for f_path in files:
|
||||||
try:
|
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:
|
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
|
service.batch_cache[ctx.note_id] = ctx
|
||||||
|
|
||||||
# 2. Look-up via Titel (Wichtig für Wikilinks [[Titel]])
|
|
||||||
service.batch_cache[ctx.title] = ctx
|
service.batch_cache[ctx.title] = ctx
|
||||||
|
# Auch den Dateinamen ohne Endung als Alias hinterlegen
|
||||||
# 3. Look-up via Dateiname (Wichtig für Wikilinks [[Filename]])
|
service.batch_cache[os.path.splitext(f_path.name)[0]] = ctx
|
||||||
fname = os.path.splitext(f_path.name)[0]
|
|
||||||
service.batch_cache[fname] = ctx
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"⚠️ Could not pre-scan {f_path.name}: {e}")
|
logger.warning(f"⚠️ Pre-scan fehlgeschlagen für {f_path.name}: {e}")
|
||||||
|
|
||||||
logger.info(f"✅ Context Cache populated for {len(files)} notes.")
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# PASS 2: Processing (Semantic Batch-Verarbeitung)
|
# PHASE 1: Authority Processing (Batch-Lauf)
|
||||||
# Nutzt den gefüllten Cache zur binären Validierung semantischer Kanten.
|
# Ziel: Verarbeitung der Dateiinhalte und Speicherung der Nutzer-Autorität.
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
stats = {"processed": 0, "skipped": 0, "errors": 0}
|
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):
|
async def process_with_limit(f_path):
|
||||||
|
"""Kapselt den Prozess-Aufruf mit Ressourcen-Limitierung."""
|
||||||
async with sem:
|
async with sem:
|
||||||
try:
|
try:
|
||||||
# Nutzt den nun gefüllten Batch-Cache in der process_file Logik
|
# Verwendet process_file (v3.4.2), das explizite Kanten sofort schreibt.
|
||||||
res = await service.process_file(
|
# Symmetrien werden im Service-Puffer gesammelt und NICHT sofort geschrieben.
|
||||||
|
return await service.process_file(
|
||||||
file_path=str(f_path),
|
file_path=str(f_path),
|
||||||
vault_root=str(vault_path),
|
vault_root=str(vault_path),
|
||||||
force_replace=args.force,
|
force_replace=args.force,
|
||||||
apply=args.apply,
|
apply=args.apply,
|
||||||
purge_before=True
|
purge_before=True
|
||||||
)
|
)
|
||||||
return res
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"status": "error", "error": str(e), "path": str(f_path)}
|
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
|
batch_size = 20
|
||||||
for i in range(0, len(files), batch_size):
|
for i in range(0, len(files), batch_size):
|
||||||
batch = files[i:i+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]
|
tasks = [process_with_limit(f) for f in batch]
|
||||||
results = await asyncio.gather(*tasks)
|
results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
for res in results:
|
for res in results:
|
||||||
if res.get("status") == "success":
|
# Robuste Auswertung der Rückgabe-Dictionaries
|
||||||
stats["processed"] += 1
|
if not isinstance(res, dict):
|
||||||
elif res.get("status") == "error":
|
|
||||||
stats["errors"] += 1
|
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:
|
else:
|
||||||
stats["skipped"] += 1
|
stats["skipped"] += 1
|
||||||
|
|
||||||
logger.info(f"Done. Stats: {stats}")
|
# =========================================================================
|
||||||
if not args.apply:
|
# PHASE 2: Global Symmetry Commitment
|
||||||
logger.info("DRY RUN. Use --apply to write to DB.")
|
# 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():
|
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")
|
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 = argparse.ArgumentParser(description="Mindnet Ingester: Two-Phase Markdown Import")
|
||||||
parser.add_argument("--vault", default="./vault", help="Path to vault root")
|
parser.add_argument("--vault", default=default_vault, help="Pfad zum Obsidian Vault")
|
||||||
parser.add_argument("--prefix", default=default_prefix, help="Collection prefix")
|
parser.add_argument("--prefix", default=default_prefix, help="Qdrant Collection Präfix")
|
||||||
parser.add_argument("--force", action="store_true", help="Force re-index all files")
|
parser.add_argument("--force", action="store_true", help="Erzwingt Neu-Indizierung aller Dateien")
|
||||||
parser.add_argument("--apply", action="store_true", help="Perform writes to Qdrant")
|
parser.add_argument("--apply", action="store_true", help="Schreibt physisch in die Datenbank")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Starte den asynchronen Haupt-Loop
|
try:
|
||||||
asyncio.run(main_async(args))
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
@ -138,11 +138,13 @@ async def process_file(path: str, root: str, args):
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.with_edges:
|
if args.with_edges:
|
||||||
|
# WP-24c v4.2.0: Übergabe des Markdown-Bodys für Note-Scope Zonen
|
||||||
edges = build_edges_for_note(
|
edges = build_edges_for_note(
|
||||||
note_id=note_pl.get("note_id") or fm.get("id"),
|
note_id=note_pl.get("note_id") or fm.get("id"),
|
||||||
chunks=chunk_pls,
|
chunks=chunk_pls,
|
||||||
note_level_references=note_pl.get("references") or [],
|
note_level_references=note_pl.get("references") or [],
|
||||||
include_note_scope_refs=False,
|
include_note_scope_refs=False,
|
||||||
|
markdown_body=body_text,
|
||||||
)
|
)
|
||||||
kinds = {}
|
kinds = {}
|
||||||
for e in edges:
|
for e in edges:
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,8 @@ def main():
|
||||||
edge_error = None
|
edge_error = None
|
||||||
edges_count = 0
|
edges_count = 0
|
||||||
try:
|
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)
|
edges_count = len(edges)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
edge_error = f"{type(e).__name__}: {e}"
|
edge_error = f"{type(e).__name__}: {e}"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user