Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d61a9e191 | |||
| 55d1a7e290 | |||
| 4537e65428 | |||
| 43327c1f6d | |||
| 39a6998123 | |||
| 273c4c6919 | |||
| 2ed4488cf6 | |||
| 36490425c5 | |||
| b8cb8bb89b | |||
| 6d268d9dfb | |||
| df5f9b3fe4 | |||
| 5e67cd470c | |||
| 0b2a1f1a63 | |||
| d0012355b9 | |||
| 1056078e6a | |||
| c42a76b3d7 | |||
| ec9b3c68af | |||
| f9118a36f8 | |||
| e52eed40ca | |||
| 43641441ef | |||
| c613d81846 | |||
| de5db09b51 | |||
| 7cb8fd6602 | |||
| 6047e94964 | |||
| 78fbc9b31b | |||
| 742792770c | |||
| b19f91c3ee | |||
| 9b0d8c18cb | |||
| f2a2f4d2df | |||
| ea0fd951f2 | |||
| c8c828c8a8 | |||
| 716a063849 | |||
| 3dc81ade0f | |||
| 1df89205ac | |||
| 2445f7cb2b | |||
| 47fdcf8eed | |||
| 3e27c72b80 | |||
| 2d87f9d816 | |||
| d7d6155203 | |||
| f8506c0bb2 | |||
| c91910ee9f | |||
| ee91583614 | |||
| 3a17b646e1 | |||
| 727de50290 | |||
| a780104b3c | |||
| f51e1cb2c4 | |||
| 20fb1e92e2 | |||
| 1d66ca0649 | |||
| 55b64c331a | |||
| 4d43cc526e | |||
| 6131b315d7 | |||
| dfff46e45c | |||
| 003a270548 | |||
| 39fd15b565 | |||
| be2bed9927 | |||
| 2da98e8e37 | |||
| a852975811 | |||
| 8fd7ef804d | |||
| b0f4309a29 | |||
| c33b1c644a | |||
| 7cc823e2f4 | |||
| 7e00344b84 | |||
| ec89d83916 | |||
| 57656bbaaf | |||
| 7953acf3ee | |||
| 3f528f2184 | |||
| 29e334625e | |||
| 114cea80de | |||
| 981b0cba1f | |||
| e2c40666d1 | |||
| c9ae58725c | |||
| 4318395c83 | |||
| 00264a9653 | |||
| 7e4ea670b1 | |||
| 008a470f02 | |||
| 7ed82ad82e | |||
| 72cf71fa87 | |||
| 9cb08777fa | |||
| 2c18f8b3de | |||
| d5d6987ce2 | |||
| 61a319a049 | |||
| a392dc2786 | |||
| 5e2a074019 | |||
| 9b3fd7723e | |||
| 4802eba27b | |||
| 745352ff3f | |||
| 13f0a0c9bc | |||
| 4a404d74de | |||
| 8ed4efaadc | |||
| d17c966301 |
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
|
||||||
|
|
@ -15,13 +15,44 @@ from dotenv import load_dotenv
|
||||||
|
|
||||||
# WP-20: Lade Umgebungsvariablen aus der .env Datei
|
# WP-20: Lade Umgebungsvariablen aus der .env Datei
|
||||||
# override=True garantiert, dass Änderungen in der .env immer Vorrang haben.
|
# override=True garantiert, dass Änderungen in der .env immer Vorrang haben.
|
||||||
load_dotenv(override=True)
|
# WP-24c v4.5.10: Expliziter Pfad für .env-Datei, um Probleme mit Arbeitsverzeichnis zu vermeiden
|
||||||
|
# Suche .env im Projekt-Root (3 Ebenen über app/config.py: app/config.py -> app/ -> root/)
|
||||||
|
_project_root = Path(__file__).parent.parent.parent
|
||||||
|
_env_file = _project_root / ".env"
|
||||||
|
_env_loaded = False
|
||||||
|
|
||||||
|
# Versuche zuerst expliziten Pfad
|
||||||
|
if _env_file.exists():
|
||||||
|
_env_loaded = load_dotenv(_env_file, override=True)
|
||||||
|
if _env_loaded:
|
||||||
|
# Optional: Logging (nur wenn logging bereits initialisiert ist)
|
||||||
|
try:
|
||||||
|
import logging
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
_logger.debug(f"✅ .env geladen von: {_env_file}")
|
||||||
|
except:
|
||||||
|
pass # Logging noch nicht initialisiert
|
||||||
|
|
||||||
|
# Fallback: Automatische Suche (für Dev/Test oder wenn .env an anderer Stelle liegt)
|
||||||
|
if not _env_loaded:
|
||||||
|
_env_loaded = load_dotenv(override=True)
|
||||||
|
if _env_loaded:
|
||||||
|
try:
|
||||||
|
import logging
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
_logger.debug(f"✅ .env geladen via automatische Suche (cwd: {Path.cwd()})")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
class Settings:
|
class Settings:
|
||||||
# --- Qdrant Datenbank ---
|
# --- Qdrant Datenbank ---
|
||||||
QDRANT_URL: str = os.getenv("QDRANT_URL", "http://127.0.0.1:6333")
|
QDRANT_URL: str = os.getenv("QDRANT_URL", "http://127.0.0.1:6333")
|
||||||
QDRANT_API_KEY: str | None = os.getenv("QDRANT_API_KEY")
|
QDRANT_API_KEY: str | None = os.getenv("QDRANT_API_KEY")
|
||||||
COLLECTION_PREFIX: str = os.getenv("MINDNET_PREFIX", "mindnet_dev")
|
# WP-24c v4.5.10: Harmonisierung - Unterstützt beide Umgebungsvariablen für Abwärtskompatibilität
|
||||||
|
# COLLECTION_PREFIX hat Priorität, MINDNET_PREFIX als Fallback
|
||||||
|
# WP-24c v4.5.10-FIX: Default auf "mindnet" (Prod) statt "mindnet_dev" (Dev)
|
||||||
|
# Dev muss explizit COLLECTION_PREFIX=mindnet_dev in .env setzen
|
||||||
|
COLLECTION_PREFIX: str = os.getenv("COLLECTION_PREFIX") or os.getenv("MINDNET_PREFIX") or "mindnet"
|
||||||
|
|
||||||
# WP-22: Vektor-Dimension für das Embedding-Modell (nomic)
|
# WP-22: Vektor-Dimension für das Embedding-Modell (nomic)
|
||||||
VECTOR_SIZE: int = int(os.getenv("VECTOR_DIM", "768"))
|
VECTOR_SIZE: int = int(os.getenv("VECTOR_DIM", "768"))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
# 5. Global Pool (Unzugeordnete Kanten aus dem Dokument-Ende)
|
# WP-24c v4.4.0-DEBUG: Schnittstelle 1 - Logging
|
||||||
# Sucht nach dem Edge-Pool Block im Original-Markdown.
|
if is_callout:
|
||||||
pool_match = re.search(
|
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}")
|
||||||
r'###?\s*(?:Unzugeordnete Kanten|Edge Pool|Candidates)\s*\n(.*?)(?:\n#|$)',
|
|
||||||
body_text,
|
# 6. Global Pool (Unzugeordnete Kanten - kann mitten im Dokument oder am Ende stehen)
|
||||||
re.DOTALL | re.IGNORECASE
|
# WP-24c v4.2.0: Konfigurierbare Header-Namen und -Ebene via .env
|
||||||
|
# Sucht nach ALLEN Edge-Pool Blöcken im Original-Markdown (nicht nur am Ende).
|
||||||
|
llm_validation_headers = os.getenv(
|
||||||
|
"MINDNET_LLM_VALIDATION_HEADERS",
|
||||||
|
"Unzugeordnete Kanten,Edge Pool,Candidates"
|
||||||
)
|
)
|
||||||
if pool_match:
|
header_list = [h.strip() for h in llm_validation_headers.split(",") if h.strip()]
|
||||||
|
# Fallback auf Defaults, falls leer
|
||||||
|
if not header_list:
|
||||||
|
header_list = ["Unzugeordnete Kanten", "Edge Pool", "Candidates"]
|
||||||
|
|
||||||
|
# Header-Ebene konfigurierbar (Default: 3 für ###)
|
||||||
|
llm_validation_level = int(os.getenv("MINDNET_LLM_VALIDATION_HEADER_LEVEL", "3"))
|
||||||
|
header_level_pattern = "#" * llm_validation_level
|
||||||
|
|
||||||
|
# Regex-Pattern mit konfigurierbaren Headern und Ebene
|
||||||
|
# WP-24c v4.2.0: finditer statt search, um ALLE Zonen zu finden (auch mitten im Dokument)
|
||||||
|
# Zone endet bei einem neuen Header (jeder Ebene) oder am Dokument-Ende
|
||||||
|
header_pattern = "|".join(re.escape(h) for h in header_list)
|
||||||
|
zone_pattern = rf'^{re.escape(header_level_pattern)}\s*(?:{header_pattern})\s*\n(.*?)(?=\n#|$)'
|
||||||
|
|
||||||
|
for pool_match in re.finditer(zone_pattern, body_text, re.DOTALL | re.IGNORECASE | re.MULTILINE):
|
||||||
global_edges = parse_edges_robust(pool_match.group(1))
|
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 = ""
|
||||||
|
|
||||||
|
# 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:
|
else:
|
||||||
current_chunk_text = combined
|
# 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"])
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,9 @@ class QdrantConfig:
|
||||||
port = os.getenv("QDRANT_PORT")
|
port = os.getenv("QDRANT_PORT")
|
||||||
port = int(port) if port else None
|
port = int(port) if port else None
|
||||||
api_key = os.getenv("QDRANT_API_KEY") or None
|
api_key = os.getenv("QDRANT_API_KEY") or None
|
||||||
prefix = os.getenv("COLLECTION_PREFIX") or "mindnet"
|
# WP-24c v4.5.10: Harmonisierung - Unterstützt beide Umgebungsvariablen für Abwärtskompatibilität
|
||||||
|
# COLLECTION_PREFIX hat Priorität, MINDNET_PREFIX als Fallback
|
||||||
|
prefix = os.getenv("COLLECTION_PREFIX") or os.getenv("MINDNET_PREFIX") or "mindnet"
|
||||||
dim = int(os.getenv("VECTOR_DIM") or 384)
|
dim = int(os.getenv("VECTOR_DIM") or 384)
|
||||||
distance = os.getenv("DISTANCE", "Cosine")
|
distance = os.getenv("DISTANCE", "Cosine")
|
||||||
on_disk_payload = (os.getenv("ON_DISK_PAYLOAD", "true").lower() == "true")
|
on_disk_payload = (os.getenv("ON_DISK_PAYLOAD", "true").lower() == "true")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,7 +15,8 @@ _REL_PIPE = re.compile(r"\[\[\s*rel:(?P<kind>[a-z_]+)\s*\|\s*(?P<target>[^\]]+?
|
||||||
_REL_SPACE = re.compile(r"\[\[\s*rel:(?P<kind>[a-z_]+)\s+(?P<target>[^\]]+?)\s*\]\]", re.IGNORECASE)
|
_REL_SPACE = re.compile(r"\[\[\s*rel:(?P<kind>[a-z_]+)\s+(?P<target>[^\]]+?)\s*\]\]", re.IGNORECASE)
|
||||||
_REL_TEXT = re.compile(r"rel\s*:\s*(?P<kind>[a-z_]+)\s*\[\[\s*(?P<target>[^\]]+?)\s*\]\]", re.IGNORECASE)
|
_REL_TEXT = re.compile(r"rel\s*:\s*(?P<kind>[a-z_]+)\s*\[\[\s*(?P<target>[^\]]+?)\s*\]\]", re.IGNORECASE)
|
||||||
|
|
||||||
_CALLOUT_START = re.compile(r"^\s*>\s*\[!edge\]\s*(.*)$", re.IGNORECASE)
|
# Erkennt [!edge] Callouts mit einem oder mehreren '>' am Anfang (für verschachtelte Callouts)
|
||||||
|
_CALLOUT_START = re.compile(r"^\s*>{1,}\s*\[!edge\]\s*(.*)$", re.IGNORECASE)
|
||||||
# Erkennt "kind: targets..."
|
# Erkennt "kind: targets..."
|
||||||
_REL_LINE = re.compile(r"^(?P<kind>[a-z_]+)\s*:\s*(?P<targets>.+?)\s*$", re.IGNORECASE)
|
_REL_LINE = re.compile(r"^(?P<kind>[a-z_]+)\s*:\s*(?P<targets>.+?)\s*$", re.IGNORECASE)
|
||||||
# Erkennt reine Typen (z.B. "depends_on" im Header)
|
# Erkennt reine Typen (z.B. "depends_on" im Header)
|
||||||
|
|
@ -43,6 +44,7 @@ def extract_callout_relations(text: str) -> Tuple[List[Tuple[str,str]], str]:
|
||||||
Unterstützt zwei Formate:
|
Unterstützt zwei Formate:
|
||||||
1. Explizit: "kind: [[Target]]"
|
1. Explizit: "kind: [[Target]]"
|
||||||
2. Implizit (Header): "> [!edge] kind" gefolgt von "[[Target]]" Zeilen
|
2. Implizit (Header): "> [!edge] kind" gefolgt von "[[Target]]" Zeilen
|
||||||
|
3. Verschachtelt: ">> [!edge] kind" in verschachtelten Callouts
|
||||||
"""
|
"""
|
||||||
if not text: return [], text
|
if not text: return [], text
|
||||||
lines = text.splitlines()
|
lines = text.splitlines()
|
||||||
|
|
@ -61,15 +63,33 @@ def extract_callout_relations(text: str) -> Tuple[List[Tuple[str,str]], str]:
|
||||||
# Callout-Block gefunden. Wir sammeln alle relevanten Zeilen.
|
# Callout-Block gefunden. Wir sammeln alle relevanten Zeilen.
|
||||||
block_lines = []
|
block_lines = []
|
||||||
|
|
||||||
# Header Content prüfen (z.B. "type" aus "> [!edge] type")
|
# Header Content prüfen (z.B. "type" aus "> [!edge] type" oder ">> [!edge] type")
|
||||||
header_raw = m.group(1).strip()
|
header_raw = m.group(1).strip()
|
||||||
if header_raw:
|
if header_raw:
|
||||||
block_lines.append(header_raw)
|
block_lines.append(header_raw)
|
||||||
|
|
||||||
|
# Bestimme die Einrückungsebene (Anzahl der '>' am Anfang der ersten Zeile)
|
||||||
|
leading_gt_count = len(line) - len(line.lstrip('>'))
|
||||||
|
if leading_gt_count == 0:
|
||||||
|
leading_gt_count = 1 # Fallback für den Fall, dass kein '>' gefunden wurde
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
while i < len(lines) and lines[i].lstrip().startswith('>'):
|
# Sammle alle Zeilen, die mit mindestens der gleichen Anzahl '>' beginnen
|
||||||
# Entferne '>' und führende Leerzeichen
|
while i < len(lines):
|
||||||
content = lines[i].lstrip()[1:].lstrip()
|
next_line = lines[i]
|
||||||
|
stripped = next_line.lstrip()
|
||||||
|
# Prüfe, ob die Zeile mit mindestens der gleichen Anzahl '>' beginnt
|
||||||
|
if not stripped.startswith('>'):
|
||||||
|
break
|
||||||
|
next_leading_gt_count = len(next_line) - len(next_line.lstrip('>'))
|
||||||
|
# Wenn die Einrückung kleiner wird, haben wir den Block verlassen
|
||||||
|
if next_leading_gt_count < leading_gt_count:
|
||||||
|
break
|
||||||
|
# Entferne genau die Anzahl der führenden '>' entsprechend der Einrückungsebene
|
||||||
|
# und dann führende Leerzeichen
|
||||||
|
if next_leading_gt_count >= leading_gt_count:
|
||||||
|
# Entferne die führenden '>' (entsprechend der Einrückungsebene)
|
||||||
|
content = stripped[leading_gt_count:].lstrip()
|
||||||
if content:
|
if content:
|
||||||
block_lines.append(content)
|
block_lines.append(content)
|
||||||
i += 1
|
i += 1
|
||||||
|
|
@ -86,6 +106,26 @@ def extract_callout_relations(text: str) -> Tuple[List[Tuple[str,str]], str]:
|
||||||
current_kind = first.lower()
|
current_kind = first.lower()
|
||||||
|
|
||||||
for bl in block_lines:
|
for bl in block_lines:
|
||||||
|
# Prüfe, ob diese Zeile selbst ein neuer [!edge] Callout ist (für verschachtelte Blöcke)
|
||||||
|
edge_match = re.match(r"^\s*\[!edge\]\s*(.*)$", bl, re.IGNORECASE)
|
||||||
|
if edge_match:
|
||||||
|
# Neuer Edge-Callout gefunden, setze den Typ
|
||||||
|
edge_content = edge_match.group(1).strip()
|
||||||
|
if edge_content:
|
||||||
|
# Prüfe, ob es ein "kind: targets" Format ist
|
||||||
|
mrel = _REL_LINE.match(edge_content)
|
||||||
|
if mrel:
|
||||||
|
current_kind = mrel.group("kind").strip().lower()
|
||||||
|
targets = mrel.group("targets")
|
||||||
|
# Links extrahieren
|
||||||
|
found = _WIKILINK_RE.findall(targets)
|
||||||
|
if found:
|
||||||
|
for t in found: out_pairs.append((current_kind, t.strip()))
|
||||||
|
elif _SIMPLE_KIND.match(edge_content):
|
||||||
|
# Reiner Typ ohne Targets
|
||||||
|
current_kind = edge_content.lower()
|
||||||
|
continue
|
||||||
|
|
||||||
# 1. Prüfen auf explizites "Kind: Targets" (überschreibt Header-Typ für diese Zeile)
|
# 1. Prüfen auf explizites "Kind: Targets" (überschreibt Header-Typ für diese Zeile)
|
||||||
mrel = _REL_LINE.match(bl)
|
mrel = _REL_LINE.match(bl)
|
||||||
if mrel:
|
if mrel:
|
||||||
|
|
@ -101,11 +141,8 @@ def extract_callout_relations(text: str) -> Tuple[List[Tuple[str,str]], str]:
|
||||||
for raw in re.split(r"[,;]", targets):
|
for raw in re.split(r"[,;]", targets):
|
||||||
if raw.strip(): out_pairs.append((line_kind, raw.strip()))
|
if raw.strip(): out_pairs.append((line_kind, raw.strip()))
|
||||||
|
|
||||||
# Wenn wir eine explizite Zeile gefunden haben, aktualisieren wir NICHT
|
# Aktualisiere current_kind für nachfolgende Zeilen
|
||||||
# den current_kind für nachfolgende Zeilen (Design-Entscheidung: lokal scope),
|
current_kind = line_kind
|
||||||
# oder wir machen es doch?
|
|
||||||
# Üblicher ist: Header setzt Default, Zeile überschreibt lokal.
|
|
||||||
# Wir lassen current_kind also unangetastet.
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 2. Kein Key:Value Muster -> Prüfen auf Links, die den current_kind nutzen
|
# 2. Kein Key:Value Muster -> Prüfen auf Links, die den current_kind nutzen
|
||||||
|
|
|
||||||
|
|
@ -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,13 +108,24 @@ 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)
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
@ -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)
|
note_pl = make_note_payload(parsed, vault_root=vault_root, file_path=normalized_file_path, types_cfg=self.registry)
|
||||||
ingest_cfg = self.registry.get("ingestion_settings", {})
|
note_id = note_pl.get("note_id")
|
||||||
ignore_list = ingest_cfg.get("ignore_statuses", ["system", "template", "archive", "hidden"])
|
|
||||||
|
|
||||||
current_status = fm.get("status", "draft").lower().strip()
|
if not note_id:
|
||||||
if current_status in ignore_list:
|
return {**result, "status": "error", "error": "missing_id"}
|
||||||
return {**result, "status": "skipped", "reason": "lifecycle_filter"}
|
|
||||||
|
|
||||||
# 2. Payload & Change Detection (Multi-Hash)
|
logger.info(f"📄 Bearbeite: '{note_id}' | Pfad: {normalized_file_path} | Title: {note_pl.get('title', 'N/A')}")
|
||||||
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)
|
# 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)
|
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)
|
# WP-24c v4.5.10: Prüfe auf ID-Kollisionen (zwei Dateien mit derselben note_id)
|
||||||
check_key = f"{self.active_hash_mode}:{hash_source}:{hash_normalize}"
|
if old_payload and not force_replace:
|
||||||
old_hash = (old_payload or {}).get("hashes", {}).get(check_key)
|
old_path = old_payload.get("path", "")
|
||||||
new_hash = note_pl.get("hashes", {}).get(check_key)
|
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}
|
||||||
|
|
||||||
# Check ob Chunks oder Kanten in der DB fehlen (Reparatur-Modus)
|
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)
|
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}")
|
||||||
# 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:
|
if not apply:
|
||||||
return {**result, "status": "dry-run", "changed": True, "note_id": note_id}
|
return {**result, "status": "dry-run", "changed": True, "note_id": note_id}
|
||||||
|
|
||||||
# 3. Deep Processing (Chunking, Validation, Embedding)
|
# Chunks & MoE
|
||||||
try:
|
|
||||||
body_text = getattr(parsed, "body", "") or ""
|
|
||||||
edge_registry.ensure_latest()
|
|
||||||
|
|
||||||
# Profil-Auflösung via Registry
|
|
||||||
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"),
|
|
||||||
context={"file": file_path, "note_id": note_id, "line": e.get("line", "system")}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. DB Upsert via modularisierter Points-Logik
|
# WP-24c v4.5.8: Kontext-Optimierung für Note-Scope Kanten
|
||||||
if purge_before and old_payload:
|
# Aggregiere den gesamten Note-Text für bessere Validierungs-Entscheidungen
|
||||||
purge_artifacts(self.client, self.prefix, note_id)
|
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
|
||||||
|
|
||||||
# Speichern der Haupt-Note
|
validated_edges = []
|
||||||
n_name, n_pts = points_for_note(self.prefix, note_pl, None, self.dim)
|
rejected_edges = []
|
||||||
upsert_batch(self.client, n_name, n_pts)
|
|
||||||
|
|
||||||
# Speichern der Chunks
|
for e in raw_edges:
|
||||||
if chunk_pls and vecs:
|
rule_id = e.get("rule_id", "")
|
||||||
c_pts = points_for_chunks(self.prefix, chunk_pls, vecs)[1]
|
provenance = e.get("provenance", "")
|
||||||
upsert_batch(self.client, f"{self.prefix}_chunks", c_pts)
|
|
||||||
|
|
||||||
# Speichern der Kanten
|
# WP-24c v4.5.8: Trigger-Kriterium - rule_id ODER provenance beginnt mit "candidate:"
|
||||||
if edges:
|
is_candidate = (rule_id and rule_id.startswith("candidate:")) or (provenance and provenance.startswith("candidate:"))
|
||||||
e_pts = points_for_edges(self.prefix, edges)[1]
|
|
||||||
upsert_batch(self.client, f"{self.prefix}_edges", e_pts)
|
|
||||||
|
|
||||||
return {
|
if is_candidate:
|
||||||
"path": file_path,
|
# Extrahiere target_id für Validierung (aus verschiedenen möglichen Feldern)
|
||||||
"status": "success",
|
target_id = e.get("target_id") or e.get("to")
|
||||||
"changed": True,
|
if not target_id:
|
||||||
"note_id": note_id,
|
# Fallback: Versuche aus Payload zu extrahieren
|
||||||
"chunks_count": len(chunk_pls),
|
payload = e.get("extra", {}) if isinstance(e.get("extra"), dict) else {}
|
||||||
"edges_count": len(edges)
|
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)
|
||||||
|
|
||||||
|
# DB Upsert
|
||||||
|
if purge_before and old_payload: purge_artifacts(self.client, self.prefix, note_id)
|
||||||
|
|
||||||
|
col_n, pts_n = points_for_note(self.prefix, note_pl, None, self.dim)
|
||||||
|
upsert_batch(self.client, col_n, pts_n, wait=True)
|
||||||
|
|
||||||
|
if chunk_pls and vecs:
|
||||||
|
col_c, pts_c = points_for_chunks(self.prefix, chunk_pls, vecs)
|
||||||
|
upsert_batch(self.client, col_c, pts_c, wait=True)
|
||||||
|
|
||||||
|
if explicit_edges:
|
||||||
|
col_e, pts_e = points_for_edges(self.prefix, explicit_edges)
|
||||||
|
upsert_batch(self.client, col_e, pts_e, wait=True)
|
||||||
|
|
||||||
|
logger.info(f" ✨ Phase 1 fertig: {len(explicit_edges)} explizite Kanten für '{note_id}'.")
|
||||||
|
return {"status": "success", "note_id": note_id}
|
||||||
|
|
||||||
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(
|
|
||||||
|
except (ValueError, KeyError) as template_error:
|
||||||
|
# WP-24c v4.5.9: Fallback auf generisches Template mit variables
|
||||||
|
# Nutzt Lazy-Loading aus WP-25b für modell-spezifische Fallback-Prompts
|
||||||
|
logger.warning(f"⚠️ [FALLBACK] Template 'fallback_synthesis' nicht gefunden: {template_error}. Versuche generisches Template.")
|
||||||
|
logger.debug(f" -> Fallback-Profile: {profile}, Context-Länge: {len(fallback_context)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# WP-24c v4.5.9: Versuche generisches Template mit variables (Lazy-Loading)
|
||||||
|
result = await self.llm_service.generate_raw_response(
|
||||||
|
prompt_key="fallback_synthesis_generic", # Fallback-Template
|
||||||
|
variables={"query": query, "context": fallback_context},
|
||||||
|
system=system_prompt, priority="realtime", profile_name=profile
|
||||||
|
)
|
||||||
|
logger.info(f"✅ [FALLBACK] Generisches Template erfolgreich (Antwort-Länge: {len(result) if result else 0})")
|
||||||
|
return result
|
||||||
|
except (ValueError, KeyError) as fallback_error:
|
||||||
|
# WP-24c v4.5.9: Letzter Fallback - direkter Prompt (nur wenn beide Templates fehlen)
|
||||||
|
logger.error(f"❌ [FALLBACK] Auch generisches Template nicht gefunden: {fallback_error}. Verwende direkten Prompt als letzten Fallback.")
|
||||||
|
result = await self.llm_service.generate_raw_response(
|
||||||
prompt=f"Beantworte: {query}\n\nKontext:\n{fallback_context}",
|
prompt=f"Beantworte: {query}\n\nKontext:\n{fallback_context}",
|
||||||
system=system_prompt, priority="realtime", profile_name=profile
|
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]
|
||||||
|
|
||||||
|
|
@ -148,6 +216,9 @@ def _build_explanation(
|
||||||
|
|
||||||
direction = "in" if tgt == target_note_id else "out"
|
direction = "in" if tgt == target_note_id else "out"
|
||||||
|
|
||||||
|
# WP-24c v4.5.10: Robuste EdgeDTO-Erstellung mit Fehlerbehandlung
|
||||||
|
# Falls Provenance-Wert nicht unterstützt wird, verwende Fallback
|
||||||
|
try:
|
||||||
edge_obj = EdgeDTO(
|
edge_obj = EdgeDTO(
|
||||||
id=f"{src}->{tgt}:{kind}",
|
id=f"{src}->{tgt}:{kind}",
|
||||||
kind=kind,
|
kind=kind,
|
||||||
|
|
@ -159,12 +230,35 @@ def _build_explanation(
|
||||||
confidence=conf
|
confidence=conf
|
||||||
)
|
)
|
||||||
edges_dto.append(edge_obj)
|
edges_dto.append(edge_obj)
|
||||||
|
except Exception as e:
|
||||||
|
# WP-24c v4.5.10: Fallback bei Validierungsfehler (z.B. alte EdgeDTO-Version im Cache)
|
||||||
|
logger.warning(
|
||||||
|
f"⚠️ [EDGE-DTO] Provenance '{prov}' nicht unterstützt für Edge {src}->{tgt} ({kind}). "
|
||||||
|
f"Fehler: {e}. Verwende Fallback 'explicit'."
|
||||||
|
)
|
||||||
|
# Fallback: Verwende 'explicit' als sicheren Default
|
||||||
|
try:
|
||||||
|
edge_obj = EdgeDTO(
|
||||||
|
id=f"{src}->{tgt}:{kind}",
|
||||||
|
kind=kind,
|
||||||
|
source=src,
|
||||||
|
target=tgt,
|
||||||
|
weight=conf,
|
||||||
|
direction=direction,
|
||||||
|
provenance="explicit", # Fallback
|
||||||
|
confidence=conf
|
||||||
|
)
|
||||||
|
edges_dto.append(edge_obj)
|
||||||
|
except Exception as e2:
|
||||||
|
logger.error(f"❌ [EDGE-DTO] Auch Fallback fehlgeschlagen: {e2}. Überspringe Edge.")
|
||||||
|
# Überspringe diese Kante - besser als kompletter Fehler
|
||||||
|
|
||||||
# Die 3 wichtigsten Kanten als Begründung formulieren
|
# Die 3 wichtigsten Kanten als Begründung formulieren
|
||||||
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 +348,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 +371,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 +424,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
|
||||||
|
|
@ -336,20 +521,31 @@ def hybrid_retrieve(req: QueryRequest) -> QueryResponse:
|
||||||
for e in edges:
|
for e in edges:
|
||||||
subgraph.in_degree[e["target"]] += 1
|
subgraph.in_degree[e["target"]] += 1
|
||||||
|
|
||||||
# --- WP-22: Kanten-Gewichtung (Provenance & Intent Boost) ---
|
# WP-24c v4.1.0: Chunk-Level In-Degree als Attribut speichern
|
||||||
|
subgraph.chunk_level_in_degree = chunk_level_in_degree
|
||||||
|
|
||||||
|
# --- 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 +553,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
|
||||||
|
|
|
||||||
13
app/main.py
13
app/main.py
|
|
@ -109,12 +109,23 @@ def create_app() -> FastAPI:
|
||||||
@app.get("/healthz")
|
@app.get("/healthz")
|
||||||
def healthz():
|
def healthz():
|
||||||
"""Bietet Statusinformationen über die Engine und Datenbank-Verbindung."""
|
"""Bietet Statusinformationen über die Engine und Datenbank-Verbindung."""
|
||||||
|
# WP-24c v4.5.10: Prüfe EdgeDTO-Version zur Laufzeit
|
||||||
|
edge_dto_supports_callout = False
|
||||||
|
try:
|
||||||
|
from app.models.dto import EdgeDTO
|
||||||
|
import inspect
|
||||||
|
source = inspect.getsource(EdgeDTO)
|
||||||
|
edge_dto_supports_callout = "explicit:callout" in source
|
||||||
|
except Exception:
|
||||||
|
pass # Fehler beim Prüfen ist nicht kritisch
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"qdrant": s.QDRANT_URL,
|
"qdrant": s.QDRANT_URL,
|
||||||
"prefix": s.COLLECTION_PREFIX,
|
"prefix": s.COLLECTION_PREFIX,
|
||||||
"moe_enabled": True
|
"moe_enabled": True,
|
||||||
|
"edge_dto_supports_callout": edge_dto_supports_callout # WP-24c v4.5.10: Diagnose-Hilfe
|
||||||
}
|
}
|
||||||
|
|
||||||
# Inkludieren der Router (100% Kompatibilität erhalten)
|
# Inkludieren der Router (100% Kompatibilität erhalten)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -69,6 +77,9 @@ class QueryRequest(BaseModel):
|
||||||
# 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):
|
||||||
"""User-Feedback zu einem spezifischen Treffer oder der Gesamtantwort."""
|
"""User-Feedback zu einem spezifischen Treffer oder der Gesamtantwort."""
|
||||||
|
|
@ -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={
|
||||||
|
|
@ -258,3 +273,90 @@ 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
|
# --- PHASE 1: EXACT MATCHES (TITEL & ALIASSE) ---
|
||||||
default_edge_type = self._get_default_edge_type(current_type)
|
# Lädt alle bekannten Titel/Aliasse für einen schnellen Scan
|
||||||
|
|
||||||
# Tracking-Sets für Deduplizierung (Wir merken uns NOTE-IDs)
|
|
||||||
seen_target_note_ids = set()
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# 1. Exact Match: Titel/Aliases
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Holt Titel, Aliases UND Typen aus dem Index
|
|
||||||
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"])
|
|
||||||
|
|
||||||
# INTELLIGENTE KANTEN-LOGIK (MATRIX)
|
seen_target_ids.add(target_id)
|
||||||
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):
|
||||||
|
v_mtime = os.path.getmtime(self.full_vocab_path)
|
||||||
|
if v_mtime > self._last_vocab_mtime:
|
||||||
self._load_vocabulary()
|
self._load_vocabulary()
|
||||||
self._last_mtime = current_mtime
|
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(" ", "_")
|
||||||
|
if clean_alias:
|
||||||
self.canonical_map[clean_alias] = canonical
|
self.canonical_map[clean_alias] = canonical
|
||||||
c_aliases += 1
|
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]
|
|
||||||
|
|
||||||
# Fallback und Logging unbekannter Typen für Admin-Review
|
def get_inverse(self, edge_type: str) -> str:
|
||||||
self._log_issue(clean_type, "unknown_type", ctx)
|
"""WP-24c: Gibt das symmetrische Gegenstück zurück."""
|
||||||
return clean_type
|
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()
|
||||||
|
|
||||||
|
# Hierarchische Suche: Spezifisch -> 'any' -> Empty
|
||||||
|
src_cfg = self.topology.get(source_type, self.topology.get("any", {}))
|
||||||
|
tgt_cfg = src_cfg.get(target_type, src_cfg.get("any", {"typical": set(), "prohibited": set()}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"typical": sorted(list(tgt_cfg["typical"])),
|
||||||
|
"prohibited": sorted(list(tgt_cfg["prohibited"]))
|
||||||
|
}
|
||||||
|
|
||||||
def _log_issue(self, edge_type: str, error_kind: str, ctx: dict):
|
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()
|
||||||
|
|
@ -32,9 +32,14 @@ streams_library:
|
||||||
top_k: 5
|
top_k: 5
|
||||||
edge_boosts:
|
edge_boosts:
|
||||||
guides: 3.0
|
guides: 3.0
|
||||||
enforced_by: 2.5
|
depends_on: 2.5
|
||||||
based_on: 2.0
|
based_on: 2.0
|
||||||
|
upholds: 2.5
|
||||||
|
violates: 2.5
|
||||||
|
aligned_with: 2.0
|
||||||
|
conflicts_with: 2.0
|
||||||
|
supports: 1.5
|
||||||
|
contradicts: 1.5
|
||||||
facts_stream:
|
facts_stream:
|
||||||
name: "Operative Realität"
|
name: "Operative Realität"
|
||||||
llm_profile: "synthesis_pro"
|
llm_profile: "synthesis_pro"
|
||||||
|
|
@ -60,6 +65,8 @@ streams_library:
|
||||||
related_to: 1.5
|
related_to: 1.5
|
||||||
experienced_in: 2.0
|
experienced_in: 2.0
|
||||||
expert_for: 2.5
|
expert_for: 2.5
|
||||||
|
followed_by: 2.0
|
||||||
|
preceded_by: 2.0
|
||||||
|
|
||||||
risk_stream:
|
risk_stream:
|
||||||
name: "Risiko-Radar"
|
name: "Risiko-Radar"
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,18 @@ 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", "```"]
|
||||||
|
|
||||||
|
|
@ -107,9 +94,8 @@ llm_settings:
|
||||||
types:
|
types:
|
||||||
|
|
||||||
experience:
|
experience:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: structured_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?)"
|
||||||
|
|
@ -118,9 +104,8 @@ types:
|
||||||
- "Reflexion & Learning (Was lerne ich daraus?)"
|
- "Reflexion & Learning (Was lerne ich daraus?)"
|
||||||
|
|
||||||
insight:
|
insight:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: structured_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?)"
|
||||||
|
|
@ -129,9 +114,8 @@ types:
|
||||||
- "Handlungsempfehlung"
|
- "Handlungsempfehlung"
|
||||||
|
|
||||||
project:
|
project:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: structured_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)"
|
||||||
|
|
@ -243,9 +214,8 @@ types:
|
||||||
- "Strategie"
|
- "Strategie"
|
||||||
|
|
||||||
need:
|
need:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: structured_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"
|
||||||
|
|
@ -253,9 +223,8 @@ types:
|
||||||
- "Bezug zu Werten"
|
- "Bezug zu Werten"
|
||||||
|
|
||||||
motivation:
|
motivation:
|
||||||
chunking_profile: sliding_smart_edges
|
chunking_profile: structured_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,77 @@ 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: structured_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: structured_smart_edges
|
||||||
retriever_weight: 0.95
|
retriever_weight: 0.95
|
||||||
edge_defaults: ["depends_on", "related_to"]
|
detection_keywords: ["ziel", "zielzustand", "kpi", "zeitrahmen", "deadline", "meilenstein"]
|
||||||
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: structured_smart_edges
|
||||||
retriever_weight: 0.60
|
retriever_weight: 0.6
|
||||||
edge_defaults: ["references", "related_to"]
|
detection_keywords: ["definition", "konzept", "begriff", "modell", "rahmen", "theorie"]
|
||||||
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.8
|
||||||
edge_defaults: ["depends_on", "part_of"]
|
detection_keywords: ["aufgabe", "todo", "next_action", "erledigen", "definition_of_done", "checkliste"]
|
||||||
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.8
|
||||||
edge_defaults: ["references", "related_to"]
|
detection_keywords: ["journal", "tagebuch", "log", "eintrag", "reflexion", "heute"]
|
||||||
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.5
|
||||||
edge_defaults: []
|
detection_keywords: ["quelle", "paper", "buch", "artikel", "link", "zitat", "studie"]
|
||||||
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.4
|
||||||
edge_defaults: ["related_to"]
|
detection_keywords: ["glossar", "begriff", "definition", "terminologie"]
|
||||||
schema: ["Begriff", "Definition"]
|
schema: ["Begriff", "Definition"]
|
||||||
|
|
||||||
person:
|
person:
|
||||||
chunking_profile: sliding_standard
|
chunking_profile: sliding_standard
|
||||||
retriever_weight: 0.50
|
retriever_weight: 0.5
|
||||||
edge_defaults: ["related_to"]
|
detection_keywords: ["person", "mensch", "kontakt", "name", "beziehung", "stakeholder"]
|
||||||
schema: ["Rolle", "Beziehung", "Kontext"]
|
schema: ["Profile", "Beziehung", "Kontext"]
|
||||||
|
|
||||||
event:
|
event:
|
||||||
chunking_profile: sliding_standard
|
chunking_profile: sliding_standard
|
||||||
retriever_weight: 0.60
|
retriever_weight: 0.6
|
||||||
edge_defaults: ["related_to"]
|
detection_keywords: ["ereignis", "termin", "datum", "ort", "teilnehmer", "meeting"]
|
||||||
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.0
|
||||||
edge_defaults: ["references"]
|
detection_keywords: []
|
||||||
schema: ["Inhalt"]
|
schema: ["Inhalt"]
|
||||||
1
debug.log
Normal file
1
debug.log
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[0114/152756.633:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: Das System kann die angegebene Datei nicht finden. (0x2)
|
||||||
69
docs/00_General/00_Marketing_V3.md
Normal file
69
docs/00_General/00_Marketing_V3.md
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Mindnet V3.0: Der Aufstieg des Digitalen Zwillings
|
||||||
|
## Von der Wissensdatenbank zum strategischen Partner – Ein Paradigmenwechsel
|
||||||
|
|
||||||
|
### Einleitung: Die Vision von Version 3.0
|
||||||
|
Mit der Vollendung des Meilensteins WP25 (inklusive der Architektur-Erweiterungen 25a und 25b) transformiert sich Mindnet von einem reinen Retrieval-System (V2) zu einem autonomen, agentischen Ökosystem (V3.0). Mindnet V3.0 ist nicht länger nur ein Werkzeug zur Informationswiedergabe; es ist ein **Digitaler Zwilling**, der in der Lage ist, komplexe Realitäten durch Multi-Stream-Analysen zu erfassen, strategische Empfehlungen auf Basis individueller Werte zu geben und eine bisher unerreichte Ausfallsicherheit zu garantieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Die 6 Säulen der Mindnet V3.0 Architektur
|
||||||
|
|
||||||
|
#### 1. Agentic Multi-Stream Retrieval (WP-25)
|
||||||
|
Das Herzstück von V3.0 ist die neue `DecisionEngine`. Während herkömmliche Systeme lediglich eine einfache Vektorsuche durchführen, orchestriert die DecisionEngine parallele Wissens-Streams:
|
||||||
|
* **Werte-Stream:** Abgleich von Anfragen mit Ihrer ethischen und strategischen Identität.
|
||||||
|
* **Fakten-Stream:** Analyse der operativen Realität und aktueller Projektdaten.
|
||||||
|
* **Biografie-Stream:** Integration persönlicher Erfahrungen und historischer Kontexte.
|
||||||
|
* **Risiko-Radar:** Proaktive Identifikation von Hindernissen und Zielkonflikten.
|
||||||
|
* **Technik-Wissen:** Tiefgreifende fachliche Expertise für spezialisierte Aufgaben.
|
||||||
|
|
||||||
|
Dieses System erlaubt es Mindnet, eine Anfrage aus fünf verschiedenen Perspektiven gleichzeitig zu beleuchten, bevor eine finale Synthese erfolgt.
|
||||||
|
|
||||||
|
#### 2. Mixture of Experts (MoE) & Dynamic Profiling (WP-25a)
|
||||||
|
Mindnet V3.0 nutzt nicht mehr nur "ein" Modell. Über die zentrale Steuerung in der `llm_profiles.yaml` wird für jede Teilaufgabe der ideale "Experte" gerufen:
|
||||||
|
* **Der Architekt (Gemini 2.0 Flash):** Für hochkomplexe reasoning-intensive Synthesen.
|
||||||
|
* **Der Ingenieur (Qwen 2.5):** Spezialisiert auf präzise Code-Generierung und technische Problemlösung.
|
||||||
|
* **Der Dampfhammer (Mistral 7B):** Optimiert für blitzschnelles Routing und asynchrone Inhaltskompression.
|
||||||
|
* **Der Wächter (Phi-3 Mini):** Ein lokales Modell via Ollama, das maximale Privatsphäre für sensible Identitätsdaten garantiert.
|
||||||
|
|
||||||
|
#### 3. Hierarchische Lazy-Prompt-Orchestration (WP-25b)
|
||||||
|
Ein technologisches Highlight ist die Einführung des **Lazy-Promptings**. Prompts werden nicht mehr statisch im Code verwaltet, sondern erst im Moment der Modellauswahl hierarchisch aufgelöst:
|
||||||
|
1. **Modell-Ebene:** Spezifisch für die jeweilige Modell-ID optimierte Instruktionen.
|
||||||
|
2. **Provider-Ebene:** Fallback-Anweisungen für OpenRouter oder Ollama.
|
||||||
|
3. **Global-Ebene:** Sicherheits-Instruktionen als ultimativer Anker.
|
||||||
|
|
||||||
|
Dies garantiert, dass jedes Modell in seiner "Muttersprache" angesprochen wird, was die Antwortqualität drastisch erhöht.
|
||||||
|
|
||||||
|
#### 4. Die unzerstörbare Fallback-Kaskade
|
||||||
|
Resilienz ist in V3.0 kein Schlagwort, sondern ein Algorithmus. Sollte ein Cloud-Anbieter (wie OpenRouter) ausfallen oder in ein Rate-Limit laufen, reagiert das System autonom:
|
||||||
|
* Automatischer Wechsel auf das Backup-Profil (z.B. von Gemini auf Llama).
|
||||||
|
* In letzter Instanz: Rückzug auf die lokale Hardware (Ollama/Phi-3), sodass Mindnet auch offline voll einsatzfähig bleibt.
|
||||||
|
* **Lazy-Re-Formatting:** Beim Wechsel des Modells wird auch der Prompt sofort neu geladen und für das neue Modell optimiert.
|
||||||
|
|
||||||
|
#### 5. Hochpräzises Intent-Routing mit Regex-Cleaning
|
||||||
|
Durch den neuen ultra-robusten Router in der `DecisionEngine` v1.3.2 erkennt Mindnet Nutzerintentionen mit chirurgischer Präzision. Modell-Artefakte (wie Stop-Marker oder überflüssige Tags freier Modelle) werden durch aggressive Regex-Filter eliminiert, bevor sie das System-Routing stören können. Dies stellt sicher, dass eine Coding-Frage niemals fälschlicherweise im Fakten-Modus landet.
|
||||||
|
|
||||||
|
#### 6. Semantische Ingestion-Validierung v2.14.0
|
||||||
|
Die Qualität des Wissensgraphen wird durch eine neue Validierungsebene geschützt. Während des Imports prüft Mindnet semantisch, ob vorgeschlagene Verknüpfungen (Edges) zwischen Informationen wirklich sinnvoll sind. Dabei unterscheidet das System zwischen temporären Netzwerkfehlern und dauerhaften Logikfehlern, um die Integrität Ihres digitalen Gedächtnisses zu wahren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Technische Highlights für Power-User
|
||||||
|
|
||||||
|
| Feature | Technologie | Nutzen |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Orchestrator** | `DecisionEngine v1.3.2` | Agentische Steuerung & Multi-Stream Retrieval |
|
||||||
|
| **Hybrid Cloud** | OpenRouter & Ollama | Maximale Flexibilität zwischen Leistung und Datenschutz |
|
||||||
|
| **Traceability** | `[PROMPT-TRACE]` Logs | Volle Transparenz über die genutzten KI-Instruktionen |
|
||||||
|
| **Context Guard** | Asynchrone Kompression | Optimierung der Kontextfenster für maximale Kosten-Effizienz |
|
||||||
|
| **Resilienz** | Rekursive Fallback-Kaskade | 100% Verfügbarkeit durch Cloud-to-Local Automatisierung |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fazit: Ihr Gehirn, erweitert durch Mindnet V3.0
|
||||||
|
Mindnet V3.0 ist das Ergebnis einer konsequenten Weiterentwicklung hin zu einer **Zero-Failure-Architektur**. Durch die Kombination aus agentischer Intelligenz, hybrider Modellnutzung und der neuen Lazy-Prompt-Infrastruktur bietet es eine Basis, die nicht nur mit Ihrem Wissen wächst, sondern aktiv dabei hilft, dieses Wissen in strategisches Handeln zu übersetzen.
|
||||||
|
|
||||||
|
**Willkommen in der Ära von Mindnet V3.0 – Ihr strategischer Partner ist bereit.**
|
||||||
|
|
||||||
|
---
|
||||||
|
*Dokumentations-Identifikator: `mindnet_v3_core_release`*
|
||||||
|
*Synchronisations-Stand: WP-25b Final*
|
||||||
|
|
@ -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
|
||||||
|
|
@ -65,3 +65,10 @@ context: "Zentrales Glossar für Mindnet v3.1.1. Enthält Definitionen zu Hybrid
|
||||||
* **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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ id: 01-authoring-guidelines
|
||||||
title: Authoring Guidelines – Handbuch für den Digitalen Zwilling
|
title: Authoring Guidelines – Handbuch für den Digitalen Zwilling
|
||||||
type: principle
|
type: principle
|
||||||
status: stable
|
status: stable
|
||||||
version: 1.1.0
|
version: 1.3.0
|
||||||
area: system_documentation
|
area: system_documentation
|
||||||
tags: [handbuch, authoring, methodik, obsidian, mindnet, best-practice]
|
tags: [handbuch, authoring, methodik, obsidian, mindnet, best-practice]
|
||||||
retriever_weight: 2.0
|
retriever_weight: 2.0
|
||||||
|
|
@ -11,145 +11,121 @@ retriever_weight: 2.0
|
||||||
|
|
||||||
# Authoring Guidelines: Dein Werkzeug für den Digitalen Zwilling
|
# Authoring Guidelines: Dein Werkzeug für den Digitalen Zwilling
|
||||||
|
|
||||||
|
Dieses Handbuch ist dein primäres Werkzeug, um Wissen so zu strukturieren, dass Mindnet deine Persönlichkeit spiegelt, empathisch reagiert und dich sowie deine Nachkommen strategisch berät. Es dient als Brücke zwischen deiner menschlichen Navigation in Obsidian und der technischen Logik der Mindnet-Engine.
|
||||||
Dieses Handbuch ist dein primäres Werkzeug, um Wissen so zu strukturieren, dass Mindnet deine Persönlichkeit spiegelt, empathisch reagiert und dich strategisch berät.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚡ Die 6 Goldenen Regeln des Knowledge Designs
|
## ⚡ Die 6 Goldenen Regeln (TL;DR)
|
||||||
|
|
||||||
1. **Atomare Gedanken:** Eine Notiz = Ein Thema. Trenne z. B. „Meditation“ von „Mobility“, auch wenn beides Ich-Pflege ist.
|
1. **Atomare Gedanken:** Eine Notiz = Ein Thema. Trenne z. B. „Meditation“ von „Mobility“.
|
||||||
2. **Explizite Typen:** Nutze den `type`, um der KI zu sagen, wie sie den Text verarbeiten soll (z. B. `insight` für Beobachtungen, `experience` für Erlebnisse).
|
2. **Explizite Typen:** Nutze den `type` im Frontmatter (z. B. `insight`, `experience`, `value`), um die mathematische Gewichtung zu steuern.
|
||||||
3. **Semantische Links:** Verknüpfe aktiv mit `[[rel:depends_on ...]]` oder `[[rel:based_on ...]]`. Sag dem System, *warum* Dinge zusammenhängen.
|
3. **H3-Hub-Pairing (NEU):** Nutze H3-Überschriften in Hubs, um spezifische Links und ihre Bedeutung (Edges) in isolierten Chunks für die KI zu fixieren, ohne die Obsidian-Graphen-Logik zu brechen.
|
||||||
4. **Werte & Ziele definieren:** Erstelle für jeden Nordstern und jeden Kernwert eine eigene Notiz. Ohne Maßstäbe kann der „Berater“ nicht entscheiden.
|
4. **Werte & Ziele definieren:** Erstelle für jeden Kernwert eine eigene Notiz (`type: value`). Ohne explizite Maßstäbe kann die Decision Engine nicht in deinem Sinne abwägen.
|
||||||
5. **Emotionales Bridging:** Nutze Begriffe wie „Druck“, „Stolz“, „Euphorie“ oder „Hilflosigkeit“, um die Empathie-Ebene der KI zu aktivieren.
|
5. **Emotionales Bridging:** Nutze Begriffe wie „Druck“, „Faszination“ oder „Angst“, um die Empathie-Ebene der KI zu aktivieren.
|
||||||
6. **Narrative Tiefe (Fleisch am Knochen):** Dokumentiere das „Warum“ hinter einer Entscheidung. Fakten informieren, aber Erzählungen prägen den Charakter.
|
6. **Narrative Tiefe (Fleisch am Knochen):** Dokumentiere das „Warum“ hinter einer Entscheidung. Erzählungen prägen deinen Charakter für die Nachwelt mehr als reine Fakten.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Strategische Steuerung (Status & Gewicht)
|
## 1. Die Vault-Architektur (Stream-Mapping)
|
||||||
|
|
||||||
Du entscheidest über das Frontmatter, wie präsent eine Information im „Gedächtnis“ ist.
|
Der Vault ist in acht funktionale Domänen unterteilt, die direkt mit den internen Wissens-Streams korrespondieren.
|
||||||
|
|
||||||
### 1.1 Status-Logik
|
| Ordner | Domäne | Stream-Logik | Zweck |
|
||||||
* **`stable`**: Gold-Standard. Für finale Leitbild-Texte und Nordsterne. Erhält +20% Relevanz-Bonus.
|
| :--- | :--- | :--- | :--- |
|
||||||
* **`active`**: Standard für laufende Projekte und aktuelle Beobachtungen.
|
| **00_Leitbild** | Verfassung | **Identity** | Chronik deiner Werte-Evolution über die Jahre. |
|
||||||
* **`draft`**: Brainstorming oder rohe Tages-Logs. Die KI nutzt diese nur nachrangig (50% Malus), um Rauschen zu vermeiden.
|
| **01_Identify** | Kern-Identität | **Identity** | SSOT für Werte, Prinzipien, Rollen, Bedürfnisse und Glaubenssätze. |
|
||||||
* **`system`**: Rein technische Dateien (Templates, Guides). Werden im Chat ignoriert.
|
| **02_Projects** | Dynamik | **Action** | Aktive Vorhaben, Missionen und operative Aufgaben (Tasks). |
|
||||||
|
| **03_Experiences** | Biografie | **History** | Speicherort für Erlebnisse (Experiences), Ereignisse (Events) und Zustände (States). |
|
||||||
### 1.2 Manuelle Gewichtung (`retriever_weight`)
|
| **04_Insights** | Erkenntnisse | **History/Basis** | Fachwissen, Konzepte, Ideen und tiefe Musteranalysen. |
|
||||||
* **Boost (1.20):** Für hochrelevante Erkenntnisse. **Beispiel:** Beobachtungen zum Verhalten von Rohan werden als `insight` mit `1.20` markiert, damit sie bei Erziehungsfragen sofort präsent sind.
|
| **05_Decisions** | Steuerung | **Action** | Dokumentation getroffener Entscheidungen (ADR-Logik). |
|
||||||
* **Deboost (0.50 - 0.80):** Für tägliche Routine-Einträge („Heute 10 Min. meditiert“). Sie dienen der Chronik, sollen aber keine tiefen Analysen verfälschen.
|
| **06_Skills** | Kompetenz | **Action** | Fertigkeiten, Lernpfade und Meisterschaftsnachweise. |
|
||||||
|
| **07_People** | Soziales Netz | **History/Basis** | Kontaktprofile, Rollen und soziale Vernetzung. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Das Kochbuch: Praktische Use-Cases
|
## 2. Das Schicht-Modell & Hub-Design
|
||||||
|
|
||||||
### 2.1 Ein Erlebnis aufschreiben (`type: experience`)
|
In Hub-Notizen (z. B. „Wendepunkte“) nutzen wir eine hierarchische Schichtung, um Präzision für die KI und Übersicht für den Menschen zu garantieren.
|
||||||
**Ziel:** Den „Spiegel“ (Empathy) mit deiner Biografie kalibrieren.
|
|
||||||
|
|
||||||
**Struktur & Leitfragen:**
|
### 2.1 Die H3-Regel für präzises Pairing
|
||||||
- **Kontext:** Was ist passiert? (Sachlich kurz).
|
Um Kausalitäten (z. B. Ereignis A führte zu Gefühl B) ohne proprietäre Syntax abzubilden, wird jedes Hauptelement eines Hubs in eine **H3-Sektion** gefasst.
|
||||||
- **Emotions-Check:** Wie habe ich mich in der Situation gefühlt? (Wichtig für Regel 5).
|
* **Technischer Effekt:** Das System nutzt für Hubs das Profil `structured_smart_edges_strict_L3` und schneidet an jeder H3-Ebene einen sauberen Chunk.
|
||||||
- **Die Lektion:** Was habe ich über mich gelernt? (z. B. „Ich reagiere allergisch auf Ungerechtigkeit“).
|
* **Vorteil:** Callouts innerhalb dieser Sektion beziehen sich ausschließlich auf diesen Kontext, was die Antwortqualität massiv erhöht.
|
||||||
- **Deep-Edge:** Mit welcher Rolle ist das verknüpft? `[[rel:supports Meine Rollenlandkarte 2025#Vater]]`.
|
|
||||||
|
|
||||||
### 2.2 Eine Beobachtung festhalten (`type: insight`)
|
### 2.2 Die 3 Schichten im Detail
|
||||||
**Ziel:** Den „Berater“ (Decision) mit Mustern versorgen.
|
* **Ebene 1: Cluster (Hub)**: Der Navigator (`type: insight`, `status: stable`). Fasst Lebensphasen oder Themenwelten zusammen.
|
||||||
|
* **Ebene 2: Reflexion (Erlebnis)**: Die `experience`-Notiz. Die bewusste Aufarbeitung mit „Emotions-Check“ und „Lektion“.
|
||||||
|
* **Ebene 3: Evidenz (Faktum)**: `event` oder `state`-Notizen. Die atomaren Rohdaten und Gefühle des Augenblicks (Momentaufnahmen).
|
||||||
|
|
||||||
**Beispiel Rohan-Beobachtung:**
|
|
||||||
- **Beobachtung:** Rohan reagiert positiv auf „leise“ Impulse und 90/10 Coaching.
|
|
||||||
- **Interpretation:** Direkte Konfrontation erzeugt Gegendruck; Fragen erzeugen Ownership.
|
|
||||||
- **Konsequenz:** Prinzip P3a (Familienregeln) muss immer einen Bedürfnischeck vorausschicken.
|
|
||||||
|
|
||||||
### 2.3 Ein Review durchführen (`type: journal` / `insight`)
|
|
||||||
**Ziel:** Den Fortschritt steuern.
|
|
||||||
- **Daily:** Rohes Log (Status `draft`).
|
|
||||||
- **Weekly/Monthly:** Verdichtung der Erkenntnisse in eine `stable` Notiz. Frage: „Was war der größte Hebel diese Woche?“.
|
|
||||||
|
|
||||||
### 2.4 Das Handlungsprinzip (`type: principle`)
|
|
||||||
**Ziel:** Testbare Regeln für schwierige Entscheidungssituationen.
|
|
||||||
|
|
||||||
**Struktur-Vorgabe:**---
|
|
||||||
id: 01-authoring-guidelines
|
|
||||||
title: Authoring Guidelines – Das Handbuch für den Digitalen Zwilling
|
|
||||||
type: principle
|
|
||||||
status: stable
|
|
||||||
version: 1.2.0
|
|
||||||
area: system_documentation
|
|
||||||
tags: [handbuch, authoring, methodik, obsidian, mindnet, best-practice]
|
|
||||||
retriever_weight: 2.0
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🕸️ Teil 3: Netzdesign (Hubs, Edges & Lücken)
|
## 3. Strategische Steuerung (Status & Gewicht)
|
||||||
|
|
||||||
|
Du entscheidest über das Frontmatter, wie präsent eine Information im „Gedächtnis“ des Systems ist.
|
||||||
|
|
||||||
|
* **`status: stable`**: Gold-Standard (+20% Relevanz-Bonus). Für finales Leitbild-Wissen und Kernwerte.
|
||||||
|
* **`status: active`**: Standard für laufende Projekte und verifizierte Erlebnisse.
|
||||||
|
* **`status: draft`**: Brainstorming oder rohe Tages-Logs. Erhält einen Malus (50%), um Rauschen zu vermeiden.
|
||||||
|
* **`retriever_weight`**: Nutze `1.2` für Hubs und `1.1` für prägende Erlebnisse.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Netzdesign & Semantic Mapping
|
||||||
|
|
||||||
Ein intelligentes Netz wächst durch strategische Verknüpfungen, nicht durch Textmenge.
|
Ein intelligentes Netz wächst durch strategische Verknüpfungen, nicht durch Textmenge.
|
||||||
|
|
||||||
### 3.1 Wissens-Hubs (Zentralnotizen)
|
### 4.1 Zentrale Kanten (Edges)
|
||||||
Hubs fungieren als Verteilerzentren im Graphen.
|
Nutze das kanonische Vokabular in `[!edge]` Callouts innerhalb der H3-Sektionen:
|
||||||
* **Struktur:** Nutze Überschriften (H2, H3) und verlinke von dort auf Detailnotizen.
|
* **`resulted_in` / `erzeugt`**: Verbindung zu einem daraus entstandenen Wert oder Gefühl.
|
||||||
* **Deep-Edges:** Verlinke präzise Abschnitte: `[[Rollenlandkarte#Vater]]`. Dies ermöglicht der KI, nur den relevanten Kontext zu laden.
|
* **`caused_by` / `wegen`**: Dokumentiert die Ursache einer emotionalen Prägung oder Entscheidung.
|
||||||
|
* **`part_of` / `gehört_zu`**: Bindet Details an einen übergeordneten Cluster oder Hub.
|
||||||
|
* **`guides` / `steuert`**: Prinzipien oder Werte, die eine Sektion oder ein Vorhaben leiten.
|
||||||
|
|
||||||
### 3.2 Forward-Mapping (Strategische Lücken)
|
### 4.2 Forward-Mapping (Strategische Lücken)
|
||||||
Setze bewusst Links auf Dateien, die noch nicht existieren (z. B. `[[Die beste Version meiner selbst]]`).
|
Setze bewusst Links auf Dateien, die noch nicht existieren (z. B. `[[Die beste Version meiner selbst]]`). Die KI erkennt diese Lücken und stellt proaktiv Fragen, um diese Felder gemeinsam mit dir zu füllen.
|
||||||
* **Zweck:** Du signalisierst dem System: „Hier entsteht ein wichtiges Konzept“.
|
|
||||||
* **Effekt:** Die KI erkennt diese Lücken und kann im Chat proaktiv Fragen stellen, um diese Felder mit dir gemeinsam zu füllen.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Obsidian Usability & Automatisierung
|
## 5. Das Kochbuch: Praktische Use-Cases
|
||||||
|
|
||||||
### 4.1 Templater-Integration
|
### 5.1 Ein Erlebnis aufschreiben (`type: experience`)
|
||||||
Nutze Vorlagen, die bereits die **Leitfragen** und das richtige **Retriever-Weight** enthalten. Ein Klick auf „Neues Erlebnis“ sollte automatisch das Frontmatter mit `type: experience` und `weight: 1.1` füllen.
|
**Ziel:** Den „Spiegel“ (Empathy) mit deiner Biografie kalibrieren.
|
||||||
|
* **Struktur:** Kontext (Was ist passiert?), Emotions-Check (Gefühle?), Lektion (Was gelernt?).
|
||||||
|
* **Deep-Edge:** Verknüpfe es immer mit einer Rolle: `[[rel:supports Meine Rollenlandkarte 2025#Vater]]`.
|
||||||
|
|
||||||
### 4.2 Meta Bind Dashboards
|
### 5.2 Eine Beobachtung festhalten (`type: insight`)
|
||||||
Nutze **Meta Bind**, um Felder wie `status` oder `retriever_weight` über Schieberegler direkt in der Notiz zu steuern. Das macht die Gewichtung deines Wissens intuitiv und spielerisch.
|
**Ziel:** Den „Berater“ (Decision) mit Mustern versorgen.
|
||||||
|
* **Beispiel:** "Beobachtung: Rohan reagiert positiv auf leise Impulse" -> Konsequenz: Prinzip Bedürfnischeck.
|
||||||
|
|
||||||
|
### 5.3 Das Handlungsprinzip (`type: principle`)
|
||||||
|
**Ziel:** Testbare Regeln für schwierige Situationen.
|
||||||
|
* **Struktur:** Das Prinzip, Anwendung & Beispiele, Wächterfrage (Was frage ich mich im Moment der Entscheidung?).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Vernetzung für die Mindnet-Personas
|
## 6. Obsidian Usability & Automatisierung
|
||||||
|
|
||||||
|
### 6.1 Templater & Meta Bind
|
||||||
|
* **Automatisierung:** Nutze Vorlagen, die bereits die Leitfragen und das richtige `retriever_weight` enthalten.
|
||||||
|
* **Interaktive Steuerung:** Nutze Meta Bind, um Felder wie `status` oder `retriever_weight` über Schieberegler direkt in der Notiz zu steuern.
|
||||||
|
|
||||||
|
### 6.2 Deep-Edges & Verknüpfung
|
||||||
|
* Verlinke Erlebnisse konsequent mit Rollen, Personen oder Clustern.
|
||||||
|
* Nutze `derived_from`, um Prinzipien an ihre Ursprungssituation zu binden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Vernetzung für die Mindnet-Personas
|
||||||
|
|
||||||
| Persona | Notiz-Fokus | Effekt im Dialog |
|
| Persona | Notiz-Fokus | Effekt im Dialog |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **🪞 Spiegel (Empathy)** | Gefühle & Biografisches | „Ich verstehe deinen Frust, das war bei Projekt X ähnlich...“. |
|
| **🪞 Spiegel (Empathy)** | `states`, `journal`, `experience` | Erzeugt Resonanz durch Nachempfinden deiner Gefühle. |
|
||||||
| **⚖️ Berater (Decision)** | Werte & Wächterfragen | „Option A ist nicht ratsam, da sie gegen dein Prinzip der Integrität verstößt“. |
|
| **⚖️ Berater (Decision)** | `values`, `principles`, `decisions` | Wägt aktuelle Fragen gegen deine lebenslangen Maßstäbe ab. |
|
||||||
| **📚 Bibliothekar (Facts)** | Struktur & Definitionen | „Deine Mission als Vater ist es, verlässliche Präsenz zu zeigen...“. |
|
| **📚 Bibliothekar (Facts)** | `events`, `concepts`, `sources`, `person` | Liefert präzise Fakten, historische Daten und soziale Kontexte. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> [!abstract] Fazit für den Autor
|
## 8. Verbindungen von Notiztypen (Graphen-Logik)
|
||||||
> Mindnet ist ein **Persönlichkeitsspiegel**. Dokumentiere weniger „Technik“ und mehr „Mensch“. Jede Notiz sollte die Frage beantworten: „Was sagt das über mich und meine Werte aus?“.
|
|
||||||
- **3 Signale:** Woran merke ich im Alltag, dass ich das Prinzip lebe?.
|
|
||||||
- **Wächterfrage:** Welche Frage stelle ich mir im Moment der Entscheidung?.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Obsidian Workflow-Hacks
|
|
||||||
|
|
||||||
### 6.1 Templater & Meta Bind
|
|
||||||
- **Automatisierung:** Nutze das **Templater-Plugin**, um beim Erstellen einer Notiz sofort die passenden Leitfragen und das richtige `retriever_weight` einzufügen.
|
|
||||||
- **Interaktive Steuerung:** Nutze **Meta Bind**, um Felder wie `status` oder `retriever_weight` über Buttons oder Slider direkt im Lesemodus zu ändern, ohne im YAML-Code zu tippen.
|
|
||||||
|
|
||||||
### 6.2 Deep-Edges & Verknüpfung
|
|
||||||
- Verknüpfe Erlebnisse immer mit der entsprechenden Rolle: `[[rel:supports Meine Rollenlandkarte 2025#Vater]]`.
|
|
||||||
- Nutze `derived_from`, um Prinzipien an ihre Ursprungs-Session zu binden.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Vernetzung für Mindnet Personas
|
|
||||||
|
|
||||||
| Persona | Notiz-Fokus | Beispiel |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| **🪞 Spiegel (Empathy)** | Emotionen & Prägungen | „Ich fühlte mich hilflos, als...“. |
|
|
||||||
| **⚖️ Berater (Decision)** | Kriterien & Abwägungen | „Option A verletzt Wert X, weil...“. |
|
|
||||||
| **📚 Bibliothekar (Facts)** | Struktur & Definition | „Rollenmission Vater bedeutet...“. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> [!tip] Best Practice
|
|
||||||
> Wenn du merkst, dass eine Notiz zu technisch wird: Halte inne und frage dich: „Was macht das mit mir als Mensch?“ Schreibe *das* auf. Das ist die Essenz für deinen KI-Zwilling.
|
|
||||||
|
|
||||||
## 8. Verbindungen von Notiztypen
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
|
|
@ -186,4 +162,3 @@ graph TD
|
||||||
style V fill:#f9f,stroke:#333,stroke-width:2px
|
style V fill:#f9f,stroke:#333,stroke-width:2px
|
||||||
style EX fill:#bbf,stroke:#333,stroke-width:2px
|
style EX fill:#bbf,stroke:#333,stroke-width:2px
|
||||||
style P fill:#bfb,stroke:#333,stroke-width:2px
|
style P fill:#bfb,stroke:#333,stroke-width:2px
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
394
docs/02_concepts/02_causal_chain_retrieving.md
Normal file
394
docs/02_concepts/02_causal_chain_retrieving.md
Normal file
|
|
@ -0,0 +1,394 @@
|
||||||
|
# Mindnet Causal Assistant – Dokumentation der bisher erreichten Resultate (0.4.x) + Architektur, Konfiguration & Strategien
|
||||||
|
|
||||||
|
> Stand: basierend auf den beobachteten Chain-Inspector-Logs und den zuletzt beschriebenen Implementierungen in 0.4.6/0.4.x.
|
||||||
|
> Ziel dieser Doku: Eine **einheitliche, belastbare Basis**, damit Weiterentwicklung (0.5.x/0.6.x) nicht mehr “im Kreis” läuft.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Zweck & Gesamtziel von Mindnet
|
||||||
|
|
||||||
|
Mindnet soll in einem Obsidian Vault **kausale/argumentative Zusammenhänge** als Graph abbilden und daraus **nützliche Diagnosen** ableiten:
|
||||||
|
|
||||||
|
- **Graph-Aufbau:** Notes/Sections als Knoten, Links/Kanten als gerichtete Beziehungen (z.B. *wirkt_auf*, *resulted_in*, *depends_on* …).
|
||||||
|
- **Analyse aus einem Kontext:** Nutzer steht in einer Note an einer bestimmten Überschrift/Section → Mindnet analysiert lokale Nachbarschaft + Pfade im Graphen.
|
||||||
|
- **Template Matching:** Einordnen der gefundenen Knoten/Kanten in “Kettenmuster” (Chain Templates) wie z.B. *trigger → transformation → outcome* oder *loop_learning*.
|
||||||
|
- **Findings (Gap-Heuristiken):** Hinweise wie “fehlende Slots”, “fehlende Links”, “unmapped edge types”, “einseitige Konnektivität”, etc.
|
||||||
|
→ Ziel: **Nutzer konkret zum besseren Graphen führen**, ohne “Noisy” zu sein.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Begriffe & Datenmodell (so arbeitet der Chain Inspector)
|
||||||
|
|
||||||
|
### 2.1 Kontext (Context)
|
||||||
|
Der Chain Inspector läuft immer gegen einen **aktuellen Kontext**:
|
||||||
|
- `file`: aktuelle Note (z.B. `Tests/03_insight_transformation.md`)
|
||||||
|
- `heading`: aktuelle Section (z.B. `Kern`)
|
||||||
|
- `zoneKind`: i.d.R. `content`
|
||||||
|
|
||||||
|
Das ist wichtig, weil Kanten teils **section-spezifisch** sind und teils (geplant/teilweise offen) **note-weit** gelten könnten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 Knoten (Nodes)
|
||||||
|
Ein Knoten ist im Report meist referenziert als:
|
||||||
|
- `file + heading` (z.B. `Tests/01_experience_trigger.md:Kontext`)
|
||||||
|
- plus abgeleitete Metadaten wie `noteType` (z.B. experience, insight, decision, event)
|
||||||
|
|
||||||
|
**noteType** ist entscheidend fürs Template Matching (Slots).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 Kanten (Edges)
|
||||||
|
Eine Kante hat typischerweise:
|
||||||
|
- `rawEdgeType`: Original-Typ aus Markdown/Notation (z.B. `wirkt_auf`, `resulted_in`, `depends_on`, `derived_from`, …)
|
||||||
|
- `from`: Quelle (file:heading)
|
||||||
|
- `to`: Ziel (file:heading)
|
||||||
|
- `scope`: Gültigkeit / Herkunft
|
||||||
|
- `section`: Edge ist “voll gültig” für die Section
|
||||||
|
- `candidate`: Edge ist nur Kandidat/unsicher, wird optional zugelassen
|
||||||
|
- (geplant/offen) `note`: Edge gilt note-weit (unabhängig von Section)
|
||||||
|
- `evidence`: Fundstelle (file, sectionHeading, lineRange)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Erreichte Resultate in 0.4.x (verifiziert)
|
||||||
|
|
||||||
|
### 3.1 includeCandidates – Kandidatenkanten funktionieren wie erwartet
|
||||||
|
**Ergebnis (bereits mehrfach in Logs verifiziert):**
|
||||||
|
- Wenn `includeCandidates=false`, werden Kanten mit `scope: candidate` **im effektiven Graphen ausgefiltert**.
|
||||||
|
- Wenn `includeCandidates=true`, werden Kandidatenkanten **als incoming/outgoing** berücksichtigt und tauchen in `neighbors`/`paths` auf.
|
||||||
|
|
||||||
|
**Implikation:**
|
||||||
|
- Das System kann “unsichere” oder “LLM-vorschlagene” Verbindungen existieren lassen, ohne in jedem Lauf die Analyse zu verfälschen.
|
||||||
|
- In “Discovery” kann man Kandidaten zulassen (mehr Explorationspower).
|
||||||
|
- In “Decisioning” kann man Kandidaten typischerweise sperren (mehr Verlässlichkeit).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 required_links: Strict vs Soft Mode – missing_link_constraints Unterdrückung ist umgesetzt
|
||||||
|
**Problem (historisch):**
|
||||||
|
- `missing_link_constraints` wurde teilweise auch dann ausgegeben, wenn `required_links=false` (Soft Mode) aktiv war → unnötig “noisy”.
|
||||||
|
|
||||||
|
**Fix (laut Cursor-Report umgesetzt + Tests):**
|
||||||
|
- `missing_link_constraints` wird nur erzeugt, wenn `effectiveRequiredLinks === true`.
|
||||||
|
- Es gibt eine definierte Auflösungsreihenfolge für `required_links`:
|
||||||
|
|
||||||
|
**Resolution Order (effective required_links):**
|
||||||
|
1. `template.matching?.required_links`
|
||||||
|
2. `profile.required_links`
|
||||||
|
3. `defaults.matching?.required_links`
|
||||||
|
4. Fallback: `false`
|
||||||
|
|
||||||
|
**Transparenz bleibt erhalten:**
|
||||||
|
- `satisfiedLinks` und `requiredLinks` werden weiterhin im Report angezeigt.
|
||||||
|
- `linksComplete` bleibt als technischer Wert im Report bestehen.
|
||||||
|
- **Nur** das Finding `missing_link_constraints` wird unterdrückt, nicht die Fakten.
|
||||||
|
|
||||||
|
**Implikation:**
|
||||||
|
- Soft Mode (= required_links=false) ist jetzt ruhig genug, um “Entdeckung” zu unterstützen.
|
||||||
|
- Strict Mode (= required_links=true) eignet sich für harte Qualitätskontrolle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 “Healthy graph” → Findings leer ([]), Template “confirmed”
|
||||||
|
Wenn Slots **und** geforderte Links erfüllt sind (z.B. `trigger_transformation_outcome` mit 2/2 Links),
|
||||||
|
dann ist `findings: []` das erwartete Ergebnis.
|
||||||
|
|
||||||
|
**Implikation:**
|
||||||
|
- Das ist das zentrale “Green Path”-Signal: Graph ist konsistent für das gewählte Template/Profil.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 Unmapped edges werden erkannt (Diagnose)
|
||||||
|
Wenn ein `rawEdgeType` nicht in die kanonischen Rollen/Edge-Rollen abgebildet werden kann, tauchen typischerweise Diagnosen auf:
|
||||||
|
- `edgesUnmapped > 0`
|
||||||
|
- Findings wie `no_causal_roles` oder link constraints bleiben unerfüllt
|
||||||
|
|
||||||
|
**Implikation:**
|
||||||
|
- Das Rollen-Mapping (chain_roles.yaml) ist “critical path”: wenn ein Edge-Typ nicht gemappt wird, bricht oft der kausale Interpretationspfad.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Was ist (noch) offen – echtes nächstes Verifikationsziel
|
||||||
|
|
||||||
|
### 4.1 Note-Level Edges / Note-Scope (“für jede Sektion gültig”)
|
||||||
|
Es existiert ein Konzept/Einbau: In `02_event_trigger_detail` gibt es einen Bereich, der Kanten **auf Note-Ebene** definieren soll (unabhängig von aktueller Section).
|
||||||
|
|
||||||
|
**Offen ist die robuste Verifikation (oder Implementierung), dass:**
|
||||||
|
- diese Edges auch dann gelten, wenn der Cursor in einer anderen Section derselben Note steht,
|
||||||
|
- idealerweise mit klar erkennbarer Kennzeichnung wie `scope: note` im Report.
|
||||||
|
|
||||||
|
**Warum ist das wichtig?**
|
||||||
|
- Das ermöglicht “globaler Kontext” pro Note, ohne alles in jede Section duplizieren zu müssen.
|
||||||
|
- Es ist eine UX-Optimierung: Nutzer kann “Meta-Verbindungen” an einer Stelle pflegen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) Konfigurationsdateien – Rolle & Interpretation (Mindnet-Strategie)
|
||||||
|
|
||||||
|
> Die folgenden Bereiche beschreiben eine **saubere, konsistente Interpretation**, wie Mindnet die Configs verwenden sollte.
|
||||||
|
> Konkrete Keys, die in Logs sichtbar waren (z.B. required_links, min_slots_filled_for_gap_findings, min_score_for_gap_findings) sind hier berücksichtigt.
|
||||||
|
|
||||||
|
### 5.1 chain_templates.yaml – “Welche Ketten gibt es, wie werden sie gematcht?”
|
||||||
|
**Zweck:**
|
||||||
|
- Definiert Templates (Muster), z.B.:
|
||||||
|
- `trigger_transformation_outcome`
|
||||||
|
- `loop_learning`
|
||||||
|
- ggf. weitere (constraint_to_adaptation usw.)
|
||||||
|
|
||||||
|
**Template enthält typischerweise:**
|
||||||
|
- Slots (Rollen für Knoten): z.B. `trigger`, `transformation`, `outcome`, `experience`, `learning`, `behavior`, `feedback`
|
||||||
|
- Required Link Constraints (welche Slot-zu-Slot Verbindungen zwingend sind)
|
||||||
|
- Scoring/Matching-Parameter (ggf. weights, thresholds)
|
||||||
|
- Optional: template-level override für `required_links`
|
||||||
|
|
||||||
|
**Matching-Profile (wie in Logs sichtbar):**
|
||||||
|
- z.B. Profile: `discovery`, `decisioning`
|
||||||
|
- Parameter im Profil (sichtbar in Logs):
|
||||||
|
- `required_links` (strict vs soft)
|
||||||
|
- `min_slots_filled_for_gap_findings`
|
||||||
|
- `min_score_for_gap_findings`
|
||||||
|
- ggf. `maxTemplateMatches`
|
||||||
|
|
||||||
|
**Interpretation:**
|
||||||
|
- Templates liefern die “Soll-Struktur”
|
||||||
|
- Profile bestimmen “Wie streng” wir die Soll-Struktur im jeweiligen Workflow bewerten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 chain_roles.yaml – “Welche rawEdgeTypes zählen als welche Rollen?”
|
||||||
|
**Zweck:**
|
||||||
|
- Mappt `rawEdgeType` → kanonische Rollen/EdgeRoles (z.B. `causal`, `influences`, `enables_constraints`, `provenance`).
|
||||||
|
- Diese Rollen sind Grundlage für:
|
||||||
|
- `no_causal_roles` Finding
|
||||||
|
- Link-Constraint-Satisfaction (Template erwartet “causal” zwischen Slots)
|
||||||
|
- Matching Score (welche Edges zählen für welches Template)
|
||||||
|
|
||||||
|
**Interpretation:**
|
||||||
|
- Wenn ein Edge-Typ nicht gemappt ist:
|
||||||
|
- Edge kann trotzdem im Graph auftauchen,
|
||||||
|
- aber Template/Constraint-Logik kann ihn nicht “verstehen” → führt zu Findings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 analysis_policies.yaml – “Wie noisy dürfen Findings sein?”
|
||||||
|
**Zweck:**
|
||||||
|
- Zentrale Policies für Findings:
|
||||||
|
- welche Finding-Codes existieren
|
||||||
|
- Default-Severity (info/warn/error)
|
||||||
|
- Profilabhängige Overrides
|
||||||
|
- Unterdrückungsregeln (z.B. suppress in soft mode, suppress wenn confirmed, suppress wenn Score hoch…)
|
||||||
|
|
||||||
|
**Interpretation:**
|
||||||
|
- Policies sind “produktseitige UX-Regeln”:
|
||||||
|
- Discovery: eher informativ, weniger warn
|
||||||
|
- Decisioning: klare Warnungen, wenn Qualität fehlt
|
||||||
|
- Der bereits umgesetzte Fix (`missing_link_constraints` nur in strict) ist exakt so eine Policy-Entscheidung (auch wenn technisch im Inspector gelöst).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) Ablauf des Chain Inspectors (Vorgehensweise in Mindnet)
|
||||||
|
|
||||||
|
Hier ist ein konsistenter “Pipeline”-Ablauf, der zu den Logs passt:
|
||||||
|
|
||||||
|
### Schritt 1: Kontext bestimmen
|
||||||
|
- Aktuelle Datei + aktuelle Section/Heading
|
||||||
|
|
||||||
|
### Schritt 2: Edges aus aktueller Note laden
|
||||||
|
- Outgoing aus der aktuellen Section extrahieren (oder aus einem definierten Block)
|
||||||
|
- (optional/offen) Note-Level Edges ebenfalls laden und für jede Section gültig machen
|
||||||
|
|
||||||
|
### Schritt 3: Nachbarn laden
|
||||||
|
- Backlinks (Notes, die auf die aktuelle Note verlinken) → incoming Kandidatenquellen
|
||||||
|
- Outgoing Neighbor Notes (Notes, auf die aktuelle Note verweist) → Nachbarschaft erweitern
|
||||||
|
|
||||||
|
### Schritt 4: Edges aus Neighbor Notes laden
|
||||||
|
- Aus den verlinkenden Notes die Edges extrahieren, die auf die aktuelle Note/Section zielen
|
||||||
|
- Canonicalization: rawEdgeTypes via chain_roles.yaml in Rollen überführen
|
||||||
|
|
||||||
|
### Schritt 5: Kandidatenfilter / Scopefilter anwenden
|
||||||
|
- Wenn `includeCandidates=false`:
|
||||||
|
- `scope: candidate` aus effective graph entfernen
|
||||||
|
- Optional weitere Filter:
|
||||||
|
- includeNoteLinks / includeSectionLinks
|
||||||
|
- direction (forward/backward/both)
|
||||||
|
- maxDepth (Traversal)
|
||||||
|
|
||||||
|
### Schritt 6: Pfade berechnen (Paths)
|
||||||
|
- Forward/Backward (oder both)
|
||||||
|
- BFS/DFS bis `maxDepth`
|
||||||
|
- Resultat: Pfadlisten mit nodes + edges
|
||||||
|
|
||||||
|
### Schritt 7: Template Matching
|
||||||
|
- Kandidatenknoten für Slots finden (via noteType + Nähe + Pfade)
|
||||||
|
- Links/Constraints prüfen (erwartete slot→slot Beziehungen)
|
||||||
|
- Score berechnen (z.B. per:
|
||||||
|
- Slots erfüllt
|
||||||
|
- Link constraints erfüllt
|
||||||
|
- “RoleEvidence” passend)
|
||||||
|
|
||||||
|
### Schritt 8: Findings berechnen (Gap-Heuristics)
|
||||||
|
Beispiele:
|
||||||
|
- `missing_slot_*` wenn wichtige Slots fehlen (abhängig von Profil-Thresholds)
|
||||||
|
- `one_sided_connectivity` wenn nur incoming oder nur outgoing
|
||||||
|
- `no_causal_roles` wenn Edges da, aber keine causal Rollen im effektiven Graph
|
||||||
|
- `missing_link_constraints` nur wenn effectiveRequiredLinks=true und slotsComplete=true, requiredLinks>0, linksComplete=false
|
||||||
|
|
||||||
|
### Schritt 9: Report ausgeben
|
||||||
|
- context, settings, neighbors, paths, findings, analysisMeta, templateMatches
|
||||||
|
- Transparenz: satisfiedLinks/requiredLinks/linksComplete bleiben sichtbar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7) Strategien, die Mindnet verfolgen kann (Produkt-/UX-Strategie)
|
||||||
|
|
||||||
|
### Strategie A: Discovery (Exploration)
|
||||||
|
**Ziel:** Möglichst schnell “wo könnte eine sinnvolle Kette entstehen?” finden.
|
||||||
|
- required_links = false (Soft Mode)
|
||||||
|
- includeCandidates = true (optional)
|
||||||
|
- findings eher informativ (info), weniger warn
|
||||||
|
- Templates mehr als Vorschläge (“plausible/weak”), nicht als harte Bewertung
|
||||||
|
|
||||||
|
**Vorteil:** Nutzer bekommt schnell Hypothesen.
|
||||||
|
**Risiko:** Mehr Noise, mehr falsche Kandidaten – muss per Policy gedämpft werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Strategie B: Decisioning (Qualitätskontrolle)
|
||||||
|
**Ziel:** Prüfung, ob eine Kette “wirklich steht” und als belastbar gelten kann.
|
||||||
|
- required_links = true (Strict Mode)
|
||||||
|
- includeCandidates = false
|
||||||
|
- findings: warn, wenn Slots/Links fehlen
|
||||||
|
- “confirmed” nur wenn link constraints komplett
|
||||||
|
|
||||||
|
**Vorteil:** Qualitätssicherung & Verlässlichkeit.
|
||||||
|
**Risiko:** Nutzer fühlt sich “blockiert”, wenn Graph noch im Aufbau ist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Strategie C: Progressive Disclosure (hybrid)
|
||||||
|
**Ziel:** Nutzer nicht überfordern, aber zielgerichtet verbessern.
|
||||||
|
- Soft Mode für Einstieg
|
||||||
|
- Button/Toggle: “Strict prüfen”
|
||||||
|
- Candidate Edges als Vorschlag-Klasse (UI: “proposed edges”)
|
||||||
|
- Findings priorisieren: erst fehlende Slots, dann fehlende Links, dann Detail-Qualität
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8) Wie ein “kausaler Retriever” funktionieren könnte (Causal Retriever)
|
||||||
|
|
||||||
|
Ein kausaler Retriever ist die Komponente, die aus dem Vault/Graphen **relevante Kausalkontexte** für den aktuellen Abschnitt liefert – idealerweise deterministisch, skalierbar und template-aware.
|
||||||
|
|
||||||
|
### 8.1 Retrieval-Ziele
|
||||||
|
- Finde Knoten/Edges, die **kausal relevant** sind zum aktuellen Kontext:
|
||||||
|
- Ursachen (backward)
|
||||||
|
- Wirkungen/Entscheidungen (forward)
|
||||||
|
- Bedingungen/Constraints (seitlich)
|
||||||
|
- Gib nicht nur Knoten zurück, sondern:
|
||||||
|
- Pfade (explainable)
|
||||||
|
- Evidence (wo steht das)
|
||||||
|
- Role-Interpretation (warum ist das causal/influences/etc.)
|
||||||
|
|
||||||
|
### 8.2 Retrieval-Inputs
|
||||||
|
- startNode = current section
|
||||||
|
- direction = forward/backward/both
|
||||||
|
- maxDepth
|
||||||
|
- roleFilter (optional): nur causal/influences/enables_constraints
|
||||||
|
- scopeFilter: includeCandidates, includeNoteLevel
|
||||||
|
- templateBias: bevorzugte Pfadformen (z.B. “experience→insight→decision”)
|
||||||
|
|
||||||
|
### 8.3 Retrieval-Algorithmus (praktisch)
|
||||||
|
**Variante 1: BFS mit Rolle-Gewichtung**
|
||||||
|
- BFS über Kanten
|
||||||
|
- Priorität/Score pro Frontier:
|
||||||
|
- causal > influences > provenance
|
||||||
|
- section-scope > note-scope > candidate (wenn candidates eingeschaltet, sonst candidate=∞)
|
||||||
|
- Stop, wenn:
|
||||||
|
- maxDepth erreicht
|
||||||
|
- genug Top-N Pfade gesammelt (z.B. topNUsed)
|
||||||
|
|
||||||
|
**Variante 2: Template-driven Retrieval**
|
||||||
|
- Wenn ein Template im Fokus ist:
|
||||||
|
- suche explizit nach Slot-Knoten (noteType matching)
|
||||||
|
- suche dann die minimalen Verbindungen, die Constraints erfüllen
|
||||||
|
- Gute Option für “Decisioning”: deterministisch prüfen.
|
||||||
|
|
||||||
|
**Variante 3: Two-phase Retrieval**
|
||||||
|
1) Kandidaten finden (Slots)
|
||||||
|
2) Verbindungen prüfen (Constraints)
|
||||||
|
→ Liefert sehr gut “warum fehlt Link X?” Diagnosen.
|
||||||
|
|
||||||
|
### 8.4 Output-Format
|
||||||
|
- `neighbors` (incoming/outgoing, mit evidence)
|
||||||
|
- `paths` (forward/backward, nodes+edges)
|
||||||
|
- plus “slot candidates” optional (für UI)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9) Empfehlungen für robuste Tests (damit ihr nicht wieder im Kreis lauft)
|
||||||
|
|
||||||
|
### Was ist bereits ausreichend getestet (nicht wiederholen)
|
||||||
|
- includeCandidates Filterverhalten ✅
|
||||||
|
- missing_link_constraints Unterdrückung bei required_links=false ✅
|
||||||
|
- strict/soft required_links via profile/template override ✅
|
||||||
|
- “healthy graph” ergibt findings: [] ✅
|
||||||
|
- unmapped edge type triggert Diagnose ✅
|
||||||
|
|
||||||
|
### Was als einziges “neues” Testziel für Abschluss 0.4.x/Start 0.5.x taugt
|
||||||
|
- **Note-level edges / note-scope**: gelten Kanten “global” pro Note oder nicht?
|
||||||
|
|
||||||
|
**Minimal-Testdefinition (einmalig, reproduzierbar):**
|
||||||
|
1) In `02_event_trigger_detail.md` einen klaren Note-Level Block definieren (z.B. “## Note-Verbindungen”).
|
||||||
|
2) Edge dort definieren, die auf eine andere Note/Section zeigt.
|
||||||
|
3) Cursor in einer anderen Section derselben Note platzieren (z.B. “## Detail” oder “## Extra”).
|
||||||
|
4) Chain Inspector laufen lassen.
|
||||||
|
5) Erwartung:
|
||||||
|
- Edge erscheint trotzdem als outgoing/incoming
|
||||||
|
- evidence zeigt auf den Note-Level Block
|
||||||
|
- ideal: `scope: note`
|
||||||
|
|
||||||
|
Wenn das FAIL ist → klarer 0.5.0 Task.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10) Implikationen für 0.5.x / 0.6.x (wohin sinnvoll weiter)
|
||||||
|
|
||||||
|
### 0.5.x (Stabilisierung)
|
||||||
|
- Note-level edge scope finalisieren (inkl. Report-Transparenz)
|
||||||
|
- policies (analysis_policies) als zentrale Noise-Steuerung weiter ausbauen
|
||||||
|
- Debug/Explainability weiter verbessern (effectiveRequiredLinks pro Match explizit ausgeben)
|
||||||
|
|
||||||
|
### 0.6.x (UX & Workflows)
|
||||||
|
- Actionable Findings: “Was genau soll ich ändern?” inkl. Vorschlagtext oder Snippet
|
||||||
|
- UI-Toggles: Strict/Soft, Candidates on/off
|
||||||
|
- Template Authoring Tools: Linter, “Warum kein Match?”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11) Kurzes “Was heißt das für Mindnet im Alltag?”
|
||||||
|
- Im Discovery-Modus: Mindnet ist ein **Explorationswerkzeug** (Hypothesen + Hinweise, wenig Warnungen).
|
||||||
|
- Im Decisioning-Modus: Mindnet ist ein **Qualitätsprüfer** (strict, wenige false positives).
|
||||||
|
- Der nächste große Hebel ist Note-scope: Damit wird Pflege einfacher und Ketten werden “wartbarer”.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12) Appendix: Beispielhafte Report-Signale (Interpretationshilfe)
|
||||||
|
|
||||||
|
- `findings: []` + `confidence: confirmed`
|
||||||
|
→ Template passt sauber (Slots + Links vollständig im gewählten Modus).
|
||||||
|
|
||||||
|
- `linksComplete=false` aber `required_links=false` und **kein** `missing_link_constraints`
|
||||||
|
→ Soft Mode: bewusst kein “Warn-Noise”, aber Transparenz bleibt.
|
||||||
|
|
||||||
|
- `no_causal_roles`
|
||||||
|
→ Edges existieren, aber keine davon wird als “causal” interpretiert (Mapping oder rawEdgeType Problem).
|
||||||
|
|
||||||
|
- `edgesUnmapped > 0`
|
||||||
|
→ chain_roles unvollständig oder Edge-Typ ist neu/fehlerhaft geschrieben.
|
||||||
|
|
||||||
|
- `effectiveIncoming=0` bei includeCandidates=false, aber incoming candidate-edge existiert
|
||||||
|
→ Filter funktioniert wie geplant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
ENDE
|
||||||
|
|
@ -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.
|
||||||
223
docs/02_concepts/02_concept_kausales_retrieval.md
Normal file
223
docs/02_concepts/02_concept_kausales_retrieval.md
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
<!-- FILE: konzept_zielbild_kausales_retrieval_mindnet.md -->
|
||||||
|
---
|
||||||
|
id: konzept_zielbild_kausales_retrieval_mindnet
|
||||||
|
title: Konzept & Zielbild – Kausalketten-Prüfung und kausales Retrieval für Mindnet (Qdrant)
|
||||||
|
type: concept
|
||||||
|
status: draft
|
||||||
|
created: 2026-01-13
|
||||||
|
lang: de
|
||||||
|
tags:
|
||||||
|
- mindnet
|
||||||
|
- obsidian
|
||||||
|
- knowledge_graph
|
||||||
|
- causal_chains
|
||||||
|
- retrieval
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Konzept & Zielbild – Kausalketten-Prüfung und kausales Retrieval für Mindnet (Qdrant)
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
Mindnet soll zu beliebigen Fragestellungen **die richtigen Notizen** nicht nur über semantische Nähe (Embeddings), sondern über **kausale Relevanz** finden.
|
||||||
|
Parallel soll ein Authoring-Assistent helfen, Obsidian-Notizen so anzulegen, dass Kausalketten **formal konsistent** und **traversierbar** sind.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ausgangslage / Problem
|
||||||
|
- Der Wissensgraph wird in Qdrant gepflegt; aktuelles Retrieval basiert primär auf **Gewichtung + semantischer Nähe**.
|
||||||
|
- Ergebnis: thematisch nahe Treffer, aber oft **nicht antwortrelevant** (fehlende Ursachen-/Folgenbezüge).
|
||||||
|
- Obsidian-Notizen enthalten Edges (Vorwärts/Rückwärts); Qualität hängt von:
|
||||||
|
- korrekter Relation (Kausalität vs Chronologie),
|
||||||
|
- konsistenten Node-Namen,
|
||||||
|
- Inversen (gegenläufigen Beziehungen),
|
||||||
|
- sauberer Typisierung ab.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Grundannahmen
|
||||||
|
- Viele Antworten benötigen einen **Erklärungspfad** statt eines Einzel-Treffers:
|
||||||
|
- Ursache → Mechanismus/Transformation → Entscheidung → Wirkung → Rückkopplung
|
||||||
|
- Kausalität ist im Graph als gerichtete Kanten modelliert und über inverse Typen **bidirektional navigierbar**:
|
||||||
|
- `resulted_in` ⇄ `caused_by`
|
||||||
|
- `followed_by` ⇄ `preceeded_by`
|
||||||
|
- `derived_from` ⇄ `source_of`
|
||||||
|
- `impacts` ⇄ `impacted_by`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System-Zielbild (2 Hauptkomponenten)
|
||||||
|
|
||||||
|
### 1) Authoring-Assistent (Obsidian Graph Linter + Chain Explorer)
|
||||||
|
Zweck: Qualitätssicherung beim Erstellen/Ändern von Notizen.
|
||||||
|
|
||||||
|
**Kernfunktionen**
|
||||||
|
- **Formale Prüfungen**
|
||||||
|
- Canonical Edge vs Alias (Normalisierung nach `edge_vocabulary`)
|
||||||
|
- Zielnoten existieren / leere Links als `open_question` oder TODO markieren
|
||||||
|
- Tippfehler/Node-Splitting erkennen (mehrere Schreibweisen desselben Knoten)
|
||||||
|
- Edge-Typ zulässig für Note-Typ (z.B. keine Kausal-Edges aus `open_question`)
|
||||||
|
- **Semantische Plausibilität (regelbasiert)**
|
||||||
|
- Chronologie (`followed_by`) ≠ Kausalität (`resulted_in`)
|
||||||
|
- Hub-/Index-Noten nutzen primär `related_to/consists_of` statt Kausalität
|
||||||
|
- Prinzipien bevorzugt `derived_from/based_on` statt pauschal `caused_by`
|
||||||
|
- **Ketten-Integrität**
|
||||||
|
- „Gap“-Warnungen (Sprünge ohne Zwischennoten)
|
||||||
|
- Zyklen ohne Sinn (A caused_by B und B caused_by A)
|
||||||
|
- Mehrfachursachen transparent markieren
|
||||||
|
|
||||||
|
**Outputs**
|
||||||
|
- Lint-Report pro Note (Fehler/Warnung/Empfehlung)
|
||||||
|
- Chain-Preview (2–4 Schritte vorwärts/rückwärts)
|
||||||
|
- Optional: Auto-Fix-Vorschläge (Alias→Canonical, Link-Normalisierung, Inversen ergänzen)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2) Mindnet Retrieval: Hybrid aus Embeddings + Graph Traversal + Reranking
|
||||||
|
Zweck: Aus einer Frage automatisch eine **kleine, kausal zusammenhängende** Menge von Notizen auswählen.
|
||||||
|
|
||||||
|
**Pipeline**
|
||||||
|
1. **Seed Retrieval (Qdrant Embeddings)**
|
||||||
|
- Top-K Kandidaten (z.B. 30) als Startpunkte
|
||||||
|
- Optional: Filter nach Node-Typ (z.B. bei „Welche Entscheidungen…“)
|
||||||
|
|
||||||
|
2. **Intent-Klassifikation (Frage → Richtung & Kettenform)**
|
||||||
|
- Regelbasiert (Start) oder später ML-Classifier
|
||||||
|
- Output: `{direction, preferred_edges, target_types, max_hops, need_explanation_chain}`
|
||||||
|
|
||||||
|
3. **Graph Expansion (Multi-Source Multi-Hop Traversal)**
|
||||||
|
- Expandiert von Seeds 1–3 Hops (typisch 2–4)
|
||||||
|
- Richtungslogik:
|
||||||
|
- „Warum/Ursache“ → rückwärts (`caused_by`, `preceeded_by`, `derived_from`)
|
||||||
|
- „Folgen/Ergebnis“ → vorwärts (`resulted_in`, `followed_by`, `impacts`)
|
||||||
|
- „Entwicklung/Veränderung“ → beides (forward + backward)
|
||||||
|
- Ergebnis: Pfad-Kandidaten (nicht nur Nodes)
|
||||||
|
|
||||||
|
4. **Reranking (Antwortrelevanz)**
|
||||||
|
- Score = Semantik + Pfadqualität + Antwortform-Passung
|
||||||
|
|
||||||
|
5. **Antwort-Bausteine (Minimal Explanation Subgraph)**
|
||||||
|
- Merged Top-Pfade zu einem kleinen Subgraph (z.B. 8–12 Nodes)
|
||||||
|
- Pruning nach zentralen Knoten und erklärender Kettenform
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spezifikation: Intent → Traversal Mode (Heuristik)
|
||||||
|
|
||||||
|
### Intent-Struktur
|
||||||
|
- `direction`: `backward | forward | both`
|
||||||
|
- `preferred_edges`: Menge Edge-Typen
|
||||||
|
- `target_types`: Menge Node-Typen
|
||||||
|
- `max_hops`: int
|
||||||
|
- `need_explanation_chain`: bool
|
||||||
|
|
||||||
|
### Heuristik (Deutsch)
|
||||||
|
- **Warum / Ursache / Auslöser / wodurch / wie kam es dazu**
|
||||||
|
- direction: backward
|
||||||
|
- preferred_edges: `{caused_by, preceeded_by, derived_from}`
|
||||||
|
- target_types: `{experience, decision, strategy, state}`
|
||||||
|
- max_hops: 2–4
|
||||||
|
- **Was führte zu / Folgen / Auswirkungen / resultierte in**
|
||||||
|
- direction: forward
|
||||||
|
- preferred_edges: `{resulted_in, followed_by, impacts}`
|
||||||
|
- target_types: `{decision, strategy, state, principle}`
|
||||||
|
- max_hops: 2–4
|
||||||
|
- **Entwicklung / Veränderung / Weltbild / Glaubenssatz / Charakter**
|
||||||
|
- direction: both
|
||||||
|
- preferred_edges backward: `{caused_by, derived_from}`
|
||||||
|
- preferred_edges forward: `{resulted_in, impacts}`
|
||||||
|
- target_types: `{principle, state, strategy, decision}`
|
||||||
|
- max_hops: 2–4
|
||||||
|
- need_explanation_chain: true
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spezifikation: Traversal (Weighted Multi-Hop)
|
||||||
|
|
||||||
|
### Gewichte (Startwerte)
|
||||||
|
**Edge Weights**
|
||||||
|
- `resulted_in`: 1.00
|
||||||
|
- `caused_by`: 1.00
|
||||||
|
- `derived_from`: 0.90
|
||||||
|
- `source_of`: 0.90
|
||||||
|
- `impacts`: 0.70
|
||||||
|
- `impacted_by`: 0.70
|
||||||
|
- `followed_by`: 0.50
|
||||||
|
- `preceeded_by`: 0.50
|
||||||
|
- `related_to`: 0.25
|
||||||
|
- `part_of/consists_of`: 0.25
|
||||||
|
|
||||||
|
**Node-Type Weights**
|
||||||
|
- `experience`: 1.00
|
||||||
|
- `decision`: 1.00
|
||||||
|
- `strategy`: 0.90
|
||||||
|
- `state`: 0.85
|
||||||
|
- `principle`: 0.85
|
||||||
|
- `insight(hub)`: 0.35
|
||||||
|
- `open_question/hypothesis/white_spot`: 0.00 (Filter)
|
||||||
|
|
||||||
|
**Hop Decay**
|
||||||
|
- `hop_decay(h) = 0.75^h`
|
||||||
|
|
||||||
|
### Traversal-Logik (pseudocode-nah)
|
||||||
|
- Multi-Source-Expansion ab Seeds
|
||||||
|
- Pfade priorisiert nach kumuliertem Pfadscore
|
||||||
|
- `visited` verhindert endlose Wiederholungen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spezifikation: Reranking (Semantik + Kausalität + Antwortform)
|
||||||
|
|
||||||
|
### Final Score
|
||||||
|
- `final_score(path) = alpha*semantic + beta*coherence + gamma*shape_match`
|
||||||
|
|
||||||
|
Startwerte:
|
||||||
|
- `alpha = 0.55` (Semantik)
|
||||||
|
- `beta = 0.30` (Kausal-Kohärenz)
|
||||||
|
- `gamma = 0.15` (Passung zur Frageform)
|
||||||
|
|
||||||
|
**Causal Coherence**
|
||||||
|
- Bonus, wenn Pfad Kausal-Edges enthält (`resulted_in/caused_by/derived_from`)
|
||||||
|
- Malus, wenn nur Navigation/Chronologie enthalten ist
|
||||||
|
- Bonus für Kernform: `experience → decision → (state|strategy|principle)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output: Minimal Explanation Subgraph (MES)
|
||||||
|
Ziel: nicht eine Liste, sondern ein erklärendes Subgraph-Set.
|
||||||
|
|
||||||
|
**Regeln**
|
||||||
|
- Top-Pfade (z.B. 3–5) mergen
|
||||||
|
- max_nodes: 8–12
|
||||||
|
- Pruning:
|
||||||
|
- Hubs raus, wenn sie nur Navigation sind
|
||||||
|
- Decision/Principle/State bevorzugen (Antwortanker)
|
||||||
|
- Bridge-Nodes behalten (in mehreren Pfaden vorkommend)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authoring-Regeln (Graph-Hygiene) – harte Leitplanken
|
||||||
|
1. Kausalität nur auf atomaren Noten (`experience/decision/state/strategy/principle`)
|
||||||
|
2. Hubs/Indexnoten: primär `related_to/consists_of` (keine „Hub verursacht X“-Kausalität)
|
||||||
|
3. Inverse Edges müssen erzeugbar sein (oder Build-Step erzeugt sie deterministisch)
|
||||||
|
4. Chronologie strikt trennen (`followed_by` ≠ `resulted_in`)
|
||||||
|
5. Prinzipien: `derived_from/based_on` für Herkunft (statt pauschal `caused_by`)
|
||||||
|
6. Leere Links als `open_question` oder TODO ohne Kausal-Edge
|
||||||
|
7. Kanonische Dateinamen: Node-Splitting verhindern
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nutzen / Erfolgskriterien
|
||||||
|
- **Bessere Answer Relevance**: Mindnet liefert Knoten mit erklärender Kausalstruktur statt nur thematischer Nähe
|
||||||
|
- **Erklärbarkeit**: Antwort kann mit Pfad(en) begründet werden
|
||||||
|
- **Debuggability**: Fehlantworten lassen sich auf falsche/fehlende Kanten zurückführen
|
||||||
|
- **Authoring-Effizienz**: Assistent verhindert typische Edge-Fehler früh
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offene Punkte (für nächste Iteration)
|
||||||
|
- Intent-Taxonomie (8–12 Frageklassen) finalisieren und evaluieren
|
||||||
|
- Welche Edges werden als „kausal“ im engeren Sinne akzeptiert?
|
||||||
|
- Welche Node-Typen sind Pflichtmetadaten für Mindnet?
|
||||||
|
- Evaluation: Retrieval-Qualität mit/ohne Traversal (A/B)
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
105
docs/03_Technical_References/ENV_LOADING_DEBUG.md
Normal file
105
docs/03_Technical_References/ENV_LOADING_DEBUG.md
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
# Debug: .env-Lade-Problem in Prod
|
||||||
|
|
||||||
|
**Datum**: 2026-01-12
|
||||||
|
**Version**: v4.5.10
|
||||||
|
**Status**: 🔴 Kritisch
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Möglicherweise wird die `.env`-Datei in Prod nicht korrekt geladen, was zu:
|
||||||
|
- Falschen Log-Levels (DEBUG=true wird ignoriert)
|
||||||
|
- Falschen Collection-Präfixen
|
||||||
|
- Falschen Konfigurationen
|
||||||
|
führen kann.
|
||||||
|
|
||||||
|
## Diagnose
|
||||||
|
|
||||||
|
### Schritt 1: Prüfe, ob .env-Datei existiert
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In Prod
|
||||||
|
cd ~/mindnet
|
||||||
|
ls -la .env
|
||||||
|
cat .env | head -20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 2: Prüfe Arbeitsverzeichnis beim Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In Prod - prüfe, von wo uvicorn gestartet wird
|
||||||
|
ps aux | grep uvicorn
|
||||||
|
# Oder in systemd service:
|
||||||
|
cat /etc/systemd/system/mindnet.service | grep WorkingDirectory
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 3: Verifikations-Script ausführen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In Prod
|
||||||
|
cd ~/mindnet
|
||||||
|
source .venv/bin/activate
|
||||||
|
python3 scripts/verify_env_loading.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erwartete Ausgabe**:
|
||||||
|
```
|
||||||
|
✅ .env geladen von: /path/to/mindnet/.env
|
||||||
|
✅ COLLECTION_PREFIX = mindnet
|
||||||
|
✅ DEBUG = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 4: Manuelle Verifikation
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In Python-REPL in Prod
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Prüfe aktuelles Verzeichnis
|
||||||
|
print(f"CWD: {Path.cwd()}")
|
||||||
|
print(f"Projekt-Root: {Path(__file__).parent.parent.parent}")
|
||||||
|
|
||||||
|
# Lade .env
|
||||||
|
env_file = Path(".env")
|
||||||
|
if env_file.exists():
|
||||||
|
load_dotenv(env_file, override=True)
|
||||||
|
print(f"✅ .env geladen: {env_file.absolute()}")
|
||||||
|
else:
|
||||||
|
print(f"❌ .env nicht gefunden in: {env_file.absolute()}")
|
||||||
|
|
||||||
|
# Prüfe kritische Variablen
|
||||||
|
print(f"DEBUG: {os.getenv('DEBUG', 'NICHT GESETZT')}")
|
||||||
|
print(f"COLLECTION_PREFIX: {os.getenv('COLLECTION_PREFIX', 'NICHT GESETZT')}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mögliche Ursachen
|
||||||
|
|
||||||
|
### 1. Arbeitsverzeichnis-Problem
|
||||||
|
- **Problem**: uvicorn wird aus einem anderen Verzeichnis gestartet
|
||||||
|
- **Lösung**: Expliziter Pfad in `config.py` (bereits implementiert)
|
||||||
|
|
||||||
|
### 2. .env-Datei nicht im Projekt-Root
|
||||||
|
- **Problem**: .env liegt in `config/prod.env` statt `.env`
|
||||||
|
- **Lösung**: Symlink erstellen oder Pfad anpassen
|
||||||
|
|
||||||
|
### 3. Systemd-Service ohne WorkingDirectory
|
||||||
|
- **Problem**: Service startet ohne korrektes Arbeitsverzeichnis
|
||||||
|
- **Lösung**: `WorkingDirectory=/path/to/mindnet` in systemd service
|
||||||
|
|
||||||
|
### 4. Mehrere .env-Dateien
|
||||||
|
- **Problem**: Es gibt `.env`, `prod.env`, `config/prod.env` - welche wird geladen?
|
||||||
|
- **Lösung**: Expliziter Pfad oder Umgebungsvariable `DOTENV_PATH`
|
||||||
|
|
||||||
|
## Fix-Implementierung
|
||||||
|
|
||||||
|
Der Code in `app/config.py` wurde erweitert:
|
||||||
|
- ✅ Expliziter Pfad für `.env` im Projekt-Root
|
||||||
|
- ✅ Fallback auf automatische Suche
|
||||||
|
- ✅ Debug-Logging (wenn verfügbar)
|
||||||
|
|
||||||
|
## Verifikation nach Fix
|
||||||
|
|
||||||
|
1. **Log prüfen**: Sollte `✅ .env geladen von: ...` zeigen
|
||||||
|
2. **Umgebungsvariablen prüfen**: `echo $DEBUG`, `echo $COLLECTION_PREFIX`
|
||||||
|
3. **Settings prüfen**: `python3 -c "from app.config import get_settings; s = get_settings(); print(f'DEBUG: {s.DEBUG}, PREFIX: {s.COLLECTION_PREFIX}')"`
|
||||||
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
|
||||||
134
docs/03_Technical_References/PROD_DEPLOYMENT_CHECKLIST.md
Normal file
134
docs/03_Technical_References/PROD_DEPLOYMENT_CHECKLIST.md
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
# Deployment-Checkliste: Prod vs. Dev Retrieval-Problem
|
||||||
|
|
||||||
|
**Datum**: 2026-01-12
|
||||||
|
**Version**: v4.5.10
|
||||||
|
**Status**: 🔴 Kritisch
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Prod-System findet keine Suchergebnisse, während Dev-System korrekt funktioniert. Identischer Code, identische Daten.
|
||||||
|
|
||||||
|
## Identifizierte Ursachen
|
||||||
|
|
||||||
|
### 1. 🔴 **KRITISCH: Alte EdgeDTO-Version in Prod**
|
||||||
|
|
||||||
|
**Symptom**:
|
||||||
|
```
|
||||||
|
ERROR: 1 validation error for EdgeDTO
|
||||||
|
provenance
|
||||||
|
Input should be 'explicit', 'rule', 'smart' or 'structure'
|
||||||
|
[type=literal_error, input_value='explicit:callout', input_type=str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ursache**:
|
||||||
|
- Prod verwendet eine **alte Version** des `EdgeDTO`-Modells aus `app/models/dto.py`
|
||||||
|
- Die alte Version unterstützt nur: `"explicit", "rule", "smart", "structure"`
|
||||||
|
- Die neue Version (v4.5.3+) unterstützt: `"explicit:callout", "explicit:wikilink", "explicit:note_zone", ...`
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
- ✅ Code in `dto.py` ist bereits korrekt (Zeile 51-56)
|
||||||
|
- ⚠️ **Prod muss neu gestartet werden**, um die neue Version zu laden
|
||||||
|
- ⚠️ **Python-Modul-Cache leeren** falls nötig: `find . -type d -name __pycache__ -exec rm -r {} +`
|
||||||
|
|
||||||
|
### 2. ✅ Collection-Präfix korrekt
|
||||||
|
|
||||||
|
- Prod: `COLLECTION_PREFIX=mindnet` → `mindnet_chunks` ✅
|
||||||
|
- Dev: `COLLECTION_PREFIX=mindnet_dev` → `mindnet_dev_chunks` ✅
|
||||||
|
- **Kein Problem hier**
|
||||||
|
|
||||||
|
## Sofortmaßnahmen
|
||||||
|
|
||||||
|
### Schritt 1: Code-Verifikation in Prod
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In Prod-System
|
||||||
|
cd /path/to/mindnet
|
||||||
|
grep -A 10 "provenance.*Literal" app/models/dto.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erwartete Ausgabe**:
|
||||||
|
```python
|
||||||
|
provenance: Optional[Literal[
|
||||||
|
"explicit", "rule", "smart", "structure",
|
||||||
|
"explicit:callout", "explicit:wikilink", "explicit:note_zone", ...
|
||||||
|
]] = "explicit"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Falls nicht vorhanden**: Code ist nicht aktualisiert → Deployment erforderlich
|
||||||
|
|
||||||
|
### Schritt 2: Python-Cache leeren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In Prod-System
|
||||||
|
find . -type d -name __pycache__ -exec rm -r {} +
|
||||||
|
find . -name "*.pyc" -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 3: Service neu starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# FastAPI/uvicorn neu starten
|
||||||
|
# Oder Docker-Container neu starten
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 4: Verifikation
|
||||||
|
|
||||||
|
1. **Test-Query ausführen**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8001/api/chat \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"message": "Was für einen Status hat das Projekt mindnet?"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Log prüfen**:
|
||||||
|
- ✅ Keine `validation error for EdgeDTO` mehr
|
||||||
|
- ✅ `✨ [SUCCESS] Stream 'facts_stream' lieferte X Treffer.`
|
||||||
|
- ✅ Ergebnisse werden zurückgegeben
|
||||||
|
|
||||||
|
## Code-Vergleich
|
||||||
|
|
||||||
|
### Aktuelle Version (sollte in Prod sein):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# app/models/dto.py (Zeile 51-56)
|
||||||
|
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"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alte Version (verursacht Fehler):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Alte Version (nur 4 Werte)
|
||||||
|
provenance: Optional[Literal[
|
||||||
|
"explicit", "rule", "smart", "structure"
|
||||||
|
]] = "explicit"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Weitere mögliche Ursachen (wenn Fix nicht hilft)
|
||||||
|
|
||||||
|
### 1. Unterschiedliche Python-Versionen
|
||||||
|
- Prüfen: `python --version` in Dev vs. Prod
|
||||||
|
- Pydantic-Verhalten kann zwischen Versionen variieren
|
||||||
|
|
||||||
|
### 2. Unterschiedliche Pydantic-Versionen
|
||||||
|
- Prüfen: `pip list | grep pydantic` in Dev vs. Prod
|
||||||
|
- `requirements.txt` sollte identisch sein
|
||||||
|
|
||||||
|
### 3. Unterschiedliche Embedding-Modelle
|
||||||
|
- Prüfen: `MINDNET_EMBEDDING_MODEL` in beiden Systemen
|
||||||
|
- **Beide verwenden**: `nomic-embed-text` ✅
|
||||||
|
|
||||||
|
### 4. Unterschiedliche Vektor-Dimensionen
|
||||||
|
- Prüfen: `VECTOR_DIM` in beiden Systemen
|
||||||
|
- **Beide verwenden**: `768` ✅
|
||||||
|
|
||||||
|
## Erwartetes Ergebnis nach Fix
|
||||||
|
|
||||||
|
- ✅ Keine Pydantic-Validierungsfehler mehr
|
||||||
|
- ✅ Alle Streams liefern Ergebnisse
|
||||||
|
- ✅ Retrieval funktioniert identisch in Dev und Prod
|
||||||
|
- ✅ `explicit:callout` Provenance wird korrekt akzeptiert
|
||||||
134
docs/03_Technical_References/PROD_PYTHON_CACHE_FIX.md
Normal file
134
docs/03_Technical_References/PROD_PYTHON_CACHE_FIX.md
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
# Fix: Python-Modul-Cache-Problem in Prod
|
||||||
|
|
||||||
|
**Datum**: 2026-01-12
|
||||||
|
**Version**: v4.5.10
|
||||||
|
**Status**: 🔴 Kritisch
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Code in `app/models/dto.py` ist korrekt (enthält `explicit:callout`), aber Prod verwendet trotzdem eine alte Version.
|
||||||
|
|
||||||
|
**Symptom**:
|
||||||
|
```
|
||||||
|
ERROR: 1 validation error for EdgeDTO
|
||||||
|
provenance
|
||||||
|
Input should be 'explicit', 'rule', 'smart' or 'structure'
|
||||||
|
[type=literal_error, input_value='explicit:callout', input_type=str]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ursache
|
||||||
|
|
||||||
|
**Python-Modul-Cache**: Python speichert kompilierte `.pyc` Dateien in `__pycache__` Verzeichnissen. Wenn der Code aktualisiert wird, aber der Service nicht neu gestartet wird, lädt Python die alte gecachte Version.
|
||||||
|
|
||||||
|
## Sofortmaßnahmen
|
||||||
|
|
||||||
|
### Schritt 1: Python-Cache leeren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In Prod-System
|
||||||
|
cd ~/mindnet
|
||||||
|
|
||||||
|
# Finde und lösche alle __pycache__ Verzeichnisse
|
||||||
|
find . -type d -name __pycache__ -exec rm -r {} + 2>/dev/null || true
|
||||||
|
|
||||||
|
# Finde und lösche alle .pyc Dateien
|
||||||
|
find . -name "*.pyc" -delete
|
||||||
|
|
||||||
|
# Speziell für dto.py
|
||||||
|
rm -rf app/models/__pycache__
|
||||||
|
rm -rf app/__pycache__
|
||||||
|
rm -rf __pycache__
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 2: Verifikation des Codes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe, ob der Code korrekt ist
|
||||||
|
grep -A 10 "provenance.*Literal" app/models/dto.py | grep "explicit:callout"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erwartete Ausgabe**: Sollte `explicit:callout` enthalten
|
||||||
|
|
||||||
|
### Schritt 3: Service neu starten
|
||||||
|
|
||||||
|
**Option A: FastAPI/uvicorn direkt**:
|
||||||
|
```bash
|
||||||
|
# Service stoppen (Ctrl+C oder kill)
|
||||||
|
# Dann neu starten
|
||||||
|
source .venv/bin/activate
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Systemd-Service**:
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart mindnet-prod
|
||||||
|
# oder
|
||||||
|
sudo systemctl restart mindnet
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option C: Docker-Container**:
|
||||||
|
```bash
|
||||||
|
docker-compose restart mindnet
|
||||||
|
# oder
|
||||||
|
docker restart mindnet-container
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 4: Verifikation zur Laufzeit
|
||||||
|
|
||||||
|
**Test-Script ausführen** (wenn verfügbar):
|
||||||
|
```bash
|
||||||
|
python3 scripts/verify_dto_import.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erwartete Ausgabe**:
|
||||||
|
```
|
||||||
|
✅ EdgeDTO unterstützt 'explicit:callout'
|
||||||
|
✅ 'explicit:callout' ist in der Literal-Liste enthalten
|
||||||
|
✅ EdgeDTO mit 'explicit:callout' erfolgreich erstellt!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Oder manuell testen**:
|
||||||
|
```python
|
||||||
|
python3 -c "
|
||||||
|
from app.models.dto import EdgeDTO
|
||||||
|
test = EdgeDTO(
|
||||||
|
id='test', kind='test', source='test', target='test',
|
||||||
|
weight=1.0, provenance='explicit:callout'
|
||||||
|
)
|
||||||
|
print('✅ EdgeDTO mit explicit:callout funktioniert!')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code-Fix (Fallback-Mechanismus)
|
||||||
|
|
||||||
|
Ein Fallback-Mechanismus wurde in `retriever.py` implementiert:
|
||||||
|
- Wenn `EdgeDTO` mit `explicit:callout` fehlschlägt, wird automatisch `explicit` als Fallback verwendet
|
||||||
|
- Dies verhindert, dass der gesamte Retrieval-Prozess fehlschlägt
|
||||||
|
- **WICHTIG**: Dies ist nur eine temporäre Lösung - der Cache muss trotzdem geleert werden!
|
||||||
|
|
||||||
|
## Verifikation nach Fix
|
||||||
|
|
||||||
|
1. **Test-Query ausführen**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8001/api/chat \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"message": "Was für einen Status hat das Projekt mindnet?"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Log prüfen**:
|
||||||
|
- ✅ Keine `validation error for EdgeDTO` mehr
|
||||||
|
- ✅ Keine `⚠️ [EDGE-DTO] Provenance 'explicit:callout' nicht unterstützt` Warnungen
|
||||||
|
- ✅ `✨ [SUCCESS] Stream 'facts_stream' lieferte X Treffer.`
|
||||||
|
- ✅ Ergebnisse werden zurückgegeben
|
||||||
|
|
||||||
|
## Warum passiert das?
|
||||||
|
|
||||||
|
1. **Code wurde aktualisiert**, aber Service läuft noch mit alter Version im Speicher
|
||||||
|
2. **Python lädt Module nur einmal** - nach dem ersten Import wird die gecachte Version verwendet
|
||||||
|
3. **__pycache__ Verzeichnisse** enthalten kompilierte Bytecode-Versionen der alten Dateien
|
||||||
|
|
||||||
|
## Prävention
|
||||||
|
|
||||||
|
- **Immer Service neu starten** nach Code-Änderungen
|
||||||
|
- **Cache regelmäßig leeren** bei Deployment
|
||||||
|
- **Verwende `--reload` Flag** bei uvicorn für automatisches Neuladen (nur für Dev!)
|
||||||
163
docs/03_Technical_References/RETRIEVAL_DEV_PROD_DIFF_ANALYSIS.md
Normal file
163
docs/03_Technical_References/RETRIEVAL_DEV_PROD_DIFF_ANALYSIS.md
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
# Analyse: Retrieval-Unterschiede zwischen Dev und Prod
|
||||||
|
|
||||||
|
**Datum**: 2026-01-12
|
||||||
|
**Version**: v4.5.10
|
||||||
|
**Status**: 🔴 Kritisch
|
||||||
|
|
||||||
|
## Problemstellung
|
||||||
|
|
||||||
|
Bei identischer Codebasis und identischen Daten liefert das Dev-System Suchergebnisse, während das Prod-System keine Ergebnisse findet.
|
||||||
|
|
||||||
|
## Identifizierte Ursachen
|
||||||
|
|
||||||
|
### 1. 🔴 **KRITISCH: Inkonsistente Collection-Präfix-Konfiguration**
|
||||||
|
|
||||||
|
**Problem**: Zwei verschiedene Umgebungsvariablen werden für den Collection-Präfix verwendet:
|
||||||
|
|
||||||
|
1. **`app/config.py` (Zeile 24)**:
|
||||||
|
```python
|
||||||
|
COLLECTION_PREFIX: str = os.getenv("MINDNET_PREFIX", "mindnet_dev")
|
||||||
|
```
|
||||||
|
- Verwendet `MINDNET_PREFIX` als Umgebungsvariable
|
||||||
|
- Default: `"mindnet_dev"`
|
||||||
|
|
||||||
|
2. **`app/core/database/qdrant.py` (Zeile 47)**:
|
||||||
|
```python
|
||||||
|
prefix = os.getenv("COLLECTION_PREFIX") or "mindnet"
|
||||||
|
```
|
||||||
|
- Verwendet `COLLECTION_PREFIX` als Umgebungsvariable
|
||||||
|
- Default: `"mindnet"`
|
||||||
|
|
||||||
|
**Auswirkung**:
|
||||||
|
- **Retriever verwendet `QdrantConfig.from_env()`**, das `COLLECTION_PREFIX` liest
|
||||||
|
- **Ingestion verwendet `Settings.COLLECTION_PREFIX`**, das `MINDNET_PREFIX` liest
|
||||||
|
- **Resultat**: Daten werden in verschiedene Collections geschrieben/gesucht:
|
||||||
|
- Dev: `mindnet_dev_chunks`, `mindnet_dev_notes`, `mindnet_dev_edges`
|
||||||
|
- Prod: `mindnet_chunks`, `mindnet_notes`, `mindnet_edges`
|
||||||
|
|
||||||
|
### 2. ⚠️ **Mögliche weitere Ursachen**
|
||||||
|
|
||||||
|
#### 2.1 Unterschiedliche Embedding-Modelle
|
||||||
|
- **Prüfen**: `MINDNET_EMBEDDING_MODEL` in Dev vs. Prod
|
||||||
|
- **Auswirkung**: Unterschiedliche Vektoren → unterschiedliche Similarity-Scores
|
||||||
|
|
||||||
|
#### 2.2 Unterschiedliche Vektor-Dimensionen
|
||||||
|
- **Prüfen**: `VECTOR_DIM` in Dev vs. Prod
|
||||||
|
- **Auswirkung**: Dimension-Mismatch → Suche schlägt fehl
|
||||||
|
|
||||||
|
#### 2.3 Unterschiedliche Qdrant-Instanzen
|
||||||
|
- **Prüfen**: `QDRANT_URL` / `QDRANT_HOST` in Dev vs. Prod
|
||||||
|
- **Auswirkung**: Daten liegen in verschiedenen Datenbanken
|
||||||
|
|
||||||
|
#### 2.4 Unterschiedliche Score-Thresholds
|
||||||
|
- **Prüfen**: Filter-Logik oder Mindest-Scores
|
||||||
|
- **Auswirkung**: Ergebnisse werden gefiltert, bevor sie zurückgegeben werden
|
||||||
|
|
||||||
|
## Diagnose-Checkliste
|
||||||
|
|
||||||
|
### ✅ Sofort prüfen:
|
||||||
|
|
||||||
|
1. **Collection-Präfix-Verifikation**:
|
||||||
|
```bash
|
||||||
|
# Dev
|
||||||
|
echo $COLLECTION_PREFIX
|
||||||
|
echo $MINDNET_PREFIX
|
||||||
|
|
||||||
|
# Prod
|
||||||
|
echo $COLLECTION_PREFIX
|
||||||
|
echo $MINDNET_PREFIX
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Qdrant Collections prüfen**:
|
||||||
|
```python
|
||||||
|
# In beiden Systemen ausführen
|
||||||
|
from app.core.database.qdrant import get_client, QdrantConfig
|
||||||
|
cfg = QdrantConfig.from_env()
|
||||||
|
client = get_client(cfg)
|
||||||
|
print(f"Prefix: {cfg.prefix}")
|
||||||
|
print(f"Collections: {client.get_collections().collections}")
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Embedding-Modell prüfen**:
|
||||||
|
```bash
|
||||||
|
# Dev
|
||||||
|
echo $MINDNET_EMBEDDING_MODEL
|
||||||
|
echo $VECTOR_DIM
|
||||||
|
|
||||||
|
# Prod
|
||||||
|
echo $MINDNET_EMBEDDING_MODEL
|
||||||
|
echo $VECTOR_DIM
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Qdrant-Verbindung prüfen**:
|
||||||
|
```bash
|
||||||
|
# Dev
|
||||||
|
echo $QDRANT_URL
|
||||||
|
echo $QDRANT_HOST
|
||||||
|
echo $QDRANT_PORT
|
||||||
|
|
||||||
|
# Prod
|
||||||
|
echo $QDRANT_URL
|
||||||
|
echo $QDRANT_HOST
|
||||||
|
echo $QDRANT_PORT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lösungsvorschläge
|
||||||
|
|
||||||
|
### Option 1: Harmonisierung der Umgebungsvariablen (Empfohlen)
|
||||||
|
|
||||||
|
**Ziel**: Eine einzige Umgebungsvariable für den Collection-Präfix verwenden.
|
||||||
|
|
||||||
|
**Änderungen**:
|
||||||
|
1. **`app/core/database/qdrant.py`**:
|
||||||
|
```python
|
||||||
|
prefix = os.getenv("COLLECTION_PREFIX") or os.getenv("MINDNET_PREFIX") or "mindnet"
|
||||||
|
```
|
||||||
|
- Unterstützt beide Variablen (Abwärtskompatibilität)
|
||||||
|
- `COLLECTION_PREFIX` hat Priorität
|
||||||
|
|
||||||
|
2. **`app/config.py`**:
|
||||||
|
```python
|
||||||
|
COLLECTION_PREFIX: str = os.getenv("COLLECTION_PREFIX") or os.getenv("MINDNET_PREFIX") or "mindnet_dev"
|
||||||
|
```
|
||||||
|
- Unterstützt beide Variablen
|
||||||
|
- `COLLECTION_PREFIX` hat Priorität
|
||||||
|
|
||||||
|
3. **Dokumentation**: Klarstellen, dass `COLLECTION_PREFIX` die primäre Variable ist
|
||||||
|
|
||||||
|
### Option 2: Explizite Konfiguration in .env
|
||||||
|
|
||||||
|
**Ziel**: Beide Systeme verwenden explizit gesetzte `COLLECTION_PREFIX` Werte.
|
||||||
|
|
||||||
|
**Dev `.env`**:
|
||||||
|
```env
|
||||||
|
COLLECTION_PREFIX=mindnet_dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prod `.env`**:
|
||||||
|
```env
|
||||||
|
COLLECTION_PREFIX=mindnet
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Daten-Migration
|
||||||
|
|
||||||
|
**Ziel**: Daten von einer Collection in die andere migrieren.
|
||||||
|
|
||||||
|
**Vorgehen**:
|
||||||
|
1. Identifizieren, welche Collection die "richtigen" Daten enthält
|
||||||
|
2. Daten von Dev nach Prod migrieren (oder umgekehrt)
|
||||||
|
3. Collection-Präfix harmonisieren
|
||||||
|
|
||||||
|
## Sofortmaßnahmen
|
||||||
|
|
||||||
|
1. ✅ **Prüfen**: Welche Collections existieren in beiden Systemen?
|
||||||
|
2. ✅ **Prüfen**: Welche Umgebungsvariablen sind gesetzt?
|
||||||
|
3. ✅ **Prüfen**: Welche Collection enthält die Daten?
|
||||||
|
4. ✅ **Fix**: Collection-Präfix-Konfiguration harmonisieren
|
||||||
|
5. ✅ **Test**: Retrieval in beiden Systemen verifizieren
|
||||||
|
|
||||||
|
## Erwartetes Ergebnis nach Fix
|
||||||
|
|
||||||
|
- ✅ Beide Systeme verwenden dieselbe Collection-Präfix-Logik
|
||||||
|
- ✅ Retrieval findet Daten in beiden Systemen
|
||||||
|
- ✅ Konsistente Konfiguration zwischen Ingestion und Retrieval
|
||||||
1007
docs/03_Technical_References/consolidated_config_files.md
Normal file
1007
docs/03_Technical_References/consolidated_config_files.md
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
<!-- DOCUMENT 1: pflichtenheft_obsidian_plugin_mindnet_assistant.md -->
|
||||||
|
---
|
||||||
|
id: pflichtenheft_obsidian_plugin_mindnet_assistant
|
||||||
|
title: Pflichtenheft – Obsidian Plugin „Mindnet Causal Assistant“
|
||||||
|
type: specification
|
||||||
|
status: draft
|
||||||
|
created: 2026-01-13
|
||||||
|
lang: de
|
||||||
|
---
|
||||||
|
|
||||||
|
# Pflichtenheft – Obsidian Plugin „Mindnet Causal Assistant“
|
||||||
|
|
||||||
|
## 1. Zielsetzung
|
||||||
|
Das Plugin unterstützt den Nutzer beim Erstellen und Pflegen eines narrativ-kausalen Wissensgraphen in Obsidian (Mindnet).
|
||||||
|
|
||||||
|
Es bietet:
|
||||||
|
- **Aktive Authoring-Unterstützung** (Guided Note Creation / Interview Flow)
|
||||||
|
- **Kausalketten-Prüfung** (Chain Explorer, Vorwärts/Rückwärts)
|
||||||
|
- **Linting + Auto-Fixes** (Graph-Hygiene, konsistente Kanten, Namensnormalisierung)
|
||||||
|
- **Export/Integration** der strukturierten Graph-Daten für ein Retrieval-System (Qdrant + Graph-Index)
|
||||||
|
|
||||||
|
## 2. Kontext & Randbedingungen
|
||||||
|
- Obsidian Vault enthält Notes als Markdown, jede Entität eine Datei.
|
||||||
|
- Notes enthalten Frontmatter (`id,title,type,status,...`) und Edges in Callout-Blöcken.
|
||||||
|
- Edge-Vokabular inklusive Aliasse & Inversen ist verfügbar (z.B. `edge_vocabulary.md`).
|
||||||
|
- Der Graph soll später von Mindnet traversierbar sein (Vorwärts/Rückwärts über inverse Relationen).
|
||||||
|
- Plugin muss offline nutzbar sein; optionale Backend/LLM-Integration ist konfigurierbar.
|
||||||
|
|
||||||
|
## 3. Begriffsdefinitionen
|
||||||
|
- **Node**: eine Obsidian Markdown-Datei (Entität).
|
||||||
|
- **Edge**: gerichtete Beziehung zwischen zwei Nodes (canonical edge type).
|
||||||
|
- **Alias**: alternative Edge-Bezeichnung in Notes, die auf canonical mapped wird.
|
||||||
|
- **Inverse**: Gegenkante zu einer Edge, laut Vokabular (z.B. `resulted_in` ⇄ `caused_by`).
|
||||||
|
- **Hub/Index**: Note, die primär navigiert (typisch `type: insight`), keine Kausalursache.
|
||||||
|
|
||||||
|
## 4. Scope
|
||||||
|
### In Scope (MVP → V2)
|
||||||
|
**MVP**
|
||||||
|
- Parser für Frontmatter + Edge-Callouts
|
||||||
|
- Normalizer: Alias → Canonical; optional Inversen-Erkennung
|
||||||
|
- Linter: Regelset (Error/Warn/Info) + Report
|
||||||
|
- Chain Explorer: forward/backward traversal (1–4 hops) ab aktueller Note
|
||||||
|
- Quickfixes: Edge-Typ ersetzen, Links normalisieren, Missing Note Stub erzeugen
|
||||||
|
|
||||||
|
**V2**
|
||||||
|
- Guided Authoring (Interview Flow) für `experience/decision/principle/state/strategy`
|
||||||
|
- Refactor Mode: Text → Node/Edge-Kandidaten + Review UI
|
||||||
|
- Export/Sync: JSON Graph Export + optional Qdrant Sync (separater Service)
|
||||||
|
|
||||||
|
### Out of Scope (initial)
|
||||||
|
- Vollautomatische Kausalitäts-Interpretation ohne User-Bestätigung
|
||||||
|
- Vollständige UI-Graph-Visualisierung (Obsidian Graph View bleibt nutzbar)
|
||||||
|
- Direkte medizinische/psychologische Beratung
|
||||||
|
|
||||||
|
## 5. Nutzerrollen & Use Cases
|
||||||
|
### Rolle: Nutzer (Author)
|
||||||
|
- UC1: „Ich schreibe eine Note und will Kanten prüfen“
|
||||||
|
- UC2: „Ich will von einem Ereignis aus die Kausalkette sehen“
|
||||||
|
- UC3: „Ich will eine neue Experience/Decision Note sauber anlegen“
|
||||||
|
- UC4: „Ich habe Text und will daraus Kandidaten extrahieren“
|
||||||
|
- UC5: „Ich will leere Links als open_question sauber erzeugen“
|
||||||
|
|
||||||
|
### Rolle: System/Indexer (Mindnet)
|
||||||
|
- UC6: „Ich brauche exportierbare adjacency lists + canonical edges“
|
||||||
|
|
||||||
|
## 6. Funktionale Anforderungen (FR)
|
||||||
|
|
||||||
|
### FR1: Vault Parsing
|
||||||
|
- FR1.1 Parse Frontmatter (YAML): `id,title,type,status,date,tags,...`
|
||||||
|
- FR1.2 Parse Edge-Callouts im Format:
|
||||||
|
- `> [!abstract]- 🕸️ Semantic Mapping`
|
||||||
|
- `>> [!edge] <relation>`
|
||||||
|
- `>> [[target]]`
|
||||||
|
- FR1.3 Extrahiere alle WikiLinks `[[...]]` aus Edge-Blocks
|
||||||
|
- FR1.4 Erkenne Datei-Pfade, Dateinamen und canonical node identifiers (Dateiname ohne `.md`)
|
||||||
|
|
||||||
|
**Output (intern)**
|
||||||
|
```ts
|
||||||
|
type Node = { id?: string; title?: string; type?: string; status?: string; path: string; slug: string };
|
||||||
|
type Edge = { srcSlug: string; dstSlug: string; rawType: string; canonicalType?: string; line?: number; blockId?: string };
|
||||||
|
```
|
||||||
|
|
||||||
|
### FR2: Edge Normalization
|
||||||
|
- FR2.1 Map rawType/Alias auf canonical edge type via Edge Vocabulary
|
||||||
|
- FR2.2 Speichere Mapping-Entscheidungen (raw → canonical) pro Edge
|
||||||
|
- FR2.3 Liefere inverse edge type (`inverseType`) pro canonical edge type (sofern definiert)
|
||||||
|
|
||||||
|
### FR3: Linting
|
||||||
|
- FR3.1 Führe Checkliste von Regeln aus (siehe separates Dokument)
|
||||||
|
- FR3.2 Liefere LintReport mit Severity (ERROR/WARN/INFO), Location (file, line), Fix-Vorschlägen
|
||||||
|
- FR3.3 Quickfix: wende Fix auf Note an (Text edit), mit Preview/Diff
|
||||||
|
|
||||||
|
### FR4: Chain Explorer (Traversal)
|
||||||
|
- FR4.1 Startpunkt: aktuelle Note im Editor
|
||||||
|
- FR4.2 Forward traversal: `resulted_in`, `followed_by`, `impacts`, `source_of`, optional `related_to`
|
||||||
|
- FR4.3 Backward traversal: `caused_by`, `preceeded_by`, `derived_from`, `impacted_by`
|
||||||
|
- FR4.4 Konfigurierbare maxHops (Default 3)
|
||||||
|
- FR4.5 Ergebnis als Liste von Pfaden + kompaktes Subgraph-Summary (Nodes/Edges)
|
||||||
|
|
||||||
|
### FR5: Missing Notes / Stubs
|
||||||
|
- FR5.1 Erkenne, wenn Edge-Target nicht existiert
|
||||||
|
- FR5.2 Biete „Create Stub Note“ an:
|
||||||
|
- Template basierend auf Typ (default `open_question` wenn unbekannt)
|
||||||
|
- Einhaltung Naming-Rules: `a-z0-9_`
|
||||||
|
- FR5.3 Optional: convert TODO-Link → open_question note
|
||||||
|
|
||||||
|
### FR6: Guided Authoring (V2)
|
||||||
|
- FR6.1 Wizard für neue Notes: Auswahl Typ → Fragen (eine nach der anderen)
|
||||||
|
- FR6.2 Wizard erstellt Datei mit Template + initialen Edge-Blocks
|
||||||
|
- FR6.3 Jede automatische Edge-Vermutung ist „review-required“
|
||||||
|
|
||||||
|
### FR7: Refactor Mode (V2)
|
||||||
|
- FR7.1 Extrahiere aus aktuellem Note-Text Kandidaten:
|
||||||
|
- Event-Kandidaten (Datum/Ort/Verben)
|
||||||
|
- Decision-Kandidaten („entschied“, „nahm an“, „wechselte“)
|
||||||
|
- Principle-Kandidaten („ich glaube“, „ich habe gelernt“)
|
||||||
|
- Relation-Kandidaten („dadurch“, „führte zu“, „weil“)
|
||||||
|
- FR7.2 UI-Review: Checkbox-Liste zum Erstellen/Verwerfen
|
||||||
|
- FR7.3 Generiere Notes + Edges nur nach Bestätigung
|
||||||
|
|
||||||
|
### FR8: Export (MVP optional / V2 empfohlen)
|
||||||
|
- FR8.1 Export JSON: Nodes + canonical edges + inverses
|
||||||
|
- FR8.2 Export adjacency list pro node slug
|
||||||
|
- FR8.3 Optional: webhook/CLI hook für Indexer (Qdrant)
|
||||||
|
|
||||||
|
## 7. Nicht-funktionale Anforderungen (NFR)
|
||||||
|
- NFR1: Performant bei 10k Notes (incremental parse, caching)
|
||||||
|
- NFR2: Offline-first; LLM/Backend optional
|
||||||
|
- NFR3: Deterministische Normalisierung (gleiches Input → gleiches Output)
|
||||||
|
- NFR4: Kein Erfinden von Fakten: Auto-Edge nur als Vorschlag
|
||||||
|
- NFR5: Sicheres Editieren (Diff/Undo via Obsidian APIs)
|
||||||
|
- NFR6: Konfigurierbarkeit (YAML/Settings Tab): maxHops, allowed edges, strict mode
|
||||||
|
|
||||||
|
## 8. Technisches Lösungsdesign
|
||||||
|
|
||||||
|
### 8.1 Obsidian Plugin Struktur (TypeScript)
|
||||||
|
- `main.ts` – Plugin lifecycle, commands, views
|
||||||
|
- `settings.ts` – Einstellungen
|
||||||
|
- `parser/` – Markdown + callout parser
|
||||||
|
- `graph/` – Node/Edge model, normalization, traversal
|
||||||
|
- `lint/` – Rules engine, reports, quickfixes
|
||||||
|
- `ui/` – Sidebar view, modals, diff preview
|
||||||
|
|
||||||
|
### 8.2 Speicherung / Cache
|
||||||
|
- In-memory cache: map `path → parsed Node + edges + hash`
|
||||||
|
- Incremental update: on file change events re-parse only changed file
|
||||||
|
- Optional persisted cache in `.obsidian/plugins/.../cache.json`
|
||||||
|
|
||||||
|
### 8.3 Edge Vocabulary Integration
|
||||||
|
- Input: `edge_vocabulary.md` im Vault ODER eingebettete JSON Ressource
|
||||||
|
- Parsing:
|
||||||
|
- canonical edge types
|
||||||
|
- alias list
|
||||||
|
- inverse mapping
|
||||||
|
- Fallback: minimal builtin vocabulary, wenn Datei fehlt
|
||||||
|
|
||||||
|
### 8.4 Traversal Engine
|
||||||
|
- Graph Index: adjacency lists aus canonical edges
|
||||||
|
- Traversal:
|
||||||
|
- BFS mit hop limit
|
||||||
|
- optional weighted expansion (für Chain Explorer „relevant paths first“)
|
||||||
|
|
||||||
|
### 8.5 Quickfix Engine
|
||||||
|
- Applies patches auf Markdown:
|
||||||
|
- Replace edge type token im Callout (`>> [!edge] ...`)
|
||||||
|
- Rename link targets (replace `[[old]]` → `[[new]]`)
|
||||||
|
- Insert stub note file from template
|
||||||
|
- Safety:
|
||||||
|
- Show diff modal
|
||||||
|
- Use Obsidian editor transactions / file API
|
||||||
|
|
||||||
|
### 8.6 Optional Backend / LLM (V2)
|
||||||
|
- Backend (local node service) für:
|
||||||
|
- text extraction (Refactor Mode)
|
||||||
|
- suggestion generation
|
||||||
|
- Communication:
|
||||||
|
- HTTP local (`127.0.0.1`) oder WebSocket
|
||||||
|
- API key storage via Obsidian settings (encrypted if possible)
|
||||||
|
- Claude-code/Cursor: nutzt Code-Agent für Implementierung, nicht zur Runtime.
|
||||||
|
|
||||||
|
## 9. UI/UX Anforderungen
|
||||||
|
|
||||||
|
### 9.1 Sidebar View „Mindnet Assistant“
|
||||||
|
Tabs:
|
||||||
|
- **Validate**: Lint Report + Fix Buttons
|
||||||
|
- **Chains**: Forward/Backward Chain Explorer + Copy as text
|
||||||
|
- **Create** (V2): Wizard new note
|
||||||
|
- **Refactor** (V2): Extract candidates
|
||||||
|
|
||||||
|
### 9.2 Commands (Command Palette)
|
||||||
|
- `Mindnet: Validate current note`
|
||||||
|
- `Mindnet: Validate vault (selected folders)`
|
||||||
|
- `Mindnet: Show chains from current note`
|
||||||
|
- `Mindnet: Normalize edges in current note`
|
||||||
|
- `Mindnet: Create stub for missing links`
|
||||||
|
- (V2) `Mindnet: Start guided authoring`
|
||||||
|
- (V2) `Mindnet: Refactor current note to graph`
|
||||||
|
|
||||||
|
## 10. Akzeptanzkriterien
|
||||||
|
- AK1: Plugin erkennt Edge-Callouts und normalisiert Aliasse deterministisch
|
||||||
|
- AK2: Linter findet Node-Splitting (mindestens Levenshtein/ähnliche Slugs) und Missing Notes
|
||||||
|
- AK3: Chain Explorer liefert identische Pfade vorwärts/rückwärts bei inversen Edge-Paaren
|
||||||
|
- AK4: Quickfix ersetzt Edge-Typen ohne Markdown zu zerstören; Undo funktioniert
|
||||||
|
- AK5: Export JSON enthält canonical edges + inverse types
|
||||||
|
|
||||||
|
## 11. Deliverables
|
||||||
|
- Obsidian Plugin (TS) mit MVP Features
|
||||||
|
- Dokumentation:
|
||||||
|
- Install/Build
|
||||||
|
- Settings
|
||||||
|
- Rule reference
|
||||||
|
- Example vault sample
|
||||||
|
- Optional: JSON Export Format Spec (nodes/edges)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Prompts für Code-Agenten (Cursor / Claude-code)
|
||||||
|
|
||||||
|
### Prompt A (Repo Scaffold + MVP)
|
||||||
|
> Du bist ein Senior TypeScript Engineer. Implementiere ein Obsidian Plugin „Mindnet Causal Assistant“.
|
||||||
|
> Ziele MVP:
|
||||||
|
> 1) Parse Frontmatter (YAML) und Edge-Callouts im Format:
|
||||||
|
> `> [!abstract]- 🕸️ Semantic Mapping` → `>> [!edge] <relation>` → `>> [[target]]`.
|
||||||
|
> 2) Normalisiere Edge Aliasse auf canonical edge types (Vokabular als JSON im Code; später ersetzbar).
|
||||||
|
> 3) Baue eine Sidebar View mit Tabs „Validate“ und „Chains“.
|
||||||
|
> 4) Implementiere Lint Regeln: missing target note, alias-not-normalized, hub-has-causal-edge, chronology-vs-causality warning.
|
||||||
|
> 5) Implementiere Chain Explorer (forward/backward, maxHops=3).
|
||||||
|
> 6) Implementiere Quickfix: replace edge type token, create stub note.
|
||||||
|
> Nutze Obsidian APIs, schreibe sauberen TS Code, mit Tests für Parser/Normalizer.
|
||||||
|
|
||||||
|
### Prompt B (Parser Unit Tests)
|
||||||
|
> Schreibe Unit Tests für den Markdown Parser:
|
||||||
|
> - erkennt mehrere Edge-Blocks pro Datei
|
||||||
|
> - erkennt mehrere Targets pro Edge
|
||||||
|
> - liefert line numbers
|
||||||
|
> - ignoriert WikiLinks außerhalb der Semantic Mapping Callouts
|
||||||
|
> Nutze vitest/jest. Erzeuge fixtures.
|
||||||
|
|
||||||
|
### Prompt C (Lint Engine + Quickfix)
|
||||||
|
> Implementiere eine Lint Engine als Rule-Pipeline.
|
||||||
|
> Jede Regel: id, severity, detect(node, graph) -> findings, fix(finding)->patch.
|
||||||
|
> Baue eine Diff Preview Modal und applyPatch über Obsidian file API.
|
||||||
|
> Implementiere zunächst 6 Regeln aus der Checkliste.
|
||||||
|
|
||||||
|
### Prompt D (Vocabulary Loader)
|
||||||
|
> Implementiere einen VocabularyLoader:
|
||||||
|
> - lädt entweder eingebettetes JSON oder eine Vault-Datei `edge_vocabulary.md`
|
||||||
|
> - parst canonical types, aliases, inverse
|
||||||
|
> - bietet getCanonical(raw) und getInverse(canonical)
|
||||||
|
> Fallback auf builtin vocabulary wenn parsing fehlschlägt.
|
||||||
136
docs/05_Development/Obsidian/lint_regeln_Kausalität.md
Normal file
136
docs/05_Development/Obsidian/lint_regeln_Kausalität.md
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<!-- DOCUMENT 2: checklist_lint_regeln_mindnet_assistant.md -->
|
||||||
|
---
|
||||||
|
id: checklist_lint_regeln_mindnet_assistant
|
||||||
|
title: Checkliste – Lint-Regeln für Mindnet Causal Assistant
|
||||||
|
type: specification
|
||||||
|
status: draft
|
||||||
|
created: 2026-01-13
|
||||||
|
lang: de
|
||||||
|
---
|
||||||
|
|
||||||
|
# Checkliste – Lint-Regeln für Mindnet Causal Assistant
|
||||||
|
|
||||||
|
## Severity Levels
|
||||||
|
- **ERROR**: bricht Traversal/Indexer oder erzeugt falsche Nodes
|
||||||
|
- **WARN**: wahrscheinlich falsche Semantik / schlechter Retrieval-Impact
|
||||||
|
- **INFO**: Optimierung / Empfehlung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A. Graph-Integrität & Naming
|
||||||
|
|
||||||
|
### L1 (ERROR) Missing Target Note
|
||||||
|
**Wenn:** Edge target `[[X]]` existiert nicht als Datei im Vault
|
||||||
|
**Dann:** Finding `missing_target`
|
||||||
|
**Fix:** „Create Stub Note“ (default `type: open_question`) oder remove edge
|
||||||
|
|
||||||
|
### L2 (ERROR) Node Splitting durch Schreibvarianten
|
||||||
|
**Wenn:** mehrere Targets im Vault sind ähnlich (slug distance), oder in Edges mehrere Varianten vorkommen
|
||||||
|
**Dann:** Finding `node_split_candidate`
|
||||||
|
**Fix:** Vorschlag canonical slug + bulk replace links
|
||||||
|
|
||||||
|
### L3 (ERROR) Invalid Filename Policy
|
||||||
|
**Wenn:** Dateiname enthält Zeichen außerhalb `[a-z0-9_]`
|
||||||
|
**Dann:** Finding `invalid_filename`
|
||||||
|
**Fix:** Rename file + update all backlinks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B. Edge-Formale Regeln
|
||||||
|
|
||||||
|
### L4 (ERROR) Unknown Edge Type / Unmapped Alias
|
||||||
|
**Wenn:** raw edge type nicht im Vokabular (alias→canonical)
|
||||||
|
**Dann:** Finding `unknown_edge_type`
|
||||||
|
**Fix:** Edge type ersetzen durch best guess oder user selection
|
||||||
|
|
||||||
|
### L5 (WARN) Alias not normalized
|
||||||
|
**Wenn:** raw edge type ist Alias, canonical bekannt, aber Note enthält Alias
|
||||||
|
**Dann:** Finding `alias_not_normalized`
|
||||||
|
**Fix:** Replace raw with canonical (optional config)
|
||||||
|
|
||||||
|
### L6 (WARN) Missing Inverse Edge (optional strict mode)
|
||||||
|
**Wenn:** Edge A->B existiert, inverse nach Vokabular fehlt in B
|
||||||
|
**Dann:** Finding `missing_inverse`
|
||||||
|
**Fix:** Add inverse edge to target note (review + diff)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## C. Semantik: Kausalität vs Chronologie
|
||||||
|
|
||||||
|
### L7 (WARN) Chronology used as Causality
|
||||||
|
**Wenn:** Knoten/Target wirkt wie Prozessschritt („warten“, „gehen“, „ankommen“) und Edge type ist `resulted_in`
|
||||||
|
**Dann:** Finding `chronology_as_causality`
|
||||||
|
**Fix:** Vorschlag `followed_by` (inverse `preceeded_by`)
|
||||||
|
|
||||||
|
### L8 (INFO) Kausalität ohne Brücken (Gap)
|
||||||
|
**Wenn:** Pfad springt von Ereignis direkt zu Entscheidung, aber intermediäre Knoten fehlen (heuristisch)
|
||||||
|
**Dann:** Finding `missing_bridge_node`
|
||||||
|
**Fix:** Vorschlag „Create open_question bridge“ (z.B. „Was war der konkrete Auslöser…?“)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D. Node-Type Regeln
|
||||||
|
|
||||||
|
### L9 (ERROR) Causal edges from open_question/hypothesis/white_spot
|
||||||
|
**Wenn:** node.type in `{open_question, hypothesis, white_spot}` und Edge type in `{caused_by, resulted_in, impacts, derived_from}`
|
||||||
|
**Dann:** Finding `invalid_causal_edge_from_uncertain`
|
||||||
|
**Fix:** Replace with `related_to` oder remove edge
|
||||||
|
|
||||||
|
### L10 (WARN) Hub/Insight Note trägt Kausalität
|
||||||
|
**Wenn:** node.type == `insight` (oder Hub-Pattern) und hat `caused_by/resulted_in`
|
||||||
|
**Dann:** Finding `hub_has_causality`
|
||||||
|
**Fix:** Replace edges with `related_to` und verschiebe Kausalität in atomare Notes
|
||||||
|
|
||||||
|
### L11 (WARN) Principle uses caused_by for origin
|
||||||
|
**Wenn:** node.type == `principle` und enthält `caused_by` zu Erlebnissen
|
||||||
|
**Dann:** Finding `principle_origin_edge`
|
||||||
|
**Fix:** Vorschlag `derived_from` oder `based_on`
|
||||||
|
|
||||||
|
### L12 (INFO) Decision without caused_by
|
||||||
|
**Wenn:** node.type == `decision` und hat keine `caused_by`-Kanten
|
||||||
|
**Dann:** Finding `decision_without_causes`
|
||||||
|
**Fix:** Wizard: „Was war der Auslöser?“ → create open_question or add edges
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E. Redundanz & Zyklen
|
||||||
|
|
||||||
|
### L13 (WARN) Duplicate Edges
|
||||||
|
**Wenn:** identische canonical edge mehrfach gesetzt (src,type,dst)
|
||||||
|
**Dann:** Finding `duplicate_edge`
|
||||||
|
**Fix:** remove duplicates
|
||||||
|
|
||||||
|
### L14 (WARN) Cycles in pure causality subgraph
|
||||||
|
**Wenn:** Zyklus ausschließlich über `{caused_by,resulted_in,derived_from,source_of}`
|
||||||
|
**Dann:** Finding `causal_cycle`
|
||||||
|
**Fix:** Markiere zur Review; oft ist eine Kante falsch gerichtet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F. Traversal-Optimierung (Retrieval-Wirkung)
|
||||||
|
|
||||||
|
### L15 (INFO) Overuse of related_to
|
||||||
|
**Wenn:** Note enthält nur `related_to` und keine spezifischeren Kanten
|
||||||
|
**Dann:** Finding `weak_semantics`
|
||||||
|
**Fix:** Vorschlag: präzisere Beziehungstypen setzen
|
||||||
|
|
||||||
|
### L16 (INFO) Missing type metadata
|
||||||
|
**Wenn:** Frontmatter `type` fehlt
|
||||||
|
**Dann:** Finding `missing_type`
|
||||||
|
**Fix:** Prompt user to choose type; set via template
|
||||||
|
|
||||||
|
### L17 (INFO) Missing date on experience
|
||||||
|
**Wenn:** node.type == `experience` und kein Datum/Zeitraum vorhanden
|
||||||
|
**Dann:** Finding `missing_date_experience`
|
||||||
|
**Fix:** Prompt: „Wann ungefähr?“ → set `date` oder `time_range`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suggested Implementation Notes
|
||||||
|
- Jede Regel liefert:
|
||||||
|
- `ruleId`, `severity`, `message`, `location`, `evidence`, `quickFixes[]`
|
||||||
|
- QuickFixes sind Patch-Operationen:
|
||||||
|
- replaceText(range, text)
|
||||||
|
- insertBlock(atLine, block)
|
||||||
|
- createFile(path, content)
|
||||||
|
- renameFile(old, new) + updateLinks(glob)
|
||||||
39
docs/06_Roadmap/06_Feature-Backlog.md
Normal file
39
docs/06_Roadmap/06_Feature-Backlog.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# 📋 Mindnet Feature-Backlog (V3.1 - V4.0)
|
||||||
|
|
||||||
|
**Projekt:** Mindnet – Der Digitale Zwilling
|
||||||
|
**Status:** Aktiv nach Meilenstein WP-25b
|
||||||
|
**Architektur:** MoE, Multi-Stream RAG, Lazy-Prompt-Orchestration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 Priorisierte Features (V3.1 - V3.5)
|
||||||
|
|
||||||
|
### WP-26: Conversational Soul (Memory & Context)
|
||||||
|
* **Nutzerwert:** Ermöglicht echte Dialoge ("Erkläre mir den Punkt von eben genauer").
|
||||||
|
* **Lösungsskizze:**
|
||||||
|
* Implementierung eines `SessionStore` (SQLite) für Chat-Verläufe.
|
||||||
|
* Erweiterung der `DecisionEngine` um einen `ContextReducer`, der die Historie asynchron zusammenfasst (via Profil `compression_fast`).
|
||||||
|
* Einspeisung der Historie in die Synthese-Prompts via `{history}` Platzhalter.
|
||||||
|
* **Abhängigkeiten:** `DecisionEngine` v1.3.2, `LLMService` v3.5.6.
|
||||||
|
|
||||||
|
### WP-27: Autonomous Tuning (Self-Calibration)
|
||||||
|
* **Nutzerwert:** Das System lernt aus Fehlern und optimiert das Retrieval selbstständig.
|
||||||
|
* **Lösungsskizze:**
|
||||||
|
* Auswertung der JSONL-Feedback-Logs (Daumen hoch/runter).
|
||||||
|
* Automatisierte Anpassung von `edge_boosts` und `top_k` in einer neuen `tuning_registry.json`, die Werte in der `decision_engine.yaml` überschreibt.
|
||||||
|
* **Besonderheit:** Nutzt den `[PROMPT-TRACE]` zur Korrelation von Erfolg und Instruktions-Set.
|
||||||
|
|
||||||
|
### WP-28: Global Discovery Screen (UI)
|
||||||
|
* **Nutzerwert:** Eine zentrale Anlaufstelle für Suche und Wissens-Exploration.
|
||||||
|
* **Lösungsskizze:**
|
||||||
|
* Hybride Suche: Kombination aus Keyword (BM25), Vektor (Semantic) und Graph-Traversierung.
|
||||||
|
* "Reasoning-Trace" Visualisierung: Grafische Darstellung, warum ein Chunk gefunden wurde (Tracing über `stream_origin`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 Zukünftige Features (Ideenspeicher)
|
||||||
|
|
||||||
|
* **WP-13 (Agentic Layer):** MCP-Server Integration für externe Agenten (Claude/OpenAI).
|
||||||
|
* **WP-21 (Semantic Routing v2):** Dynamisches Intent-Boosting (Bessere Gewichte basierend auf der Frage-Art).
|
||||||
|
* **WP-18 (Graph Health):** Automatisierte Reparatur von "Dangling Edges" und Inconsistencies.
|
||||||
|
* **WP-24 (Knowledge Mining):** Automatisches Erstellen von Notiz-Drafts aus Chat-Erkenntnissen ("Chat-to-Vault").
|
||||||
29
docs/06_Roadmap/06_Sprintplanung_01.md
Normal file
29
docs/06_Roadmap/06_Sprintplanung_01.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# 🏃 Sprint-Planung: Sprint 1 (V3.1)
|
||||||
|
**Fokus:** Intelligenz & Gedächtnis
|
||||||
|
|
||||||
|
## 🎯 Sprint-Ziel
|
||||||
|
Mindnet kann sich an den Kontext des aktuellen Gesprächs erinnern und seine Retrieval-Logik basierend auf historischen Feedback-Daten anpassen.
|
||||||
|
|
||||||
|
## 🛠️ Aufgaben & Lösungsskizzen
|
||||||
|
|
||||||
|
### Aufgabe 1: SQLite Session Management (Backend)
|
||||||
|
* **Datei:** `app/core/retrieval/session_manager.py` (Neu)
|
||||||
|
* **Details:** Erstelle eine DB-Struktur für `sessions` und `messages`.
|
||||||
|
* **Logik:** Jede Anfrage im `chat.py` Endpunkt muss eine `session_id` verarbeiten.
|
||||||
|
|
||||||
|
### Aufgabe 2: Context Injection in DecisionEngine
|
||||||
|
* **Datei:** `app/core/retrieval/decision_engine.py`
|
||||||
|
* **Details:** 1. Abruf der letzten 5 Nachrichten aus dem `SessionManager`.
|
||||||
|
2. Verdichtung der Historie auf max. 500 Token via `llm_service.generate_raw_response(prompt_key="compression_template", ...)`.
|
||||||
|
3. Injection in `_generate_final_answer` als Variable `history`.
|
||||||
|
|
||||||
|
### Aufgabe 3: Das Tuning-Modul (Self-Calibration)
|
||||||
|
* **Datei:** `app/services/tuning_service.py` (Neu)
|
||||||
|
* **Details:** 1. Parser für `logs/feedback.jsonl`.
|
||||||
|
2. Logik: Wenn `negative_feedback` + `intent == 'CODING'`, erhöhe `tech_stream` boost um 0.5.
|
||||||
|
3. Persistenz in `config/tuning_registry.yaml`.
|
||||||
|
|
||||||
|
## 📦 Definition of Done (DoD)
|
||||||
|
- [ ] Test A: Rückfrage "Was meinst du damit?" bezieht sich auf das vorherige Ergebnis.
|
||||||
|
- [ ] Test B: Tuning-Werte werden nach manuellem Feedback in den Logs berücksichtigt.
|
||||||
|
- [ ] Test C: `[PROMPT-TRACE]` zeigt korrekte Level-1/2 Matches für Memory-Prompts.
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "mindnet",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
|
|
@ -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)]
|
# --- 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()
|
files.sort()
|
||||||
|
logger.info(f"Found {len(files)} relevant markdown files (filtered trash/system/hidden).")
|
||||||
logger.info(f"Found {len(files)} markdown files.")
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# 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."""
|
||||||
default_prefix = os.getenv("COLLECTION_PREFIX", "mindnet")
|
# WP-24c v4.5.9: load_dotenv() wurde bereits beim Modul-Import aufgerufen
|
||||||
|
# (oben, vor dem Logging-Setup, damit DEBUG=true korrekt gelesen wird)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Two-Pass Markdown Ingestion for Mindnet")
|
# Standard-Präfix aus Umgebungsvariable oder Fallback
|
||||||
parser.add_argument("--vault", default="./vault", help="Path to vault root")
|
default_prefix = os.getenv("COLLECTION_PREFIX", "mindnet")
|
||||||
parser.add_argument("--prefix", default=default_prefix, help="Collection prefix")
|
# Optionaler Vault-Root aus .env
|
||||||
parser.add_argument("--force", action="store_true", help="Force re-index all files")
|
default_vault = os.getenv("MINDNET_VAULT_ROOT", "./vault")
|
||||||
parser.add_argument("--apply", action="store_true", help="Perform writes to Qdrant")
|
|
||||||
|
parser = argparse.ArgumentParser(description="Mindnet Ingester: Two-Phase Markdown Import")
|
||||||
|
parser.add_argument("--vault", default=default_vault, help="Pfad zum Obsidian Vault")
|
||||||
|
parser.add_argument("--prefix", default=default_prefix, help="Qdrant Collection Präfix")
|
||||||
|
parser.add_argument("--force", action="store_true", help="Erzwingt Neu-Indizierung aller Dateien")
|
||||||
|
parser.add_argument("--apply", action="store_true", help="Schreibt physisch in die Datenbank")
|
||||||
|
|
||||||
args = parser.parse_args()
|
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:
|
||||||
|
|
|
||||||
69
scripts/verify_dto_import.py
Normal file
69
scripts/verify_dto_import.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script zur Verifikation der EdgeDTO-Import-Version in Prod.
|
||||||
|
Prüft, ob die korrekte Version des EdgeDTO-Modells geladen wird.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Stelle sicher, dass der Projekt-Pfad im Python-Path ist
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.models.dto import EdgeDTO
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
# Extrahiere die Literal-Definition aus dem Source-Code
|
||||||
|
source = inspect.getsource(EdgeDTO)
|
||||||
|
|
||||||
|
# Prüfe, ob explicit:callout in der Literal-Liste ist
|
||||||
|
if "explicit:callout" in source:
|
||||||
|
print("✅ EdgeDTO unterstützt 'explicit:callout'")
|
||||||
|
print(f" -> Modul-Pfad: {EdgeDTO.__module__}")
|
||||||
|
print(f" -> Datei: {inspect.getfile(EdgeDTO)}")
|
||||||
|
|
||||||
|
# Zeige die Provenance-Definition
|
||||||
|
import re
|
||||||
|
match = re.search(r'provenance.*?Literal\[(.*?)\]', source, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
literal_values = match.group(1)
|
||||||
|
if "explicit:callout" in literal_values:
|
||||||
|
print("✅ 'explicit:callout' ist in der Literal-Liste enthalten")
|
||||||
|
print(f"\n Literal-Werte (erste 200 Zeichen):\n {literal_values[:200]}...")
|
||||||
|
else:
|
||||||
|
print("❌ 'explicit:callout' ist NICHT in der Literal-Liste!")
|
||||||
|
print(f"\n Gefundene Literal-Werte:\n {literal_values}")
|
||||||
|
else:
|
||||||
|
print("⚠️ Konnte Literal-Definition nicht finden")
|
||||||
|
else:
|
||||||
|
print("❌ EdgeDTO unterstützt NICHT 'explicit:callout'")
|
||||||
|
print(f" -> Modul-Pfad: {EdgeDTO.__module__}")
|
||||||
|
print(f" -> Datei: {inspect.getfile(EdgeDTO)}")
|
||||||
|
print("\n Source-Code (erste 500 Zeichen):")
|
||||||
|
print(f" {source[:500]}...")
|
||||||
|
|
||||||
|
# Test: Versuche ein EdgeDTO mit explicit:callout zu erstellen
|
||||||
|
print("\n🧪 Test: Erstelle EdgeDTO mit provenance='explicit:callout'...")
|
||||||
|
try:
|
||||||
|
test_edge = EdgeDTO(
|
||||||
|
id="test",
|
||||||
|
kind="test",
|
||||||
|
source="test",
|
||||||
|
target="test",
|
||||||
|
weight=1.0,
|
||||||
|
provenance="explicit:callout"
|
||||||
|
)
|
||||||
|
print("✅ EdgeDTO mit 'explicit:callout' erfolgreich erstellt!")
|
||||||
|
print(f" -> Provenance: {test_edge.provenance}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Fehler beim Erstellen: {e}")
|
||||||
|
print(f" -> Typ: {type(e).__name__}")
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"❌ Import-Fehler: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Unerwarteter Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
114
scripts/verify_env_loading.py
Normal file
114
scripts/verify_env_loading.py
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script zur Verifikation des .env-Ladens in Prod.
|
||||||
|
Prüft, ob die .env-Datei korrekt geladen wird und welche Werte tatsächlich verwendet werden.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Stelle sicher, dass der Projekt-Pfad im Python-Path ist
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("🔍 .env-Lade-Verifikation")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
# 1. Prüfe, ob .env-Datei existiert
|
||||||
|
env_files = [
|
||||||
|
project_root / ".env",
|
||||||
|
project_root / "prod.env",
|
||||||
|
project_root / "config" / "prod.env",
|
||||||
|
Path.cwd() / ".env",
|
||||||
|
Path.cwd() / "prod.env",
|
||||||
|
]
|
||||||
|
|
||||||
|
print("\n1. Suche nach .env-Dateien:")
|
||||||
|
found_env = None
|
||||||
|
for env_file in env_files:
|
||||||
|
if env_file.exists():
|
||||||
|
print(f" ✅ Gefunden: {env_file}")
|
||||||
|
if found_env is None:
|
||||||
|
found_env = env_file
|
||||||
|
else:
|
||||||
|
print(f" ❌ Nicht gefunden: {env_file}")
|
||||||
|
|
||||||
|
if not found_env:
|
||||||
|
print("\n ⚠️ WARNUNG: Keine .env-Datei gefunden!")
|
||||||
|
print(" -> load_dotenv() wird Standard-Werte verwenden")
|
||||||
|
|
||||||
|
# 2. Lade .env manuell
|
||||||
|
print("\n2. Lade .env-Datei:")
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
if found_env:
|
||||||
|
result = load_dotenv(found_env, override=True)
|
||||||
|
print(f" ✅ load_dotenv('{found_env}') = {result}")
|
||||||
|
else:
|
||||||
|
result = load_dotenv(override=True)
|
||||||
|
print(f" ⚠️ load_dotenv() ohne expliziten Pfad = {result}")
|
||||||
|
print(" -> Sucht automatisch nach .env im aktuellen Verzeichnis")
|
||||||
|
|
||||||
|
# 3. Prüfe kritische Umgebungsvariablen
|
||||||
|
print("\n3. Kritische Umgebungsvariablen:")
|
||||||
|
critical_vars = [
|
||||||
|
"COLLECTION_PREFIX",
|
||||||
|
"MINDNET_PREFIX",
|
||||||
|
"DEBUG",
|
||||||
|
"VECTOR_DIM",
|
||||||
|
"MINDNET_EMBEDDING_MODEL",
|
||||||
|
"QDRANT_URL",
|
||||||
|
]
|
||||||
|
|
||||||
|
for var in critical_vars:
|
||||||
|
value = os.getenv(var, "NICHT GESETZT")
|
||||||
|
source = "Umgebung" if var in os.environ else "Default/Code"
|
||||||
|
print(f" {var:30} = {value:40} ({source})")
|
||||||
|
|
||||||
|
# 4. Prüfe, welche .env-Datei tatsächlich geladen wurde
|
||||||
|
print("\n4. Verifikation der geladenen Werte:")
|
||||||
|
print(f" Arbeitsverzeichnis: {Path.cwd()}")
|
||||||
|
print(f" Projekt-Root: {project_root}")
|
||||||
|
print(f" Python-Pfad[0]: {sys.path[0] if sys.path else 'N/A'}")
|
||||||
|
|
||||||
|
# 5. Test: Importiere Settings
|
||||||
|
print("\n5. Test: Importiere Settings aus app.config:")
|
||||||
|
try:
|
||||||
|
from app.config import get_settings
|
||||||
|
settings = get_settings()
|
||||||
|
print(f" ✅ Settings erfolgreich geladen")
|
||||||
|
print(f" -> COLLECTION_PREFIX: {settings.COLLECTION_PREFIX}")
|
||||||
|
print(f" -> VECTOR_SIZE: {settings.VECTOR_SIZE}")
|
||||||
|
print(f" -> EMBEDDING_MODEL: {settings.EMBEDDING_MODEL}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Fehler beim Laden der Settings: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# 6. Test: Prüfe EdgeDTO
|
||||||
|
print("\n6. Test: Prüfe EdgeDTO-Import:")
|
||||||
|
try:
|
||||||
|
from app.models.dto import EdgeDTO
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
source = inspect.getsource(EdgeDTO)
|
||||||
|
if "explicit:callout" in source:
|
||||||
|
print(" ✅ EdgeDTO unterstützt 'explicit:callout'")
|
||||||
|
print(f" -> Modul-Pfad: {EdgeDTO.__module__}")
|
||||||
|
print(f" -> Datei: {inspect.getfile(EdgeDTO)}")
|
||||||
|
else:
|
||||||
|
print(" ❌ EdgeDTO unterstützt NICHT 'explicit:callout'")
|
||||||
|
|
||||||
|
# Test-Erstellung
|
||||||
|
test_edge = EdgeDTO(
|
||||||
|
id="test", kind="test", source="test", target="test",
|
||||||
|
weight=1.0, provenance="explicit:callout"
|
||||||
|
)
|
||||||
|
print(" ✅ EdgeDTO mit 'explicit:callout' erfolgreich erstellt!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
print("\n" + "=" * 80)
|
||||||
160
scripts/verify_runtime_service.py
Normal file
160
scripts/verify_runtime_service.py
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script zur Laufzeit-Verifikation des laufenden Mindnet-Services.
|
||||||
|
Prüft, ob der Service die korrekte EdgeDTO-Version verwendet und ob die .env korrekt geladen wurde.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Stelle sicher, dass der Projekt-Pfad im Python-Path ist
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("🔍 Laufzeit-Verifikation des Mindnet-Services")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
# 1. Prüfe Health-Endpoint
|
||||||
|
print("\n1. Prüfe Service-Status (Health-Check):")
|
||||||
|
try:
|
||||||
|
response = requests.get("http://localhost:8001/healthz", timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
health_data = response.json()
|
||||||
|
print(f" ✅ Service läuft")
|
||||||
|
print(f" -> Status: {health_data.get('status')}")
|
||||||
|
print(f" -> Version: {health_data.get('version')}")
|
||||||
|
print(f" -> Prefix: {health_data.get('prefix')}")
|
||||||
|
print(f" -> Qdrant: {health_data.get('qdrant')}")
|
||||||
|
|
||||||
|
# WP-24c v4.5.10: Prüfe EdgeDTO-Version im laufenden Service
|
||||||
|
edge_dto_supports = health_data.get('edge_dto_supports_callout')
|
||||||
|
if edge_dto_supports is not None:
|
||||||
|
if edge_dto_supports:
|
||||||
|
print(f" ✅ Service unterstützt 'explicit:callout' (zur Laufzeit verifiziert)")
|
||||||
|
else:
|
||||||
|
print(f" ❌ Service unterstützt NICHT 'explicit:callout' (alte Version im Speicher!)")
|
||||||
|
print(f" -> Aktion erforderlich: Cache leeren und Service neu starten")
|
||||||
|
else:
|
||||||
|
print(f" ⚠️ Health-Check meldet keine EdgeDTO-Version (alte API-Version?)")
|
||||||
|
else:
|
||||||
|
print(f" ⚠️ Service antwortet mit Status {response.status_code}")
|
||||||
|
print(f" -> Response: {response.text[:200]}")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print(" ❌ Service nicht erreichbar (läuft er auf Port 8001?)")
|
||||||
|
print(" -> Tipp: sudo systemctl status mindnet-prod")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Fehler beim Health-Check: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 2. Test: Versuche eine Test-Query mit explicit:callout Edge
|
||||||
|
print("\n2. Test: Retrieval mit explicit:callout Edge:")
|
||||||
|
print(" -> Sende Test-Query an /chat/...")
|
||||||
|
print(" -> Hinweis: Timeout nach 30s ist möglich bei LLM-Calls")
|
||||||
|
try:
|
||||||
|
test_query = {
|
||||||
|
"message": "Test query für EdgeDTO-Verifikation",
|
||||||
|
"explain": False
|
||||||
|
}
|
||||||
|
# WP-24c: Router ist mit prefix="/chat" eingebunden, Endpoint ist "/"
|
||||||
|
# Erhöhter Timeout für LLM-Calls (können länger dauern)
|
||||||
|
response = requests.post(
|
||||||
|
"http://localhost:8001/chat/",
|
||||||
|
json=test_query,
|
||||||
|
timeout=60 # Erhöht von 30 auf 60 Sekunden
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
print(f" ✅ Query erfolgreich verarbeitet")
|
||||||
|
print(f" -> Antwort-Länge: {len(result.get('response', ''))} Zeichen")
|
||||||
|
|
||||||
|
# Prüfe Logs auf EdgeDTO-Fehler (wenn verfügbar)
|
||||||
|
if 'warnings' in result or 'errors' in result:
|
||||||
|
warnings = result.get('warnings', [])
|
||||||
|
errors = result.get('errors', [])
|
||||||
|
if warnings:
|
||||||
|
print(f" ⚠️ Warnings: {warnings}")
|
||||||
|
if errors:
|
||||||
|
print(f" ❌ Errors: {errors}")
|
||||||
|
else:
|
||||||
|
print(f" ⚠️ Query fehlgeschlagen mit Status {response.status_code}")
|
||||||
|
print(f" -> Response: {response.text[:500]}")
|
||||||
|
|
||||||
|
except requests.exceptions.ReadTimeout:
|
||||||
|
print(f" ⚠️ Query-Timeout (Service antwortet nicht innerhalb von 60s)")
|
||||||
|
print(f" -> Mögliche Ursachen:")
|
||||||
|
print(f" - LLM-Call dauert länger als erwartet")
|
||||||
|
print(f" - Service hängt bei der Verarbeitung")
|
||||||
|
print(f" -> Tipp: Prüfe Service-Logs mit: sudo journalctl -u mindnet-prod -n 50")
|
||||||
|
print(f" -> WICHTIG: EdgeDTO-Problem ist gelöst (siehe Punkt 1)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ Fehler bei Test-Query: {e}")
|
||||||
|
print(f" -> WICHTIG: EdgeDTO-Problem ist gelöst (siehe Punkt 1)")
|
||||||
|
# Kein vollständiger Traceback für Timeouts, da diese erwartbar sind
|
||||||
|
|
||||||
|
# 3. Direkte Code-Verifikation (falls Service-Code zugänglich)
|
||||||
|
print("\n3. Code-Verifikation (lokale Dateien):")
|
||||||
|
try:
|
||||||
|
from app.models.dto import EdgeDTO
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
source = inspect.getsource(EdgeDTO)
|
||||||
|
if "explicit:callout" in source:
|
||||||
|
print(" ✅ Lokaler Code unterstützt 'explicit:callout'")
|
||||||
|
print(f" -> Datei: {inspect.getfile(EdgeDTO)}")
|
||||||
|
|
||||||
|
# Prüfe, ob die Datei aktuell ist
|
||||||
|
dto_file = Path(inspect.getfile(EdgeDTO))
|
||||||
|
if dto_file.exists():
|
||||||
|
import time
|
||||||
|
mtime = dto_file.stat().st_mtime
|
||||||
|
mtime_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime))
|
||||||
|
print(f" -> Letzte Änderung: {mtime_str}")
|
||||||
|
else:
|
||||||
|
print(" ❌ Lokaler Code unterstützt NICHT 'explicit:callout'")
|
||||||
|
|
||||||
|
# Test-Erstellung
|
||||||
|
test_edge = EdgeDTO(
|
||||||
|
id="test", kind="test", source="test", target="test",
|
||||||
|
weight=1.0, provenance="explicit:callout"
|
||||||
|
)
|
||||||
|
print(" ✅ EdgeDTO mit 'explicit:callout' erfolgreich erstellt!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Fehler bei Code-Verifikation: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# 4. Prüfe Python-Cache
|
||||||
|
print("\n4. Prüfe Python-Cache:")
|
||||||
|
try:
|
||||||
|
dto_cache_dir = project_root / "app" / "models" / "__pycache__"
|
||||||
|
if dto_cache_dir.exists():
|
||||||
|
cache_files = list(dto_cache_dir.glob("dto*.pyc"))
|
||||||
|
if cache_files:
|
||||||
|
print(f" ⚠️ Gefunden: {len(cache_files)} Cache-Datei(en) in app/models/__pycache__")
|
||||||
|
print(f" -> Tipp: Cache leeren mit: find . -type d -name __pycache__ -exec rm -r {{}} +")
|
||||||
|
else:
|
||||||
|
print(" ✅ Keine Cache-Dateien gefunden")
|
||||||
|
else:
|
||||||
|
print(" ✅ Kein Cache-Verzeichnis vorhanden")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ Fehler bei Cache-Prüfung: {e}")
|
||||||
|
|
||||||
|
# 5. Empfehlungen
|
||||||
|
print("\n5. Empfehlungen:")
|
||||||
|
print(" 📋 Wenn der Service noch eine alte Version verwendet:")
|
||||||
|
print(" 1. Python-Cache leeren:")
|
||||||
|
print(" find . -type d -name __pycache__ -exec rm -r {} + 2>/dev/null || true")
|
||||||
|
print(" find . -name '*.pyc' -delete")
|
||||||
|
print(" 2. Service neu starten:")
|
||||||
|
print(" sudo systemctl restart mindnet-prod")
|
||||||
|
print(" 3. Status prüfen:")
|
||||||
|
print(" sudo systemctl status mindnet-prod")
|
||||||
|
print(" 4. Logs prüfen:")
|
||||||
|
print(" sudo journalctl -u mindnet-prod -f")
|
||||||
|
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
|
@ -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}"
|
||||||
|
|
|
||||||
171
tests/test_callout_edges.py
Normal file
171
tests/test_callout_edges.py
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
tests/test_callout_edges.py
|
||||||
|
Unit-Tests für extract_callout_relations Funktion.
|
||||||
|
Testet einfache und verschachtelte Callout-Formate.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from app.core.graph.graph_extractors import extract_callout_relations
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalloutEdges(unittest.TestCase):
|
||||||
|
"""Testet die Extraktion von Edge-Callouts aus Markdown."""
|
||||||
|
|
||||||
|
def test_simple_callout_single_target(self):
|
||||||
|
"""Test: Einfaches Callout mit einem Target."""
|
||||||
|
text = """> [!edge] related_to
|
||||||
|
> [[Target]]"""
|
||||||
|
|
||||||
|
pairs, remaining = extract_callout_relations(text)
|
||||||
|
|
||||||
|
self.assertEqual(len(pairs), 1)
|
||||||
|
self.assertEqual(pairs[0], ("related_to", "Target"))
|
||||||
|
self.assertNotIn("[!edge]", remaining)
|
||||||
|
|
||||||
|
def test_simple_callout_multiple_targets(self):
|
||||||
|
"""Test: Einfaches Callout mit mehreren Targets."""
|
||||||
|
text = """> [!edge] related_to
|
||||||
|
> [[Target1]]
|
||||||
|
> [[Target2]]
|
||||||
|
> [[Target3]]"""
|
||||||
|
|
||||||
|
pairs, remaining = extract_callout_relations(text)
|
||||||
|
|
||||||
|
self.assertEqual(len(pairs), 3)
|
||||||
|
self.assertIn(("related_to", "Target1"), pairs)
|
||||||
|
self.assertIn(("related_to", "Target2"), pairs)
|
||||||
|
self.assertIn(("related_to", "Target3"), pairs)
|
||||||
|
|
||||||
|
def test_nested_callout_single_edge(self):
|
||||||
|
"""Test: Verschachteltes Callout mit einem Edge-Typ."""
|
||||||
|
text = """> [!abstract]- 🕸️ Semantic Mapping
|
||||||
|
>> [!edge] related_to
|
||||||
|
>> [[Brigitte]]
|
||||||
|
>> [[Klaus]]
|
||||||
|
>> [[Salami Fertigpizza]]"""
|
||||||
|
|
||||||
|
pairs, remaining = extract_callout_relations(text)
|
||||||
|
|
||||||
|
self.assertEqual(len(pairs), 3)
|
||||||
|
self.assertIn(("related_to", "Brigitte"), pairs)
|
||||||
|
self.assertIn(("related_to", "Klaus"), pairs)
|
||||||
|
self.assertIn(("related_to", "Salami Fertigpizza"), pairs)
|
||||||
|
|
||||||
|
def test_nested_callout_multiple_edges(self):
|
||||||
|
"""Test: Verschachteltes Callout mit mehreren Edge-Typen."""
|
||||||
|
text = """> [!abstract]- 🕸️ Semantic Mapping
|
||||||
|
>> [!edge] related_to
|
||||||
|
>> [[Brigitte]]
|
||||||
|
>> [[Klaus]]
|
||||||
|
>> [[Salami Fertigpizza]]
|
||||||
|
>
|
||||||
|
>> [!edge] derived_from
|
||||||
|
>> [[Link 2]]"""
|
||||||
|
|
||||||
|
pairs, remaining = extract_callout_relations(text)
|
||||||
|
|
||||||
|
# Prüfe related_to Edges
|
||||||
|
related_pairs = [p for p in pairs if p[0] == "related_to"]
|
||||||
|
self.assertEqual(len(related_pairs), 3)
|
||||||
|
self.assertIn(("related_to", "Brigitte"), pairs)
|
||||||
|
self.assertIn(("related_to", "Klaus"), pairs)
|
||||||
|
self.assertIn(("related_to", "Salami Fertigpizza"), pairs)
|
||||||
|
|
||||||
|
# Prüfe derived_from Edge
|
||||||
|
derived_pairs = [p for p in pairs if p[0] == "derived_from"]
|
||||||
|
self.assertEqual(len(derived_pairs), 1)
|
||||||
|
self.assertIn(("derived_from", "Link 2"), pairs)
|
||||||
|
|
||||||
|
def test_mixed_callouts(self):
|
||||||
|
"""Test: Gemischte einfache und verschachtelte Callouts."""
|
||||||
|
text = """> [!edge] depends_on
|
||||||
|
> [[Dependency1]]
|
||||||
|
|
||||||
|
> [!abstract] Test
|
||||||
|
>> [!edge] related_to
|
||||||
|
>> [[Target1]]
|
||||||
|
>> [!edge] derived_from
|
||||||
|
>> [[Target2]]"""
|
||||||
|
|
||||||
|
pairs, remaining = extract_callout_relations(text)
|
||||||
|
|
||||||
|
# Einfaches Callout
|
||||||
|
self.assertIn(("depends_on", "Dependency1"), pairs)
|
||||||
|
|
||||||
|
# Verschachtelte Callouts
|
||||||
|
self.assertIn(("related_to", "Target1"), pairs)
|
||||||
|
self.assertIn(("derived_from", "Target2"), pairs)
|
||||||
|
|
||||||
|
def test_callout_with_explicit_format(self):
|
||||||
|
"""Test: Callout mit explizitem 'kind: targets' Format."""
|
||||||
|
text = """> [!edge] related_to: [[Target1]] [[Target2]]"""
|
||||||
|
|
||||||
|
pairs, remaining = extract_callout_relations(text)
|
||||||
|
|
||||||
|
self.assertEqual(len(pairs), 2)
|
||||||
|
self.assertIn(("related_to", "Target1"), pairs)
|
||||||
|
self.assertIn(("related_to", "Target2"), pairs)
|
||||||
|
|
||||||
|
def test_empty_callout(self):
|
||||||
|
"""Test: Leeres Callout ohne Targets."""
|
||||||
|
text = """> [!edge] related_to"""
|
||||||
|
|
||||||
|
pairs, remaining = extract_callout_relations(text)
|
||||||
|
|
||||||
|
self.assertEqual(len(pairs), 0)
|
||||||
|
|
||||||
|
def test_no_callouts(self):
|
||||||
|
"""Test: Text ohne Callouts bleibt unverändert."""
|
||||||
|
text = """Normaler Text ohne Callouts.
|
||||||
|
[[Ein normaler Link]]"""
|
||||||
|
|
||||||
|
pairs, remaining = extract_callout_relations(text)
|
||||||
|
|
||||||
|
self.assertEqual(len(pairs), 0)
|
||||||
|
self.assertEqual(remaining, text)
|
||||||
|
|
||||||
|
def test_callout_with_section_links(self):
|
||||||
|
"""Test: Callout mit Section-Links (Deep-Links)."""
|
||||||
|
text = """> [!edge] related_to
|
||||||
|
> [[Target#Section1]]
|
||||||
|
> [[Target#Section2]]"""
|
||||||
|
|
||||||
|
pairs, remaining = extract_callout_relations(text)
|
||||||
|
|
||||||
|
self.assertEqual(len(pairs), 2)
|
||||||
|
self.assertIn(("related_to", "Target#Section1"), pairs)
|
||||||
|
self.assertIn(("related_to", "Target#Section2"), pairs)
|
||||||
|
|
||||||
|
def test_original_format_example(self):
|
||||||
|
"""Test: Das ursprünglich gefragte Format aus der Roadmap."""
|
||||||
|
text = """> [!abstract]- 🕸️ Semantic Mapping
|
||||||
|
>> [!edge] related_to
|
||||||
|
>> [[Brigitte]]
|
||||||
|
>> [[Klaus]]
|
||||||
|
>> [[Salami Fertigpizza]]
|
||||||
|
>
|
||||||
|
>> [!edge] derived_from
|
||||||
|
>> [[Link 2]]"""
|
||||||
|
|
||||||
|
pairs, remaining = extract_callout_relations(text)
|
||||||
|
|
||||||
|
# Prüfe, dass alle Edges erkannt wurden
|
||||||
|
self.assertEqual(len(pairs), 4)
|
||||||
|
|
||||||
|
# Prüfe related_to Edges
|
||||||
|
related_pairs = [p for p in pairs if p[0] == "related_to"]
|
||||||
|
self.assertEqual(len(related_pairs), 3)
|
||||||
|
self.assertIn(("related_to", "Brigitte"), pairs)
|
||||||
|
self.assertIn(("related_to", "Klaus"), pairs)
|
||||||
|
self.assertIn(("related_to", "Salami Fertigpizza"), pairs)
|
||||||
|
|
||||||
|
# Prüfe derived_from Edge
|
||||||
|
derived_pairs = [p for p in pairs if p[0] == "derived_from"]
|
||||||
|
self.assertEqual(len(derived_pairs), 1)
|
||||||
|
self.assertIn(("derived_from", "Link 2"), pairs)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in New Issue
Block a user