Compare commits

..

No commits in common. "d2e0b48aa5a758b457b3f656e40ff22326686c17" and "c741cc7d1bac267002dfa7f051f47c6ee9ea1cf1" have entirely different histories.

26 changed files with 976 additions and 2031 deletions

View File

@ -1,6 +1,6 @@
# mindnet v2.4 — Programmplan # mindnet v2.4 — Programmplan
**Version:** 2.6.0 (Inkl. WP-15 Smart Edge Allocation) **Version:** 2.4.0 (Inkl. WP-11 Backend Intelligence)
**Stand:** 2025-12-12 **Stand:** 2025-12-11
**Status:** Aktiv **Status:** Aktiv
--- ---
@ -33,9 +33,6 @@
- [WP-12 Knowledge Rewriter (Soft Mode, geplant)](#wp-12--knowledge-rewriter-soft-mode-geplant) - [WP-12 Knowledge Rewriter (Soft Mode, geplant)](#wp-12--knowledge-rewriter-soft-mode-geplant)
- [WP-13 MCP-Integration \& Agenten-Layer (geplant)](#wp-13--mcp-integration--agenten-layer-geplant) - [WP-13 MCP-Integration \& Agenten-Layer (geplant)](#wp-13--mcp-integration--agenten-layer-geplant)
- [WP-14 Review / Refactoring / Dokumentation (geplant)](#wp-14--review--refactoring--dokumentation-geplant) - [WP-14 Review / Refactoring / Dokumentation (geplant)](#wp-14--review--refactoring--dokumentation-geplant)
- [WP-15 Smart Edge Allocation \& Chunking Strategies (abgeschlossen)](#wp-15--smart-edge-allocation--chunking-strategies-abgeschlossen)
- [WP-16 Auto-Discovery \& Enrichment (geplant)](#wp-16--auto-discovery--enrichment-geplant)
- [WP-17 Conversational Memory (Dialog-Gedächtnis) (geplant)](#wp-17--conversational-memory-dialog-gedächtnis-geplant)
- [7. Abhängigkeiten (vereinfacht, aktualisiert)](#7-abhängigkeiten-vereinfacht-aktualisiert) - [7. Abhängigkeiten (vereinfacht, aktualisiert)](#7-abhängigkeiten-vereinfacht-aktualisiert)
- [8. Laufzeit- \& Komplexitätsindikatoren (aktualisiert)](#8-laufzeit---komplexitätsindikatoren-aktualisiert) - [8. Laufzeit- \& Komplexitätsindikatoren (aktualisiert)](#8-laufzeit---komplexitätsindikatoren-aktualisiert)
- [9. Programmfortschritt (Ampel, aktualisiert)](#9-programmfortschritt-ampel-aktualisiert) - [9. Programmfortschritt (Ampel, aktualisiert)](#9-programmfortschritt-ampel-aktualisiert)
@ -517,63 +514,6 @@ Aufräumen, dokumentieren, stabilisieren insbesondere für Onboarding Dritte
--- ---
### WP-15 Smart Edge Allocation & Chunking Strategies (abgeschlossen)
**Phase:** D
**Status:** 🟢 abgeschlossen
**Ziel:**
Einführung einer intelligenten Verteilung von Wissenskanten an spezifische Text-Chunks, um die Präzision des Retrievals zu erhöhen, ohne die Stabilität des Systems durch lange LLM-Verarbeitungszeiten zu gefährden.
**Erreichte Ergebnisse:**
- **5-Stufen-Workflow:** Implementierung von "Smart Edge Allocation" (Global Scan -> Deterministic Split -> LLM Filter -> Injection -> Fallback).
- **Neue Chunking-Strategien:** Einführung von `by_heading` (für strukturierte Daten) und verbessertem `sliding_window` als deterministische Basis.
- **Robustheit:** Trennung von Zerlegung (Code) und Analyse (LLM). Bei LLM-Fehlern oder Timeouts greift ein Fallback-Mechanismus (Datenverlust ausgeschlossen).
- **Architektur:** Trennung der Orchestrierung (`chunker.py`) von der KI-Logik (`semantic_analyzer.py`).
- **Konfiguration:** Steuerung über `types.yaml` (`enable_smart_edge_allocation`) ermöglicht granulare Anpassung pro Notiztyp.
**Aufwand / Komplexität:**
- Aufwand: Mittel
- Komplexität: Hoch
---
### WP-16 Auto-Discovery & Enrichment (geplant)
**Phase:** D
**Status:** 🟡 geplant
**Ziel:**
Automatisches Erkennen und Vorschlagen von fehlenden Kanten in "dummem" Text (ohne explizite Wikilinks) vor der Speicherung. Umwandlung von Text in "smarten Text" durch Nutzung des `DiscoveryService`.
**Umfang:**
- Integration eines "Enrichers" in die Ingestion-Pipeline (Schritt 0).
- Unterscheidung zwischen "Hard Candidates" (explizite Links) und "Soft Candidates" (Vektor-basierte Vorschläge).
- LLM-basierte Verifikation der Vorschläge zur Vermeidung von Halluzinationen.
**Aufwand / Komplexität:**
- Aufwand: Mittel
- Komplexität: Hoch
---
### WP-17 Conversational Memory (Dialog-Gedächtnis) (geplant)
**Phase:** D
**Status:** 🟡 geplant
**Ziel:**
Erweiterung des Chat-Backends von einem statischen Request-Response-Modell zu einem zustandsbehafteten Dialogsystem ("Multi-Turn Conversation"). Das System soll sich an vorherige Aussagen im aktuellen Gesprächsverlauf erinnern, um Rückfragen ("Was meinst du damit?") korrekt zu interpretieren.
**Umfang:**
- **API-Erweiterung:** `ChatRequest` DTO erhält ein `history`-Feld (Liste von Nachrichten).
- **Context Management:** Implementierung einer Token-Budget-Logik, die dynamisch zwischen RAG-Kontext (Dokumente) und Dialog-Verlauf (History) balanciert, um das Kontextfenster (8k) optimal zu nutzen.
- **Prompt Engineering:** Integration eines `{chat_history}` Platzhalters in den System-Prompt.
- **Frontend-Update:** Die `ui.py` muss die letzten N Nachrichtenpaare (User/AI) bei jedem Request mitsenden.
**Aufwand / Komplexität:**
- Aufwand: Mittel
- Komplexität: Mittel
---
## 7. Abhängigkeiten (vereinfacht, aktualisiert) ## 7. Abhängigkeiten (vereinfacht, aktualisiert)
WP01 → WP02 → WP03 → WP04a WP01 → WP02 → WP03 → WP04a
@ -582,8 +522,6 @@ Erweiterung des Chat-Backends von einem statischen Request-Response-Modell zu ei
WP07 → WP10a WP07 → WP10a
WP03 → WP09 WP03 → WP09
WP01/WP03 → WP10 → WP11 → WP12 WP01/WP03 → WP10 → WP11 → WP12
WP10 → WP17
WP11 → WP15 → WP16
WP03/WP04 → WP13 WP03/WP04 → WP13
Alles → WP14 Alles → WP14
@ -606,9 +544,6 @@ Erweiterung des Chat-Backends von einem statischen Request-Response-Modell zu ei
| WP12 | Niedrig/Mittel | Mittel | | WP12 | Niedrig/Mittel | Mittel |
| WP13 | Mittel | Mittel | | WP13 | Mittel | Mittel |
| WP14 | Mittel | Niedrig/Mittel | | WP14 | Mittel | Niedrig/Mittel |
| WP15 | Mittel | Hoch |
| WP16 | Mittel | Hoch |
| WP17 | Mittel | Mittel |
--- ---
@ -634,9 +569,6 @@ Erweiterung des Chat-Backends von einem statischen Request-Response-Modell zu ei
| WP12 | 🟡 | | WP12 | 🟡 |
| WP13 | 🟡 | | WP13 | 🟡 |
| WP14 | 🟡 | | WP14 | 🟡 |
| WP15 | 🟢 |
| WP16 | 🟡 |
| WP17 | 🟡 |
--- ---

13
app/core/chunk_config.py Normal file
View File

@ -0,0 +1,13 @@
TYPE_SIZES = {
"thought": {"target": (150, 250), "max": 300, "overlap": (30, 40)},
"experience":{"target": (250, 350), "max": 450, "overlap": (40, 60)},
"journal": {"target": (200, 300), "max": 400, "overlap": (30, 50)},
"task": {"target": (120, 200), "max": 250, "overlap": (20, 30)},
"project": {"target": (300, 450), "max": 600, "overlap": (50, 70)},
"concept": {"target": (250, 400), "max": 550, "overlap": (40, 60)},
"source": {"target": (200, 350), "max": 500, "overlap": (30, 50)},
}
DEFAULT = {"target": (250, 350), "max": 500, "overlap": (40, 60)}
def get_sizes(note_type: str):
return TYPE_SIZES.get(str(note_type).lower(), DEFAULT)

View File

@ -1,330 +1,226 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Dict, Optional, Tuple, Any, Set from typing import List, Dict, Optional, Tuple
import re import re
import math import math
import yaml
from pathlib import Path
from markdown_it import MarkdownIt from markdown_it import MarkdownIt
from markdown_it.token import Token from markdown_it.token import Token
import asyncio from .chunk_config import get_sizes
import logging
# Services # --- Hilfen ---
from app.services.semantic_analyzer import get_semantic_analyzer _SENT_SPLIT = re.compile(r'(?<=[.!?])\s+(?=[A-ZÄÖÜ0-9„(])')
_WS = re.compile(r'\s+')
# Core Imports
try:
from app.core.derive_edges import build_edges_for_note
except ImportError:
# Mock für Tests
def build_edges_for_note(note_id, chunks, note_level_references=None, include_note_scope_refs=False): return []
logger = logging.getLogger(__name__)
# ==========================================
# 1. HELPER & CONFIG
# ==========================================
BASE_DIR = Path(__file__).resolve().parent.parent.parent
CONFIG_PATH = BASE_DIR / "config" / "types.yaml"
DEFAULT_PROFILE = {"strategy": "sliding_window", "target": 400, "max": 600, "overlap": (50, 80)}
_CONFIG_CACHE = None
def _load_yaml_config() -> Dict[str, Any]:
global _CONFIG_CACHE
if _CONFIG_CACHE is not None: return _CONFIG_CACHE
if not CONFIG_PATH.exists(): return {}
try:
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
_CONFIG_CACHE = data
return data
except Exception: return {}
def get_chunk_config(note_type: str) -> Dict[str, Any]:
full_config = _load_yaml_config()
profiles = full_config.get("chunking_profiles", {})
type_def = full_config.get("types", {}).get(note_type.lower(), {})
profile_name = type_def.get("chunking_profile")
if not profile_name:
profile_name = full_config.get("defaults", {}).get("chunking_profile", "sliding_standard")
config = profiles.get(profile_name, DEFAULT_PROFILE).copy()
if "overlap" in config and isinstance(config["overlap"], list):
config["overlap"] = tuple(config["overlap"])
return config
def extract_frontmatter_from_text(md_text: str) -> Tuple[Dict[str, Any], str]:
fm_match = re.match(r'^\s*---\s*\n(.*?)\n---', md_text, re.DOTALL)
if not fm_match: return {}, md_text
try:
frontmatter = yaml.safe_load(fm_match.group(1))
if not isinstance(frontmatter, dict): frontmatter = {}
except yaml.YAMLError:
frontmatter = {}
text_without_fm = re.sub(r'^\s*---\s*\n(.*?)\n---', '', md_text, flags=re.DOTALL)
return frontmatter, text_without_fm.strip()
# ==========================================
# 2. DATA CLASSES
# ==========================================
_SENT_SPLIT = re.compile(r'(?<=[.!?])\s+(?=[A-ZÄÖÜ0-9„(])'); _WS = re.compile(r'\s+')
def estimate_tokens(text: str) -> int: def estimate_tokens(text: str) -> int:
return max(1, math.ceil(len(text.strip()) / 4)) # leichte Approximation: 1 Token ≈ 4 Zeichen; robust + schnell
t = len(text.strip())
return max(1, math.ceil(t / 4))
def split_sentences(text: str) -> list[str]: def split_sentences(text: str) -> list[str]:
text = _WS.sub(' ', text.strip()) text = _WS.sub(' ', text.strip())
if not text: return [] if not text:
return []
parts = _SENT_SPLIT.split(text) parts = _SENT_SPLIT.split(text)
return [p.strip() for p in parts if p.strip()] return [p.strip() for p in parts if p.strip()]
@dataclass @dataclass
class RawBlock: class RawBlock:
kind: str; text: str; level: Optional[int]; section_path: str; section_title: Optional[str] kind: str # "heading" | "paragraph" | "list" | "code" | "table" | "thematic_break" | "blockquote"
text: str
level: Optional[int] # heading level (2,3,...) or None
section_path: str # e.g., "/H2 Title/H3 Subtitle"
@dataclass @dataclass
class Chunk: class Chunk:
id: str; note_id: str; index: int; text: str; window: str; token_count: int id: str
section_title: Optional[str]; section_path: str note_id: str
neighbors_prev: Optional[str]; neighbors_next: Optional[str] index: int
suggested_edges: Optional[List[str]] = None text: str
token_count: int
section_title: Optional[str]
section_path: str
neighbors_prev: Optional[str]
neighbors_next: Optional[str]
char_start: int
char_end: int
# ========================================== # --- Markdown zu RawBlocks: H2/H3 als Sections, andere Blöcke gruppiert ---
# 3. PARSING & STRATEGIES (SYNCHRON) def parse_blocks(md_text: str) -> List[RawBlock]:
# ========================================== md = MarkdownIt("commonmark").enable("table")
tokens: List[Token] = md.parse(md_text)
def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]: blocks: List[RawBlock] = []
"""Zerlegt Text in logische Blöcke (Absätze, Header).""" h2, h3 = None, None
blocks = []
h1_title = "Dokument"
section_path = "/" section_path = "/"
current_h2 = None cur_text = []
cur_kind = None
fm, text_without_fm = extract_frontmatter_from_text(md_text) def push(kind: str, txt: str, lvl: Optional[int]):
nonlocal section_path
txt = txt.strip()
if not txt:
return
title = None
if kind == "heading" and lvl:
title = txt
blocks.append(RawBlock(kind=kind, text=txt, level=lvl, section_path=section_path))
h1_match = re.search(r'^#\s+(.*)', text_without_fm, re.MULTILINE) i = 0
if h1_match: while i < len(tokens):
h1_title = h1_match.group(1).strip() t = tokens[i]
if t.type == "heading_open":
lvl = int(t.tag[1])
# Sammle heading inline
i += 1
title_txt = ""
while i < len(tokens) and tokens[i].type != "heading_close":
if tokens[i].type == "inline":
title_txt += tokens[i].content
i += 1
title_txt = title_txt.strip()
# Section-Pfad aktualisieren
if lvl == 2:
h2, h3 = title_txt, None
section_path = f"/{h2}"
elif lvl == 3:
h3 = title_txt
section_path = f"/{h2}/{h3}" if h2 else f"/{h3}"
push("heading", title_txt, lvl)
elif t.type in ("paragraph_open", "bullet_list_open", "ordered_list_open",
"fence", "code_block", "blockquote_open", "table_open", "hr"):
kind = {
"paragraph_open": "paragraph",
"bullet_list_open": "list",
"ordered_list_open": "list",
"fence": "code",
"code_block": "code",
"blockquote_open": "blockquote",
"table_open": "table",
"hr": "thematic_break",
}[t.type]
lines = text_without_fm.split('\n') if t.type in ("fence", "code_block"):
buffer = [] # Codeblock hat eigenen content im selben Token
content = t.content or ""
for line in lines: push(kind, content, None)
stripped = line.strip()
if stripped.startswith('# '):
continue
elif stripped.startswith('## '):
if buffer:
content = "\n".join(buffer).strip()
if content:
blocks.append(RawBlock("paragraph", content, None, section_path, current_h2))
buffer = []
current_h2 = stripped[3:].strip()
section_path = f"/{current_h2}"
blocks.append(RawBlock("heading", stripped, 2, section_path, current_h2))
elif not stripped:
if buffer:
content = "\n".join(buffer).strip()
if content:
blocks.append(RawBlock("paragraph", content, None, section_path, current_h2))
buffer = []
else: else:
buffer.append(line) # inline sammeln bis close
content = ""
i += 1
depth = 1
while i < len(tokens) and depth > 0:
tk = tokens[i]
if tk.type.endswith("_open"):
depth += 1
elif tk.type.endswith("_close"):
depth -= 1
elif tk.type == "inline":
content += tk.content
i += 1
push(kind, content, None)
continue # wir sind schon auf nächstem Token
i += 1
if buffer: return blocks
content = "\n".join(buffer).strip()
if content:
blocks.append(RawBlock("paragraph", content, None, section_path, current_h2))
return blocks, h1_title def assemble_chunks(note_id: str, md_text: str, note_type: str) -> List[Chunk]:
sizes = get_sizes(note_type)
target = sum(sizes["target"]) // 2 # mittlerer Zielwert
max_tokens = sizes["max"]
ov_min, ov_max = sizes["overlap"]
overlap = (ov_min + ov_max) // 2
def _strategy_sliding_window(blocks: List[RawBlock], config: Dict[str, Any], note_id: str, doc_title: str = "", context_prefix: str = "") -> List[Chunk]: blocks = parse_blocks(md_text)
target = config.get("target", 400)
max_tokens = config.get("max", 600)
overlap_val = config.get("overlap", (50, 80))
overlap = sum(overlap_val) // 2 if isinstance(overlap_val, tuple) else overlap_val
chunks = []; buf = []
def _create_chunk(txt, win, sec, path): chunks: List[Chunk] = []
idx = len(chunks) buf: List[Tuple[str, str, str]] = [] # (text, section_title, section_path)
chunks.append(Chunk( char_pos = 0
id=f"{note_id}#c{idx:02d}", note_id=note_id, index=idx,
text=txt, window=win, token_count=estimate_tokens(txt),
section_title=sec, section_path=path, neighbors_prev=None, neighbors_next=None,
suggested_edges=[]
))
def flush_buffer(): def flush_buffer(force=False):
nonlocal buf nonlocal buf, chunks, char_pos
if not buf: return if not buf:
return
text = "\n\n".join([b[0] for b in buf]).strip()
if not text:
buf = []
return
text_body = "\n\n".join([b.text for b in buf]) # Wenn zu groß, satzbasiert weich umbrechen
win_body = f"{context_prefix}\n{text_body}".strip() if context_prefix else text_body toks = estimate_tokens(text)
if toks > max_tokens:
if estimate_tokens(text_body) <= max_tokens: sentences = split_sentences(text)
_create_chunk(text_body, win_body, buf[-1].section_title, buf[-1].section_path) cur = []
cur_tokens = 0
for s in sentences:
st = estimate_tokens(s)
if cur_tokens + st > target and cur:
_emit("\n".join(cur))
# Overlap: letzte Sätze wiederverwenden
ov_text = " ".join(cur)[-overlap*4:] # 4 chars/token Heuristik
cur = [ov_text, s] if ov_text else [s]
cur_tokens = estimate_tokens(" ".join(cur))
else: else:
sentences = split_sentences(text_body) cur.append(s)
current_chunk_sents = [] cur_tokens += st
current_len = 0 if cur:
_emit("\n".join(cur))
for sent in sentences:
sent_len = estimate_tokens(sent)
if current_len + sent_len > target and current_chunk_sents:
c_txt = " ".join(current_chunk_sents)
c_win = f"{context_prefix}\n{c_txt}".strip() if context_prefix else c_txt
_create_chunk(c_txt, c_win, buf[-1].section_title, buf[-1].section_path)
overlap_sents = []
ov_len = 0
for s in reversed(current_chunk_sents):
if ov_len + estimate_tokens(s) < overlap:
overlap_sents.insert(0, s)
ov_len += estimate_tokens(s)
else: else:
break _emit(text)
current_chunk_sents = list(overlap_sents)
current_chunk_sents.append(sent)
current_len = ov_len + sent_len
else:
current_chunk_sents.append(sent)
current_len += sent_len
if current_chunk_sents:
c_txt = " ".join(current_chunk_sents)
c_win = f"{context_prefix}\n{c_txt}".strip() if context_prefix else c_txt
_create_chunk(c_txt, c_win, buf[-1].section_title, buf[-1].section_path)
buf = [] buf = []
def _emit(text_block: str):
nonlocal chunks, char_pos
idx = len(chunks)
chunk_id = f"{note_id}#c{idx:02d}"
token_count = estimate_tokens(text_block)
# section aus letztem buffer-entry ableiten
sec_title = buf[-1][1] if buf else None
sec_path = buf[-1][2] if buf else "/"
start = char_pos
end = start + len(text_block)
chunks.append(Chunk(
id=chunk_id,
note_id=note_id,
index=idx,
text=text_block,
token_count=token_count,
section_title=sec_title,
section_path=sec_path,
neighbors_prev=None,
neighbors_next=None,
char_start=start,
char_end=end
))
char_pos = end + 1
# Blocks in Puffer sammeln; bei Überschreiten Zielbereich flushen
cur_sec_title = None
for b in blocks: for b in blocks:
if b.kind == "heading": continue if b.kind == "heading" and b.level in (2, 3):
current_buf_text = "\n\n".join([x.text for x in buf]) # Sectionwechsel ⇒ Buffer flushen
if estimate_tokens(current_buf_text) + estimate_tokens(b.text) >= target:
flush_buffer() flush_buffer()
buf.append(b) cur_sec_title = b.text.strip()
if estimate_tokens(b.text) >= target: # Heading selbst nicht als Chunk, aber als Kontexttitel nutzen
continue
txt = b.text.strip()
if not txt:
continue
tentative = "\n\n".join([*(x[0] for x in buf), txt]).strip()
if estimate_tokens(tentative) > max(get_sizes(note_type)["target"]):
# weicher Schnitt vor Hinzufügen
flush_buffer()
buf.append((txt, cur_sec_title, b.section_path))
# bei Erreichen ~Target flushen
if estimate_tokens("\n\n".join([x[0] for x in buf])) >= target:
flush_buffer() flush_buffer()
flush_buffer() flush_buffer(force=True)
return chunks
def _strategy_by_heading(blocks: List[RawBlock], config: Dict[str, Any], note_id: str, doc_title: str = "") -> List[Chunk]:
return _strategy_sliding_window(blocks, config, note_id, doc_title, context_prefix=f"# {doc_title}")
# ==========================================
# 4. ORCHESTRATION (ASYNC) - WP-15 CORE
# ==========================================
async def assemble_chunks(note_id: str, md_text: str, note_type: str, config: Optional[Dict] = None) -> List[Chunk]:
if config is None:
config = get_chunk_config(note_type)
fm, body_text = extract_frontmatter_from_text(md_text)
note_status = fm.get("status", "").lower()
primary_strategy = config.get("strategy", "sliding_window")
enable_smart_edges = config.get("enable_smart_edge_allocation", False)
if enable_smart_edges and note_status in ["draft", "initial_gen"]:
logger.info(f"Chunker: Skipping Smart Edges for draft '{note_id}'.")
enable_smart_edges = False
blocks, doc_title = parse_blocks(md_text)
if primary_strategy == "by_heading":
chunks = await asyncio.to_thread(_strategy_by_heading, blocks, config, note_id, doc_title)
else:
chunks = await asyncio.to_thread(_strategy_sliding_window, blocks, config, note_id, doc_title)
if not chunks:
return []
if enable_smart_edges:
# Hier rufen wir nun die Smart Edge Allocation auf
chunks = await _run_smart_edge_allocation(chunks, md_text, note_id, note_type)
# neighbors setzen
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
ch.neighbors_next = chunks[i+1].id if i < len(chunks)-1 else None ch.neighbors_next = chunks[i+1].id if i < len(chunks)-1 else None
return chunks
def _extract_all_edges_from_md(md_text: str, note_id: str, note_type: str) -> List[str]:
"""
Hilfsfunktion: Erstellt einen Dummy-Chunk für den gesamten Text und ruft
den Edge-Parser auf, um ALLE Kanten der Notiz zu finden.
"""
# 1. Dummy Chunk erstellen, der den gesamten Text enthält
# Das ist notwendig, da build_edges_for_note Kanten nur aus Chunks extrahiert.
dummy_chunk = {
"chunk_id": f"{note_id}#full",
"text": md_text,
"content": md_text, # Sicherstellen, dass der Parser Text findet
"window": md_text,
"type": note_type
}
# 2. Aufruf des Parsers (Signatur-Fix!)
# derive_edges.py: build_edges_for_note(note_id, chunks, note_level_references=None, include_note_scope_refs=False)
raw_edges = build_edges_for_note(
note_id,
[dummy_chunk],
note_level_references=None,
include_note_scope_refs=False
)
# 3. Kanten extrahieren
all_candidates = set()
for e in raw_edges:
kind = e.get("kind")
target = e.get("target_id")
if target and kind not in ["belongs_to", "next", "prev", "backlink"]:
all_candidates.add(f"{kind}:{target}")
return list(all_candidates)
async def _run_smart_edge_allocation(chunks: List[Chunk], full_text: str, note_id: str, note_type: str) -> List[Chunk]:
analyzer = get_semantic_analyzer()
# A. Alle potenziellen Kanten der Notiz sammeln (über den Dummy-Chunk Trick)
candidate_list = _extract_all_edges_from_md(full_text, note_id, note_type)
if not candidate_list:
return chunks
# B. LLM Filterung pro Chunk (Parallel)
tasks = []
for chunk in chunks:
tasks.append(analyzer.assign_edges_to_chunk(chunk.text, candidate_list, note_type))
results_per_chunk = await asyncio.gather(*tasks)
# C. Injection & Fallback
assigned_edges_global = set()
for i, confirmed_edges in enumerate(results_per_chunk):
chunk = chunks[i]
chunk.suggested_edges = confirmed_edges
assigned_edges_global.update(confirmed_edges)
if confirmed_edges:
injection_str = "\n" + " ".join([f"[[rel:{e.split(':')[0]}|{e.split(':')[1]}]]" for e in confirmed_edges if ':' in e])
chunk.text += injection_str
chunk.window += injection_str
# D. Fallback: Unassigned Kanten überall hin
unassigned = set(candidate_list) - assigned_edges_global
if unassigned:
fallback_str = "\n" + " ".join([f"[[rel:{e.split(':')[0]}|{e.split(':')[1]}]]" for e in unassigned if ':' in e])
for chunk in chunks:
chunk.text += fallback_str
chunk.window += fallback_str
if chunk.suggested_edges is None: chunk.suggested_edges = []
chunk.suggested_edges.extend(list(unassigned))
return chunks return chunks

View File

@ -1,13 +1,14 @@
""" """
app/core/ingestion.py app/core/ingestion.py
Zentraler Service für die Transformation von Markdown-Dateien in Qdrant-Objekte. Zentraler Service für die Transformation von Markdown-Dateien in Qdrant-Objekte (Notes, Chunks, Edges).
Version: 2.5.2 (Full Feature: Change Detection + Robust IO + Clean Config) Dient als Shared Logic für:
1. CLI-Imports (scripts/import_markdown.py)
2. API-Uploads (WP-11)
Refactored for Async Embedding Support.
""" """
import os import os
import logging import logging
import asyncio
import time
from typing import Dict, List, Optional, Tuple, Any from typing import Dict, List, Optional, Tuple, Any
# Core Module Imports # Core Module Imports
@ -17,13 +18,21 @@ from app.core.parser import (
validate_required_frontmatter, validate_required_frontmatter,
) )
from app.core.note_payload import make_note_payload from app.core.note_payload import make_note_payload
from app.core.chunker import assemble_chunks, get_chunk_config from app.core.chunker import assemble_chunks
from app.core.chunk_payload import make_chunk_payloads from app.core.chunk_payload import make_chunk_payloads
# Fallback für Edges # Fallback für Edges Import (Robustheit)
try: try:
from app.core.derive_edges import build_edges_for_note from app.core.derive_edges import build_edges_for_note
except ImportError: except ImportError:
try:
from app.core.derive_edges import derive_edges_for_note as build_edges_for_note
except ImportError:
try:
from app.core.edges import build_edges_for_note
except ImportError:
# Fallback Mock
logging.warning("Could not import edge derivation logic. Edges will be empty.")
def build_edges_for_note(*args, **kwargs): return [] def build_edges_for_note(*args, **kwargs): return []
from app.core.qdrant import QdrantConfig, get_client, ensure_collections, ensure_payload_indexes from app.core.qdrant import QdrantConfig, get_client, ensure_collections, ensure_payload_indexes
@ -34,22 +43,30 @@ from app.core.qdrant_points import (
upsert_batch, upsert_batch,
) )
# WICHTIG: Wir nutzen den API-Client für Embeddings (Async Support)
from app.services.embeddings_client import EmbeddingsClient from app.services.embeddings_client import EmbeddingsClient
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# --- Helper --- # --- Helper für Type-Registry ---
def load_type_registry(custom_path: Optional[str] = None) -> dict: def load_type_registry(custom_path: Optional[str] = None) -> dict:
import yaml import yaml
path = custom_path or os.getenv("MINDNET_TYPES_FILE", "config/types.yaml") path = custom_path or os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
if not os.path.exists(path): return {} if not os.path.exists(path):
if os.path.exists("types.yaml"):
path = "types.yaml"
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:
return {}
def resolve_note_type(requested: Optional[str], reg: dict) -> str: def resolve_note_type(requested: Optional[str], reg: dict) -> str:
types = reg.get("types", {}) types = reg.get("types", {})
if requested and requested in types: return requested if requested and requested in types:
return requested
return "concept" return "concept"
def effective_chunk_profile(note_type: str, reg: dict) -> str: def effective_chunk_profile(note_type: str, reg: dict) -> str:
@ -67,6 +84,7 @@ def effective_retriever_weight(note_type: str, reg: dict) -> float:
class IngestionService: class IngestionService:
def __init__(self, collection_prefix: str = None): def __init__(self, collection_prefix: str = None):
# Prefix Logik vereinheitlichen
env_prefix = os.getenv("COLLECTION_PREFIX", "mindnet") env_prefix = os.getenv("COLLECTION_PREFIX", "mindnet")
self.prefix = collection_prefix or env_prefix self.prefix = collection_prefix or env_prefix
@ -74,14 +92,19 @@ class IngestionService:
self.cfg.prefix = self.prefix self.cfg.prefix = self.prefix
self.client = get_client(self.cfg) self.client = get_client(self.cfg)
self.dim = self.cfg.dim self.dim = self.cfg.dim
# Registry laden
self.registry = load_type_registry() self.registry = load_type_registry()
# Embedding Service initialisieren (Async Client)
self.embedder = EmbeddingsClient() self.embedder = EmbeddingsClient()
# Init DB Checks (Fehler abfangen, falls DB nicht erreichbar)
try: try:
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 init warning: {e}") logger.warning(f"DB initialization warning: {e}")
async def process_file( async def process_file(
self, self,
@ -96,8 +119,7 @@ class IngestionService:
hash_normalize: str = "canonical" hash_normalize: str = "canonical"
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Verarbeitet eine einzelne Datei (ASYNC). Verarbeitet eine einzelne Datei (ASYNC Version).
Inklusive Change Detection (Hash-Check) gegen Qdrant.
""" """
result = { result = {
"path": file_path, "path": file_path,
@ -106,7 +128,7 @@ class IngestionService:
"error": None "error": None
} }
# 1. Parse & Frontmatter Validation # 1. Parse & Frontmatter
try: try:
parsed = read_markdown(file_path) parsed = read_markdown(file_path)
if not parsed: if not parsed:
@ -147,7 +169,7 @@ class IngestionService:
logger.error(f"Payload build failed: {e}") logger.error(f"Payload build failed: {e}")
return {**result, "error": f"Payload build failed: {str(e)}"} return {**result, "error": f"Payload build failed: {str(e)}"}
# 4. Change Detection (Das fehlende Stück!) # 4. Change Detection
old_payload = None old_payload = None
if not force_replace: if not force_replace:
old_payload = self._fetch_note_payload(note_id) old_payload = self._fetch_note_payload(note_id)
@ -171,38 +193,47 @@ class IngestionService:
# 5. Processing (Chunking, Embedding, Edges) # 5. Processing (Chunking, Embedding, Edges)
try: try:
body_text = getattr(parsed, "body", "") or "" body_text = getattr(parsed, "body", "") or ""
chunks = assemble_chunks(fm["id"], body_text, fm["type"])
# --- Config Loading (Clean) ---
chunk_config = get_chunk_config(note_type)
# Hier greift die Logik aus types.yaml (smart=True/False)
chunks = await assemble_chunks(fm["id"], body_text, fm["type"], config=chunk_config)
chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text) chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text)
# Embedding # --- EMBEDDING FIX (ASYNC) ---
vecs = [] vecs = []
if chunk_pls: if chunk_pls:
texts = [c.get("window") or c.get("text") or "" for c in chunk_pls] texts = [c.get("window") or c.get("text") or "" for c in chunk_pls]
try: try:
# Async Aufruf des Embedders (via Batch oder Loop)
if hasattr(self.embedder, 'embed_documents'): if hasattr(self.embedder, 'embed_documents'):
vecs = await self.embedder.embed_documents(texts) vecs = await self.embedder.embed_documents(texts)
else: else:
# Fallback Loop falls Client kein Batch unterstützt
for t in texts: for t in texts:
v = await self.embedder.embed_query(t) v = await self.embedder.embed_query(t)
vecs.append(v) vecs.append(v)
# Validierung der Dimensionen
if vecs and len(vecs) > 0:
dim_got = len(vecs[0])
if dim_got != self.dim:
# Wirf keinen Fehler, aber logge Warnung. Qdrant Upsert wird failen wenn 0.
logger.warning(f"Vector dimension mismatch. Expected {self.dim}, got {dim_got}")
if dim_got == 0:
raise ValueError("Embedding returned empty vectors (Dim 0)")
except Exception as e: except Exception as e:
logger.error(f"Embedding failed: {e}") logger.error(f"Embedding generation failed: {e}")
raise RuntimeError(f"Embedding failed: {e}") raise RuntimeError(f"Embedding failed: {e}")
# Edges # Edges
note_refs = note_pl.get("references") or []
# Versuche flexible Signatur für Edges (V1 vs V2)
try: try:
edges = build_edges_for_note( edges = build_edges_for_note(
note_id, note_id,
chunk_pls, chunk_pls,
note_level_references=note_pl.get("references", []), note_level_references=note_refs,
include_note_scope_refs=note_scope_refs include_note_scope_refs=note_scope_refs
) )
except TypeError: except TypeError:
# Fallback für ältere Signatur
edges = build_edges_for_note(note_id, chunk_pls) edges = build_edges_for_note(note_id, chunk_pls)
except Exception as e: except Exception as e:
@ -237,7 +268,7 @@ class IngestionService:
logger.error(f"Upsert failed: {e}", exc_info=True) logger.error(f"Upsert failed: {e}", exc_info=True)
return {**result, "error": f"DB Upsert failed: {e}"} return {**result, "error": f"DB Upsert failed: {e}"}
# --- Qdrant Helper (Restored) --- # --- Interne Qdrant Helper ---
def _fetch_note_payload(self, note_id: str) -> Optional[dict]: def _fetch_note_payload(self, note_id: str) -> Optional[dict]:
from qdrant_client.http import models as rest from qdrant_client.http import models as rest
@ -266,7 +297,8 @@ class IngestionService:
for suffix in ["chunks", "edges"]: for suffix in ["chunks", "edges"]:
try: try:
self.client.delete(collection_name=f"{self.prefix}_{suffix}", points_selector=selector) self.client.delete(collection_name=f"{self.prefix}_{suffix}", points_selector=selector)
except Exception: pass except Exception:
pass
async def create_from_text( async def create_from_text(
self, self,
@ -277,25 +309,31 @@ class IngestionService:
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
WP-11 Persistence API Entrypoint. WP-11 Persistence API Entrypoint.
Schreibt Text in Vault und indiziert ihn sofort.
""" """
# 1. Zielordner
target_dir = os.path.join(vault_root, folder) target_dir = os.path.join(vault_root, folder)
os.makedirs(target_dir, exist_ok=True)
file_path = os.path.join(target_dir, filename)
try: try:
# Robust Write: Ensure Flush & Sync os.makedirs(target_dir, exist_ok=True)
except Exception as e:
return {"status": "error", "error": f"Could not create folder {target_dir}: {e}"}
# 2. Dateiname
safe_filename = os.path.basename(filename)
if not safe_filename.endswith(".md"):
safe_filename += ".md"
file_path = os.path.join(target_dir, safe_filename)
# 3. Schreiben
try:
with open(file_path, "w", encoding="utf-8") as f: with open(file_path, "w", encoding="utf-8") as f:
f.write(markdown_content) f.write(markdown_content)
f.flush()
os.fsync(f.fileno())
await asyncio.sleep(0.1)
logger.info(f"Written file to {file_path}") logger.info(f"Written file to {file_path}")
except Exception as e: except Exception as e:
return {"status": "error", "error": f"Disk write failed: {str(e)}"} return {"status": "error", "error": f"Disk write failed at {file_path}: {str(e)}"}
# 4. Indizieren (Async Aufruf!)
# Wir rufen process_file auf, das jetzt ASYNC ist
return await self.process_file( return await self.process_file(
file_path=file_path, file_path=file_path,
vault_root=vault_root, vault_root=vault_root,

View File

@ -5,7 +5,6 @@ import os
import json import json
import re import re
import yaml import yaml
import unicodedata
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
@ -24,7 +23,7 @@ timeout_setting = os.getenv("MINDNET_API_TIMEOUT") or os.getenv("MINDNET_LLM_TIM
API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0 API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0
# --- PAGE SETUP --- # --- PAGE SETUP ---
st.set_page_config(page_title="mindnet v2.5", page_icon="🧠", layout="wide") st.set_page_config(page_title="mindnet v2.3.10", page_icon="🧠", layout="wide")
# --- CSS STYLING --- # --- CSS STYLING ---
st.markdown(""" st.markdown("""
@ -54,6 +53,15 @@ st.markdown("""
background-color: white; background-color: white;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
} }
.suggestion-card {
border-left: 3px solid #1a73e8;
background-color: #ffffff;
padding: 10px;
margin-bottom: 8px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
</style> </style>
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
@ -63,18 +71,8 @@ if "user_id" not in st.session_state: st.session_state.user_id = str(uuid.uuid4(
# --- HELPER FUNCTIONS --- # --- HELPER FUNCTIONS ---
def slugify(value):
if not value: return ""
value = str(value).lower()
replacements = {'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 'ß': 'ss', '&': 'und', '+': 'und'}
for k, v in replacements.items():
value = value.replace(k, v)
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
value = re.sub(r'[^\w\s-]', '', value).strip()
return re.sub(r'[-\s]+', '-', value)
def normalize_meta_and_body(meta, body): def normalize_meta_and_body(meta, body):
"""Sanitizer: Stellt sicher, dass nur erlaubte Felder im Frontmatter bleiben."""
ALLOWED_KEYS = {"title", "type", "status", "tags", "id", "created", "updated", "aliases", "lang"} ALLOWED_KEYS = {"title", "type", "status", "tags", "id", "created", "updated", "aliases", "lang"}
clean_meta = {} clean_meta = {}
extra_content = [] extra_content = []
@ -101,11 +99,7 @@ def normalize_meta_and_body(meta, body):
extra_content.append(f"## {header}\n{val}\n") extra_content.append(f"## {header}\n{val}\n")
if all_tags: if all_tags:
clean_tags = [] clean_meta["tags"] = list(set(all_tags))
for t in all_tags:
t_clean = str(t).replace("#", "").strip()
if t_clean: clean_tags.append(t_clean)
clean_meta["tags"] = list(set(clean_tags))
if extra_content: if extra_content:
new_section = "\n".join(extra_content) new_section = "\n".join(extra_content)
@ -116,67 +110,37 @@ def normalize_meta_and_body(meta, body):
return clean_meta, final_body return clean_meta, final_body
def parse_markdown_draft(full_text): def parse_markdown_draft(full_text):
""" """Robustes Parsing + Sanitization."""
HEALING PARSER: Repariert kaputten LLM Output (z.B. fehlendes schließendes '---'). clean_text = full_text
"""
clean_text = full_text.strip()
# 1. Code-Block Wrapper entfernen pattern_block = r"```(?:markdown|md)?\s*(.*?)\s*```"
pattern_block = r"```(?:markdown|md|yaml)?\s*(.*?)\s*```" match_block = re.search(pattern_block, full_text, re.DOTALL | re.IGNORECASE)
match_block = re.search(pattern_block, clean_text, re.DOTALL | re.IGNORECASE)
if match_block: if match_block:
clean_text = match_block.group(1).strip() clean_text = match_block.group(1).strip()
parts = re.split(r"^---+\s*$", clean_text, maxsplit=2, flags=re.MULTILINE)
meta = {} meta = {}
body = clean_text body = clean_text
yaml_str = ""
# 2. Versuch A: Standard Split (Idealfall)
parts = re.split(r"^---+\s*$", clean_text, maxsplit=2, flags=re.MULTILINE)
if len(parts) >= 3: if len(parts) >= 3:
yaml_str = parts[1] yaml_str = parts[1]
body = parts[2] body_candidate = parts[2]
# 3. Versuch B: Healing (Wenn LLM das schließende --- vergessen hat)
elif clean_text.startswith("---"):
# Wir suchen die erste Überschrift '#', da Frontmatter davor sein muss
# Pattern: Suche --- am Anfang, dann nimm alles bis zum ersten # am Zeilenanfang
fallback_match = re.search(r"^---\s*(.*?)(?=\n#)", clean_text, re.DOTALL | re.MULTILINE)
if fallback_match:
yaml_str = fallback_match.group(1)
# Der Body ist alles NACH dem YAML String (inklusive dem #)
body = clean_text.replace(f"---{yaml_str}", "", 1).strip()
# 4. YAML Parsing
if yaml_str:
yaml_str_clean = yaml_str.replace("#", "") # Tags cleanen
try: try:
parsed = yaml.safe_load(yaml_str_clean) parsed = yaml.safe_load(yaml_str)
if isinstance(parsed, dict): if isinstance(parsed, dict):
meta = parsed meta = parsed
except Exception as e: body = body_candidate.strip()
print(f"YAML Parsing Warning: {e}") except Exception:
pass
# Fallback: Titel aus H1
if not meta.get("title"):
h1_match = re.search(r"^#\s+(.*)$", body, re.MULTILINE)
if h1_match:
meta["title"] = h1_match.group(1).strip()
# Correction: type/status swap
if meta.get("type") == "draft":
meta["status"] = "draft"
meta["type"] = "experience"
return normalize_meta_and_body(meta, body) return normalize_meta_and_body(meta, body)
def build_markdown_doc(meta, body): def build_markdown_doc(meta, body):
"""Baut das finale Dokument zusammen.""" """Baut das finale Dokument zusammen."""
if "id" not in meta or meta["id"] == "generated_on_save": if "id" not in meta or meta["id"] == "generated_on_save":
raw_title = meta.get('title', 'note') safe_title = re.sub(r'[^a-zA-Z0-9]', '-', meta.get('title', 'note')).lower()[:30]
clean_slug = slugify(raw_title)[:50] or "note" meta["id"] = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}-{uuid.uuid4().hex[:4]}"
meta["id"] = f"{datetime.now().strftime('%Y%m%d')}-{clean_slug}"
meta["updated"] = datetime.now().strftime("%Y-%m-%d") meta["updated"] = datetime.now().strftime("%Y-%m-%d")
@ -225,6 +189,7 @@ def send_chat_message(message: str, top_k: int, explain: bool):
return {"error": str(e)} return {"error": str(e)}
def analyze_draft_text(text: str, n_type: str): def analyze_draft_text(text: str, n_type: str):
"""Ruft den neuen Intelligence-Service (WP-11) auf."""
try: try:
response = requests.post( response = requests.post(
INGEST_ANALYZE_ENDPOINT, INGEST_ANALYZE_ENDPOINT,
@ -237,11 +202,12 @@ def analyze_draft_text(text: str, n_type: str):
return {"error": str(e)} return {"error": str(e)}
def save_draft_to_vault(markdown_content: str, filename: str = None): def save_draft_to_vault(markdown_content: str, filename: str = None):
"""Ruft den neuen Persistence-Service (WP-11) auf."""
try: try:
response = requests.post( response = requests.post(
INGEST_SAVE_ENDPOINT, INGEST_SAVE_ENDPOINT,
json={"markdown_content": markdown_content, "filename": filename}, json={"markdown_content": markdown_content, "filename": filename},
timeout=API_TIMEOUT timeout=60
) )
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
@ -259,7 +225,7 @@ def submit_feedback(query_id, node_id, score, comment=None):
def render_sidebar(): def render_sidebar():
with st.sidebar: with st.sidebar:
st.title("🧠 mindnet") st.title("🧠 mindnet")
st.caption("v2.5 | Healing Parser") st.caption("v2.3.10 | Mode Switch Fix")
mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0) mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0)
st.divider() st.divider()
st.subheader("⚙️ Settings") st.subheader("⚙️ Settings")
@ -274,6 +240,7 @@ def render_sidebar():
return mode, top_k, explain return mode, top_k, explain
def render_draft_editor(msg): def render_draft_editor(msg):
# Ensure ID Stability
if "query_id" not in msg or not msg["query_id"]: if "query_id" not in msg or not msg["query_id"]:
msg["query_id"] = str(uuid.uuid4()) msg["query_id"] = str(uuid.uuid4())
@ -286,7 +253,7 @@ def render_draft_editor(msg):
widget_body_key = f"{key_base}_widget_body" widget_body_key = f"{key_base}_widget_body"
data_body_key = f"{key_base}_data_body" data_body_key = f"{key_base}_data_body"
# --- 1. INIT STATE --- # --- 1. INIT STATE (Nur beim allerersten Laden der Message) ---
if f"{key_base}_init" not in st.session_state: if f"{key_base}_init" not in st.session_state:
meta, body = parse_markdown_draft(msg["content"]) meta, body = parse_markdown_draft(msg["content"])
if "type" not in meta: meta["type"] = "default" if "type" not in meta: meta["type"] = "default"
@ -294,34 +261,26 @@ def render_draft_editor(msg):
tags = meta.get("tags", []) tags = meta.get("tags", [])
meta["tags_str"] = ", ".join(tags) if isinstance(tags, list) else str(tags) meta["tags_str"] = ", ".join(tags) if isinstance(tags, list) else str(tags)
# Persistent Data # Persistent Data (Source of Truth)
st.session_state[data_meta_key] = meta st.session_state[data_meta_key] = meta
st.session_state[data_sugg_key] = [] st.session_state[data_sugg_key] = []
st.session_state[data_body_key] = body.strip() st.session_state[data_body_key] = body.strip()
# Init Widgets Keys
st.session_state[f"{key_base}_wdg_title"] = meta["title"]
st.session_state[f"{key_base}_wdg_type"] = meta["type"]
st.session_state[f"{key_base}_wdg_tags"] = meta["tags_str"]
st.session_state[f"{key_base}_init"] = True st.session_state[f"{key_base}_init"] = True
# --- 2. RESURRECTION --- # --- 2. RESURRECTION FIX (WICHTIG!) ---
# Wenn wir vom Manuellen Editor zurückkommen, wurde der widget_key von Streamlit gelöscht.
# Wir müssen ihn aus dem persistenten data_body_key wiederherstellen.
if widget_body_key not in st.session_state and data_body_key in st.session_state: if widget_body_key not in st.session_state and data_body_key in st.session_state:
st.session_state[widget_body_key] = st.session_state[data_body_key] st.session_state[widget_body_key] = st.session_state[data_body_key]
# --- CALLBACKS --- # --- CALLBACKS ---
def _sync_meta():
meta = st.session_state[data_meta_key]
meta["title"] = st.session_state.get(f"{key_base}_wdg_title", "")
meta["type"] = st.session_state.get(f"{key_base}_wdg_type", "default")
meta["tags_str"] = st.session_state.get(f"{key_base}_wdg_tags", "")
st.session_state[data_meta_key] = meta
def _sync_body(): def _sync_body():
# Sync Widget -> Data (Source of Truth)
st.session_state[data_body_key] = st.session_state[widget_body_key] st.session_state[data_body_key] = st.session_state[widget_body_key]
def _insert_text(text_to_insert): def _insert_text(text_to_insert):
# Insert in Widget Key und Sync Data
current = st.session_state.get(widget_body_key, "") current = st.session_state.get(widget_body_key, "")
new_text = f"{current}\n\n{text_to_insert}" new_text = f"{current}\n\n{text_to_insert}"
st.session_state[widget_body_key] = new_text st.session_state[widget_body_key] = new_text
@ -337,23 +296,33 @@ def render_draft_editor(msg):
st.markdown(f'<div class="draft-box">', unsafe_allow_html=True) st.markdown(f'<div class="draft-box">', unsafe_allow_html=True)
st.markdown("### 📝 Entwurf bearbeiten") st.markdown("### 📝 Entwurf bearbeiten")
# Metadata Form
meta_ref = st.session_state[data_meta_key] meta_ref = st.session_state[data_meta_key]
c1, c2 = st.columns([2, 1]) c1, c2 = st.columns([2, 1])
with c1: with c1:
st.text_input("Titel", key=f"{key_base}_wdg_title", on_change=_sync_meta) # Auch hier Keys für Widgets nutzen, um Resets zu vermeiden
title_key = f"{key_base}_wdg_title"
if title_key not in st.session_state: st.session_state[title_key] = meta_ref["title"]
meta_ref["title"] = st.text_input("Titel", key=title_key)
with c2: with c2:
known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle", "risk", "belief"] known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle"]
curr_type = st.session_state.get(f"{key_base}_wdg_type", meta_ref["type"]) curr = meta_ref["type"]
if curr_type not in known_types: known_types.append(curr_type) if curr not in known_types: known_types.append(curr)
st.selectbox("Typ", known_types, key=f"{key_base}_wdg_type", on_change=_sync_meta) type_key = f"{key_base}_wdg_type"
if type_key not in st.session_state: st.session_state[type_key] = meta_ref["type"]
meta_ref["type"] = st.selectbox("Typ", known_types, index=known_types.index(curr) if curr in known_types else 0, key=type_key)
st.text_input("Tags", key=f"{key_base}_wdg_tags", on_change=_sync_meta) tags_key = f"{key_base}_wdg_tags"
if tags_key not in st.session_state: st.session_state[tags_key] = meta_ref.get("tags_str", "")
meta_ref["tags_str"] = st.text_input("Tags", key=tags_key)
# Tabs
tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"]) tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"])
# --- TAB 1: EDITOR --- # --- TAB 1: EDITOR ---
with tab_edit: with tab_edit:
# Hier kein 'value' Argument mehr, da wir den Key oben (Resurrection) initialisiert haben.
st.text_area( st.text_area(
"Body", "Body",
key=widget_body_key, key=widget_body_key,
@ -369,11 +338,11 @@ def render_draft_editor(msg):
if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"): if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"):
st.session_state[data_sugg_key] = [] st.session_state[data_sugg_key] = []
# Lese vom Widget (aktuell) oder Data (Fallback)
text_to_analyze = st.session_state.get(widget_body_key, st.session_state.get(data_body_key, "")) text_to_analyze = st.session_state.get(widget_body_key, st.session_state.get(data_body_key, ""))
current_doc_type = st.session_state.get(f"{key_base}_wdg_type", "concept")
with st.spinner("Analysiere..."): with st.spinner("Analysiere..."):
analysis = analyze_draft_text(text_to_analyze, current_doc_type) analysis = analyze_draft_text(text_to_analyze, meta_ref["type"])
if "error" in analysis: if "error" in analysis:
st.error(f"Fehler: {analysis['error']}") st.error(f"Fehler: {analysis['error']}")
@ -385,6 +354,7 @@ def render_draft_editor(msg):
else: else:
st.success(f"{len(suggestions)} Vorschläge gefunden.") st.success(f"{len(suggestions)} Vorschläge gefunden.")
# Render List
suggestions = st.session_state[data_sugg_key] suggestions = st.session_state[data_sugg_key]
if suggestions: if suggestions:
current_text_state = st.session_state.get(widget_body_key, "") current_text_state = st.session_state.get(widget_body_key, "")
@ -410,24 +380,16 @@ def render_draft_editor(msg):
st.button(" Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,)) st.button(" Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,))
# --- TAB 3: SAVE --- # --- TAB 3: SAVE ---
final_tags_str = st.session_state.get(f"{key_base}_wdg_tags", "") final_tags = [t.strip() for t in meta_ref["tags_str"].split(",") if t.strip()]
final_tags = [t.strip() for t in final_tags_str.split(",") if t.strip()]
final_meta = { final_meta = {
"id": "generated_on_save", "id": "generated_on_save",
"type": st.session_state.get(f"{key_base}_wdg_type", "default"), "type": meta_ref["type"],
"title": st.session_state.get(f"{key_base}_wdg_title", "").strip(), "title": meta_ref["title"],
"status": "draft", "status": "draft",
"tags": final_tags "tags": final_tags
} }
# Final Doc aus Data
final_body = st.session_state.get(widget_body_key, st.session_state[data_body_key]) final_body = st.session_state.get(widget_body_key, st.session_state[data_body_key])
if not final_meta["title"]:
h1_match = re.search(r"^#\s+(.*)$", final_body, re.MULTILINE)
if h1_match:
final_meta["title"] = h1_match.group(1).strip()
final_doc = build_markdown_doc(final_meta, final_body) final_doc = build_markdown_doc(final_meta, final_body)
with tab_view: with tab_view:
@ -441,13 +403,7 @@ def render_draft_editor(msg):
with b1: with b1:
if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"): if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"):
with st.spinner("Speichere im Vault..."): with st.spinner("Speichere im Vault..."):
safe_title = re.sub(r'[^a-zA-Z0-9]', '-', meta_ref["title"]).lower()[:30] or "draft"
raw_title = final_meta.get("title", "")
if not raw_title:
clean_body = re.sub(r"[#*_\[\]()]", "", final_body).strip()
raw_title = clean_body[:40] if clean_body else "draft"
safe_title = slugify(raw_title)[:60] or "draft"
fname = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md" fname = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md"
result = save_draft_to_vault(final_doc, filename=fname) result = save_draft_to_vault(final_doc, filename=fname)
@ -467,6 +423,7 @@ def render_chat_interface(top_k, explain):
for idx, msg in enumerate(st.session_state.messages): for idx, msg in enumerate(st.session_state.messages):
with st.chat_message(msg["role"]): with st.chat_message(msg["role"]):
if msg["role"] == "assistant": if msg["role"] == "assistant":
# Header
intent = msg.get("intent", "UNKNOWN") intent = msg.get("intent", "UNKNOWN")
src = msg.get("intent_source", "?") src = msg.get("intent_source", "?")
icon = {"EMPATHY":"❤️", "DECISION":"⚖️", "CODING":"💻", "FACT":"📚", "INTERVIEW":"📝"}.get(intent, "🧠") icon = {"EMPATHY":"❤️", "DECISION":"⚖️", "CODING":"💻", "FACT":"📚", "INTERVIEW":"📝"}.get(intent, "🧠")
@ -475,11 +432,13 @@ def render_chat_interface(top_k, explain):
with st.expander("🐞 Debug Raw Payload", expanded=False): with st.expander("🐞 Debug Raw Payload", expanded=False):
st.json(msg) st.json(msg)
# Logic
if intent == "INTERVIEW": if intent == "INTERVIEW":
render_draft_editor(msg) render_draft_editor(msg)
else: else:
st.markdown(msg["content"]) st.markdown(msg["content"])
# Sources
if "sources" in msg and msg["sources"]: if "sources" in msg and msg["sources"]:
for hit in msg["sources"]: for hit in msg["sources"]:
with st.expander(f"📄 {hit.get('note_id', '?')} ({hit.get('total_score', 0):.2f})"): with st.expander(f"📄 {hit.get('note_id', '?')} ({hit.get('total_score', 0):.2f})"):

View File

@ -1,15 +1,21 @@
""" """
app/routers/chat.py RAG Endpunkt app/routers/chat.py RAG Endpunkt (WP-06 Hybrid Router + WP-07 Interview Mode)
Version: 2.5.0 (Fix: Question Detection protects against False-Positive Interviews) Version: 2.4.0 (Interview Support)
Features:
- Hybrid Intent Router (Keyword + LLM)
- Strategic Retrieval (Late Binding via Config)
- Interview Loop (Schema-driven Data Collection)
- Context Enrichment (Payload/Source Fallback)
- Data Flywheel (Feedback Logging Integration)
""" """
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
import time import time
import uuid import uuid
import logging import logging
import yaml import yaml
import os
from pathlib import Path from pathlib import Path
from app.config import get_settings from app.config import get_settings
@ -24,7 +30,6 @@ logger = logging.getLogger(__name__)
# --- Helper: Config Loader --- # --- Helper: Config Loader ---
_DECISION_CONFIG_CACHE = None _DECISION_CONFIG_CACHE = None
_TYPES_CONFIG_CACHE = None
def _load_decision_config() -> Dict[str, Any]: def _load_decision_config() -> Dict[str, Any]:
settings = get_settings() settings = get_settings()
@ -46,27 +51,12 @@ def _load_decision_config() -> Dict[str, Any]:
logger.error(f"Failed to load decision config: {e}") logger.error(f"Failed to load decision config: {e}")
return default_config return default_config
def _load_types_config() -> Dict[str, Any]:
"""Lädt die types.yaml für Keyword-Erkennung."""
path = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
try:
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
except Exception:
return {}
def get_full_config() -> Dict[str, Any]: def get_full_config() -> Dict[str, Any]:
global _DECISION_CONFIG_CACHE global _DECISION_CONFIG_CACHE
if _DECISION_CONFIG_CACHE is None: if _DECISION_CONFIG_CACHE is None:
_DECISION_CONFIG_CACHE = _load_decision_config() _DECISION_CONFIG_CACHE = _load_decision_config()
return _DECISION_CONFIG_CACHE return _DECISION_CONFIG_CACHE
def get_types_config() -> Dict[str, Any]:
global _TYPES_CONFIG_CACHE
if _TYPES_CONFIG_CACHE is None:
_TYPES_CONFIG_CACHE = _load_types_config()
return _TYPES_CONFIG_CACHE
def get_decision_strategy(intent: str) -> Dict[str, Any]: def get_decision_strategy(intent: str) -> Dict[str, Any]:
config = get_full_config() config = get_full_config()
strategies = config.get("strategies", {}) strategies = config.get("strategies", {})
@ -77,39 +67,39 @@ def get_decision_strategy(intent: str) -> Dict[str, Any]:
def _detect_target_type(message: str, configured_schemas: Dict[str, Any]) -> str: def _detect_target_type(message: str, configured_schemas: Dict[str, Any]) -> str:
""" """
Versucht zu erraten, welchen Notiz-Typ der User erstellen will. Versucht zu erraten, welchen Notiz-Typ der User erstellen will.
Nutzt Keywords aus types.yaml UND Mappings. Nutzt Keywords und Mappings.
""" """
message_lower = message.lower() message_lower = message.lower()
# 1. Check types.yaml detection_keywords (Priority!) # 1. Direkter Match mit Schema-Keys (z.B. "projekt", "entscheidung")
types_cfg = get_types_config() # Ignoriere 'default' hier
types_def = types_cfg.get("types", {})
for type_name, type_data in types_def.items():
keywords = type_data.get("detection_keywords", [])
for kw in keywords:
if kw.lower() in message_lower:
return type_name
# 2. Direkter Match mit Schema-Keys
for type_key in configured_schemas.keys(): for type_key in configured_schemas.keys():
if type_key == "default": continue if type_key == "default":
continue
if type_key in message_lower: if type_key in message_lower:
return type_key return type_key
# 3. Synonym-Mapping (Legacy Fallback) # 2. Synonym-Mapping (Deutsch -> Schema Key)
# Dies verbessert die UX, falls User deutsche Begriffe nutzen
synonyms = { synonyms = {
"projekt": "project", "vorhaben": "project", "projekt": "project",
"entscheidung": "decision", "beschluss": "decision", "vorhaben": "project",
"entscheidung": "decision",
"beschluss": "decision",
"ziel": "goal", "ziel": "goal",
"erfahrung": "experience", "lektion": "experience", "erfahrung": "experience",
"lektion": "experience",
"wert": "value", "wert": "value",
"prinzip": "principle", "prinzip": "principle",
"notiz": "default", "idee": "default" "grundsatz": "principle",
"notiz": "default",
"idee": "default"
} }
for term, schema_key in synonyms.items(): for term, schema_key in synonyms.items():
if term in message_lower: if term in message_lower:
# Prüfen, ob der gemappte Key auch konfiguriert ist
if schema_key in configured_schemas:
return schema_key return schema_key
return "default" return "default"
@ -136,6 +126,7 @@ def _build_enriched_context(hits: List[QueryHit]) -> str:
) )
title = hit.note_id or "Unbekannt" title = hit.note_id or "Unbekannt"
# [FIX] Robustes Auslesen des Typs (Payload > Source > Unknown)
payload = hit.payload or {} payload = hit.payload or {}
note_type = payload.get("type") or source.get("type", "unknown") note_type = payload.get("type") or source.get("type", "unknown")
note_type = str(note_type).upper() note_type = str(note_type).upper()
@ -149,77 +140,54 @@ def _build_enriched_context(hits: List[QueryHit]) -> str:
return "\n\n".join(context_parts) return "\n\n".join(context_parts)
def _is_question(query: str) -> bool:
"""Prüft, ob der Input wahrscheinlich eine Frage ist."""
q = query.strip().lower()
if "?" in q: return True
# W-Fragen Indikatoren (falls User das ? vergisst)
starters = ["wer", "wie", "was", "wo", "wann", "warum", "weshalb", "wozu", "welche", "bist du", "entspricht"]
if any(q.startswith(s + " ") for s in starters):
return True
return False
async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]: async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]:
""" """
Hybrid Router v5: Hybrid Router v3:
1. Decision Keywords (Strategie) -> Prio 1 Gibt Tuple zurück: (Intent, Source)
2. Type Keywords (Interview Trigger) -> Prio 2, ABER NUR WENN KEINE FRAGE!
3. LLM (Fallback) -> Prio 3
""" """
config = get_full_config() config = get_full_config()
strategies = config.get("strategies", {}) strategies = config.get("strategies", {})
settings = config.get("settings", {}) settings = config.get("settings", {})
query_lower = query.lower() query_lower = query.lower()
best_intent = None
max_match_length = 0
# 1. FAST PATH A: Strategie Keywords (z.B. "Soll ich...") # 1. FAST PATH: Keywords
for intent_name, strategy in strategies.items(): for intent_name, strategy in strategies.items():
if intent_name == "FACT": continue if intent_name == "FACT": continue
keywords = strategy.get("trigger_keywords", []) keywords = strategy.get("trigger_keywords", [])
for k in keywords: for k in keywords:
if k.lower() in query_lower: if k.lower() in query_lower:
return intent_name, "Keyword (Strategy)" if len(k) > max_match_length:
max_match_length = len(k)
best_intent = intent_name
# 2. FAST PATH B: Type Keywords (z.B. "Projekt", "Werte") -> INTERVIEW if best_intent:
# FIX: Wir prüfen, ob es eine Frage ist. Fragen zu Typen sollen RAG (FACT/DECISION) sein, return best_intent, "Keyword (Fast Path)"
# keine Interviews. Wir überlassen das dann dem LLM Router (Slow Path).
if not _is_question(query_lower): # 2. SLOW PATH: LLM Router
types_cfg = get_types_config()
types_def = types_cfg.get("types", {})
for type_name, type_data in types_def.items():
keywords = type_data.get("detection_keywords", [])
for kw in keywords:
if kw.lower() in query_lower:
return "INTERVIEW", f"Keyword (Type: {type_name})"
# 3. SLOW PATH: LLM Router
if settings.get("llm_fallback_enabled", False): if settings.get("llm_fallback_enabled", False):
# Nutze Prompts aus prompts.yaml (via LLM Service) router_prompt_template = settings.get("llm_router_prompt", "")
router_prompt_template = llm.prompts.get("router_prompt", "")
if router_prompt_template: if router_prompt_template:
prompt = router_prompt_template.replace("{query}", query) prompt = router_prompt_template.replace("{query}", query)
logger.info("Keywords failed (or Question detected). Asking LLM for Intent...") logger.info("Keywords failed. Asking LLM for Intent...")
try: raw_response = await llm.generate_raw_response(prompt)
# Nutze priority="realtime" für den Router, damit er nicht wartet
raw_response = await llm.generate_raw_response(prompt, priority="realtime") # Parsing logic
llm_output_upper = raw_response.upper() llm_output_upper = raw_response.upper()
found_intents = []
# Zuerst INTERVIEW prüfen
if "INTERVIEW" in llm_output_upper or "CREATE" in llm_output_upper:
return "INTERVIEW", "LLM Router"
for strat_key in strategies.keys(): for strat_key in strategies.keys():
if strat_key in llm_output_upper: if strat_key in llm_output_upper:
return strat_key, "LLM Router" found_intents.append(strat_key)
except Exception as e: if len(found_intents) == 1:
logger.error(f"Router LLM failed: {e}") return found_intents[0], "LLM Router (Slow Path)"
elif len(found_intents) > 1:
return found_intents[0], f"LLM Ambiguous {found_intents}"
else:
return "FACT", "LLM Fallback (No Match)"
return "FACT", "Default (No Match)" return "FACT", "Default (No Match)"
@ -234,7 +202,7 @@ async def chat_endpoint(
logger.info(f"Chat request [{query_id}]: {request.message[:50]}...") logger.info(f"Chat request [{query_id}]: {request.message[:50]}...")
try: try:
# 1. Intent Detection # 1. Intent Detection (mit Source)
intent, intent_source = await _classify_intent(request.message, llm) intent, intent_source = await _classify_intent(request.message, llm)
logger.info(f"[{query_id}] Final Intent: {intent} via {intent_source}") logger.info(f"[{query_id}] Final Intent: {intent} via {intent_source}")
@ -242,41 +210,57 @@ async def chat_endpoint(
strategy = get_decision_strategy(intent) strategy = get_decision_strategy(intent)
prompt_key = strategy.get("prompt_template", "rag_template") prompt_key = strategy.get("prompt_template", "rag_template")
# --- SPLIT LOGIC: INTERVIEW vs. RAG ---
sources_hits = [] sources_hits = []
final_prompt = "" final_prompt = ""
if intent == "INTERVIEW": if intent == "INTERVIEW":
# --- INTERVIEW MODE --- # --- WP-07: INTERVIEW MODE ---
target_type = _detect_target_type(request.message, strategy.get("schemas", {})) # Kein Retrieval. Wir nutzen den Dialog-Kontext.
types_cfg = get_types_config() # 1. Schema Loading (Late Binding)
type_def = types_cfg.get("types", {}).get(target_type, {}) schemas = strategy.get("schemas", {})
fields_list = type_def.get("schema", []) target_type = _detect_target_type(request.message, schemas)
active_schema = schemas.get(target_type, schemas.get("default"))
if not fields_list: logger.info(f"[{query_id}] Starting Interview for Type: {target_type}")
configured_schemas = strategy.get("schemas", {})
fallback_schema = configured_schemas.get(target_type, configured_schemas.get("default")) # Robustes Schema-Parsing (Dict vs List)
if isinstance(fallback_schema, dict): if isinstance(active_schema, dict):
fields_list = fallback_schema.get("fields", []) fields_list = active_schema.get("fields", [])
hint_str = active_schema.get("hint", "")
else: else:
fields_list = fallback_schema or [] fields_list = active_schema # Fallback falls nur Liste definiert
hint_str = ""
logger.info(f"[{query_id}] Interview Type: {target_type}. Fields: {len(fields_list)}")
fields_str = "\n- " + "\n- ".join(fields_list) fields_str = "\n- " + "\n- ".join(fields_list)
# 2. Context Logic
# Hinweis: In einer Stateless-API ist {context_str} idealerweise die History.
# Da ChatRequest (noch) kein History-Feld hat, nutzen wir einen Placeholder
# oder verlassen uns darauf, dass der Client die History im Prompt mitschickt
# (Streamlit Pattern: Appends history to prompt).
# Wir labeln es hier explizit.
context_str = "Bisheriger Verlauf (falls vorhanden): Siehe oben/unten."
# 3. Prompt Assembly
template = llm.prompts.get(prompt_key, "") template = llm.prompts.get(prompt_key, "")
final_prompt = template.replace("{context_str}", "Dialogverlauf...") \ final_prompt = template.replace("{context_str}", context_str) \
.replace("{query}", request.message) \ .replace("{query}", request.message) \
.replace("{target_type}", target_type) \ .replace("{target_type}", target_type) \
.replace("{schema_fields}", fields_str) \ .replace("{schema_fields}", fields_str) \
.replace("{schema_hint}", "") .replace("{schema_hint}", hint_str)
# Keine Hits im Interview
sources_hits = [] sources_hits = []
else: else:
# --- RAG MODE --- # --- WP-06: STANDARD RAG MODE ---
inject_types = strategy.get("inject_types", []) inject_types = strategy.get("inject_types", [])
prepend_instr = strategy.get("prepend_instruction", "") prepend_instr = strategy.get("prepend_instruction", "")
# 2. Primary Retrieval
query_req = QueryRequest( query_req = QueryRequest(
query=request.message, query=request.message,
mode="hybrid", mode="hybrid",
@ -286,7 +270,9 @@ async def chat_endpoint(
retrieve_result = await retriever.search(query_req) retrieve_result = await retriever.search(query_req)
hits = retrieve_result.results hits = retrieve_result.results
# 3. Strategic Retrieval (WP-06 Kernfeature)
if inject_types: if inject_types:
logger.info(f"[{query_id}] Executing Strategic Retrieval for types: {inject_types}...")
strategy_req = QueryRequest( strategy_req = QueryRequest(
query=request.message, query=request.message,
mode="hybrid", mode="hybrid",
@ -295,16 +281,19 @@ async def chat_endpoint(
explain=False explain=False
) )
strategy_result = await retriever.search(strategy_req) strategy_result = await retriever.search(strategy_req)
existing_ids = {h.node_id for h in hits} existing_ids = {h.node_id for h in hits}
for strat_hit in strategy_result.results: for strat_hit in strategy_result.results:
if strat_hit.node_id not in existing_ids: if strat_hit.node_id not in existing_ids:
hits.append(strat_hit) hits.append(strat_hit)
# 4. Context Building
if not hits: if not hits:
context_str = "Keine relevanten Notizen gefunden." context_str = "Keine relevanten Notizen gefunden."
else: else:
context_str = _build_enriched_context(hits) context_str = _build_enriched_context(hits)
# 5. Generation Setup
template = llm.prompts.get(prompt_key, "{context_str}\n\n{query}") template = llm.prompts.get(prompt_key, "{context_str}\n\n{query}")
if prepend_instr: if prepend_instr:
@ -313,29 +302,35 @@ async def chat_endpoint(
final_prompt = template.replace("{context_str}", context_str).replace("{query}", request.message) final_prompt = template.replace("{context_str}", context_str).replace("{query}", request.message)
sources_hits = hits sources_hits = hits
# --- GENERATION --- # --- COMMON GENERATION ---
system_prompt = llm.prompts.get("system_prompt", "") system_prompt = llm.prompts.get("system_prompt", "")
# Chat nutzt IMMER realtime priority logger.info(f"[{query_id}] Sending to LLM (Intent: {intent}, Template: {prompt_key})...")
answer_text = await llm.generate_raw_response(
prompt=final_prompt, # System-Prompt separat übergeben
system=system_prompt, answer_text = await llm.generate_raw_response(prompt=final_prompt, system=system_prompt)
priority="realtime"
)
duration_ms = int((time.time() - start_time) * 1000) duration_ms = int((time.time() - start_time) * 1000)
# Logging # 6. Logging (Fire & Forget)
try: try:
log_search( log_search(
query_id=query_id, query_id=query_id,
query_text=request.message, query_text=request.message,
results=sources_hits, results=sources_hits,
mode="interview" if intent == "INTERVIEW" else "chat_rag", mode="interview" if intent == "INTERVIEW" else "chat_rag",
metadata={"intent": intent, "source": intent_source} metadata={
"intent": intent,
"intent_source": intent_source,
"generated_answer": answer_text,
"model": llm.settings.LLM_MODEL
}
) )
except: pass except Exception as e:
logger.error(f"Logging failed: {e}")
# 7. Response
return ChatResponse( return ChatResponse(
query_id=query_id, query_id=query_id,
answer=answer_text, answer=answer_text,

View File

@ -1,142 +1,80 @@
""" """
app/services/llm_service.py LLM Client app/services/llm_service.py LLM Client (Ollama)
Version: 2.8.0 (Configurable Concurrency Limit) Version: 0.2.1 (Fix: System Prompt Handling for Phi-3)
""" """
import httpx import httpx
import yaml import yaml
import logging import logging
import os import os
import asyncio
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any, Literal from app.config import get_settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Settings:
OLLAMA_URL = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434")
LLM_TIMEOUT = float(os.getenv("MINDNET_LLM_TIMEOUT", 300.0))
LLM_MODEL = os.getenv("MINDNET_LLM_MODEL", "phi3:mini")
PROMPTS_PATH = os.getenv("MINDNET_PROMPTS_PATH", "./config/prompts.yaml")
# NEU: Konfigurierbares Limit für Hintergrund-Last
# Default auf 2 (konservativ), kann in .env erhöht werden.
BACKGROUND_LIMIT = int(os.getenv("MINDNET_LLM_BACKGROUND_LIMIT", "2"))
def get_settings():
return Settings()
class LLMService: class LLMService:
# GLOBALER SEMAPHOR (Lazy Initialization)
# Wir initialisieren ihn erst, wenn wir die Settings kennen.
_background_semaphore = None
def __init__(self): def __init__(self):
self.settings = get_settings() self.settings = get_settings()
self.prompts = self._load_prompts() self.prompts = self._load_prompts()
# Initialisiere Semaphore einmalig auf Klassen-Ebene basierend auf Config
if LLMService._background_semaphore is None:
limit = self.settings.BACKGROUND_LIMIT
logger.info(f"🚦 LLMService: Initializing Background Semaphore with limit: {limit}")
LLMService._background_semaphore = asyncio.Semaphore(limit)
self.timeout = httpx.Timeout(self.settings.LLM_TIMEOUT, connect=10.0)
self.client = httpx.AsyncClient( self.client = httpx.AsyncClient(
base_url=self.settings.OLLAMA_URL, base_url=self.settings.OLLAMA_URL,
timeout=self.timeout timeout=self.settings.LLM_TIMEOUT
) )
def _load_prompts(self) -> dict: def _load_prompts(self) -> dict:
path = Path(self.settings.PROMPTS_PATH) path = Path(self.settings.PROMPTS_PATH)
if not path.exists(): return {} if not path.exists():
return {}
try: try:
with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
except Exception as e: except Exception as e:
logger.error(f"Failed to load prompts: {e}") logger.error(f"Failed to load prompts: {e}")
return {} return {}
async def generate_raw_response( async def generate_raw_response(self, prompt: str, system: str = None) -> str:
self,
prompt: str,
system: str = None,
force_json: bool = False,
max_retries: int = 0,
base_delay: float = 2.0,
priority: Literal["realtime", "background"] = "realtime"
) -> str:
""" """
Führt einen LLM Call aus. Führt einen LLM Call aus.
priority="realtime": Chat (Sofort, keine Bremse). Unterstützt nun explizite System-Prompts für sauberes Templating.
priority="background": Import/Analyse (Gedrosselt durch Semaphore).
""" """
payload = {
use_semaphore = (priority == "background")
if use_semaphore and LLMService._background_semaphore:
async with LLMService._background_semaphore:
return await self._execute_request(prompt, system, force_json, max_retries, base_delay)
else:
# Realtime oder Fallback (falls Semaphore Init fehlschlug)
return await self._execute_request(prompt, system, force_json, max_retries, base_delay)
async def _execute_request(self, prompt, system, force_json, max_retries, base_delay):
payload: Dict[str, Any] = {
"model": self.settings.LLM_MODEL, "model": self.settings.LLM_MODEL,
"prompt": prompt, "prompt": prompt,
"stream": False, "stream": False,
"options": { "options": {
"temperature": 0.1 if force_json else 0.7, # Temperature etwas höher für Empathie, niedriger für Code?
"num_ctx": 8192 # Wir lassen es auf Standard, oder steuern es später via Config.
"temperature": 0.7,
"num_ctx": 2048
} }
} }
if force_json: # WICHTIG: System-Prompt separat übergeben, damit Ollama formatiert
payload["format"] = "json"
if system: if system:
payload["system"] = system payload["system"] = system
attempt = 0
while True:
try: try:
response = await self.client.post("/api/generate", json=payload) response = await self.client.post("/api/generate", json=payload)
if response.status_code != 200:
logger.error(f"Ollama Error ({response.status_code}): {response.text}")
return "Fehler bei der Generierung."
if response.status_code == 200:
data = response.json() data = response.json()
return data.get("response", "").strip() return data.get("response", "").strip()
else:
response.raise_for_status()
except Exception as e: except Exception as e:
attempt += 1 logger.error(f"LLM Raw Gen Error: {e}")
if attempt > max_retries: return "Interner LLM Fehler."
logger.error(f"LLM Final Error (Versuch {attempt}): {e}")
raise e
wait_time = base_delay * (2 ** (attempt - 1))
logger.warning(f"⚠️ LLM Retry ({attempt}/{max_retries}) in {wait_time}s: {e}")
await asyncio.sleep(wait_time)
async def generate_rag_response(self, query: str, context_str: str) -> str: async def generate_rag_response(self, query: str, context_str: str) -> str:
""" """Legacy Support"""
Chat-Wrapper: Immer Realtime.
"""
system_prompt = self.prompts.get("system_prompt", "") system_prompt = self.prompts.get("system_prompt", "")
rag_template = self.prompts.get("rag_template", "{context_str}\n\n{query}") rag_template = self.prompts.get("rag_template", "{context_str}\n\n{query}")
final_prompt = rag_template.format(context_str=context_str, query=query) final_prompt = rag_template.format(context_str=context_str, query=query)
return await self.generate_raw_response( # Leite an die neue Methode weiter
final_prompt, return await self.generate_raw_response(final_prompt, system=system_prompt)
system=system_prompt,
max_retries=0,
force_json=False,
priority="realtime"
)
async def close(self): async def close(self):
if self.client:
await self.client.aclose() await self.client.aclose()

View File

@ -1,138 +0,0 @@
"""
app/services/semantic_analyzer.py Edge Validation & Filtering
Version: 2.0 (Update: Background Priority for Batch Jobs)
"""
import json
import logging
from typing import List, Optional
from dataclasses import dataclass
# Importe
from app.services.llm_service import LLMService
logger = logging.getLogger(__name__)
class SemanticAnalyzer:
def __init__(self):
self.llm = LLMService()
async def assign_edges_to_chunk(self, chunk_text: str, all_edges: List[str], note_type: str) -> List[str]:
"""
Sendet einen Chunk und eine Liste potenzieller Kanten an das LLM.
Das LLM filtert heraus, welche Kanten für diesen Chunk relevant sind.
Features:
- Retry Strategy: Wartet bei Überlastung (max_retries=5).
- Priority Queue: Läuft als "background" Task, um den Chat nicht zu blockieren.
- Observability: Loggt Input-Größe, Raw-Response und Parsing-Details.
"""
if not all_edges:
return []
# 1. Prompt laden
prompt_template = self.llm.prompts.get("edge_allocation_template")
if not prompt_template:
logger.warning("⚠️ [SemanticAnalyzer] Prompt 'edge_allocation_template' fehlt. Nutze Fallback.")
prompt_template = (
"TASK: Wähle aus den Kandidaten die relevanten Kanten für den Text.\n"
"TEXT: {chunk_text}\n"
"KANDIDATEN: {edge_list}\n"
"OUTPUT: JSON Liste von Strings [\"kind:target\"]."
)
# 2. Kandidaten-Liste formatieren
edges_str = "\n".join([f"- {e}" for e in all_edges])
# LOG: Request Info
logger.debug(f"🔍 [SemanticAnalyzer] Request: {len(chunk_text)} chars Text, {len(all_edges)} Candidates.")
# 3. Prompt füllen
final_prompt = prompt_template.format(
chunk_text=chunk_text[:3500],
edge_list=edges_str
)
try:
# 4. LLM Call mit Traffic Control (NEU: priority="background")
# Wir nutzen die "Slow Lane", damit der User im Chat nicht warten muss.
response_json = await self.llm.generate_raw_response(
prompt=final_prompt,
force_json=True,
max_retries=5,
base_delay=5.0,
priority="background" # <--- WICHTIG: Drosselung aktivieren
)
# LOG: Raw Response Preview
logger.debug(f"📥 [SemanticAnalyzer] Raw Response (Preview): {response_json[:200]}...")
# 5. Parsing & Cleaning
clean_json = response_json.replace("```json", "").replace("```", "").strip()
if not clean_json:
logger.warning("⚠️ [SemanticAnalyzer] Leere Antwort vom LLM erhalten. Trigger Fallback.")
return []
try:
data = json.loads(clean_json)
except json.JSONDecodeError as json_err:
logger.error(f"❌ [SemanticAnalyzer] JSON Decode Error.")
logger.error(f" Grund: {json_err}")
logger.error(f" Empfangener String: {clean_json[:500]}")
logger.info(" -> Workaround: Fallback auf 'Alle Kanten' (durch Chunker).")
return []
valid_edges = []
# 6. Robuste Validierung (List vs Dict)
if isinstance(data, list):
# Standardfall: ["kind:target", ...]
valid_edges = [str(e) for e in data if isinstance(e, str) and ":" in e]
elif isinstance(data, dict):
# Abweichende Formate behandeln
logger.info(f" [SemanticAnalyzer] LLM lieferte Dict statt Liste. Versuche Reparatur. Keys: {list(data.keys())}")
for key, val in data.items():
# Fall A: {"edges": ["kind:target"]}
if key.lower() in ["edges", "results", "kanten", "matches"] and isinstance(val, list):
valid_edges.extend([str(e) for e in val if isinstance(e, str) and ":" in e])
# Fall B: {"kind": "target"}
elif isinstance(val, str):
valid_edges.append(f"{key}:{val}")
# Fall C: {"kind": ["target1", "target2"]}
elif isinstance(val, list):
for target in val:
if isinstance(target, str):
valid_edges.append(f"{key}:{target}")
# Safety: Filtere nur Kanten, die halbwegs valide aussehen
final_result = [e for e in valid_edges if ":" in e]
# LOG: Ergebnis
if final_result:
logger.info(f"✅ [SemanticAnalyzer] Success. {len(final_result)} Kanten zugewiesen.")
else:
logger.debug(" [SemanticAnalyzer] Keine spezifischen Kanten erkannt (Empty Result).")
return final_result
except Exception as e:
logger.error(f"💥 [SemanticAnalyzer] Kritischer Fehler: {e}", exc_info=True)
return []
async def close(self):
if self.llm:
await self.llm.close()
# Singleton Helper
_analyzer_instance = None
def get_semantic_analyzer():
global _analyzer_instance
if _analyzer_instance is None:
_analyzer_instance = SemanticAnalyzer()
return _analyzer_instance

View File

@ -1,31 +1,32 @@
# config/decision_engine.yaml # config/decision_engine.yaml
# Steuerung der Decision Engine (Intent Recognition) # Steuerung der Decision Engine (WP-06 + WP-07)
# Version: 2.4.0 (Clean Architecture: Generic Intents only) # Hybrid-Modus: Keywords (Fast) + LLM Router (Smart Fallback)
version: 1.3
version: 1.4
settings: settings:
llm_fallback_enabled: true llm_fallback_enabled: true
# Few-Shot Prompting für den LLM-Router (Slow Path) # Few-Shot Prompting für bessere SLM-Performance
# Erweitert um INTERVIEW Beispiele
llm_router_prompt: | llm_router_prompt: |
Du bist ein Klassifikator. Analysiere die Nachricht und wähle die passende Strategie. Du bist ein Klassifikator. Analysiere die Nachricht und wähle die passende Strategie.
Antworte NUR mit dem Namen der Strategie. Antworte NUR mit dem Namen der Strategie.
STRATEGIEN: STRATEGIEN:
- INTERVIEW: User will Wissen erfassen, Notizen anlegen oder Dinge festhalten. - INTERVIEW: User will Wissen strukturieren, Notizen anlegen, Projekte starten ("Neu", "Festhalten").
- DECISION: Rat, Strategie, Vor/Nachteile, "Soll ich". - DECISION: Rat, Strategie, Vor/Nachteile, "Soll ich".
- EMPATHY: Gefühle, Frust, Freude, Probleme. - EMPATHY: Gefühle, Frust, Freude, Probleme, "Alles ist sinnlos", "Ich bin traurig".
- CODING: Code, Syntax, Programmierung. - CODING: Code, Syntax, Programmierung, Python.
- FACT: Wissen, Fakten, Definitionen. - FACT: Wissen, Fakten, Definitionen.
BEISPIELE: BEISPIELE:
User: "Wie funktioniert Qdrant?" -> FACT User: "Wie funktioniert Qdrant?" -> FACT
User: "Soll ich Qdrant nutzen?" -> DECISION User: "Soll ich Qdrant nutzen?" -> DECISION
User: "Ich möchte etwas notieren" -> INTERVIEW User: "Ich möchte ein neues Projekt anlegen" -> INTERVIEW
User: "Lass uns das festhalten" -> INTERVIEW User: "Lass uns eine Entscheidung festhalten" -> INTERVIEW
User: "Schreibe ein Python Script" -> CODING User: "Schreibe ein Python Script" -> CODING
User: "Alles ist grau und sinnlos" -> EMPATHY User: "Alles ist grau und sinnlos" -> EMPATHY
User: "Mir geht es heute gut" -> EMPATHY
NACHRICHT: "{query}" NACHRICHT: "{query}"
@ -50,9 +51,11 @@ strategies:
- "empfehlung" - "empfehlung"
- "strategie" - "strategie"
- "entscheidung" - "entscheidung"
- "wert"
- "prinzip"
- "vor- und nachteile"
- "abwägung" - "abwägung"
- "vergleich" inject_types: ["value", "principle", "goal"]
inject_types: ["value", "principle", "goal", "risk"]
prompt_template: "decision_template" prompt_template: "decision_template"
prepend_instruction: | prepend_instruction: |
!!! ENTSCHEIDUNGS-MODUS !!! !!! ENTSCHEIDUNGS-MODUS !!!
@ -69,7 +72,6 @@ strategies:
- "angst" - "angst"
- "nervt" - "nervt"
- "überfordert" - "überfordert"
- "müde"
inject_types: ["experience", "belief", "profile"] inject_types: ["experience", "belief", "profile"]
prompt_template: "empathy_template" prompt_template: "empathy_template"
prepend_instruction: null prepend_instruction: null
@ -86,37 +88,56 @@ strategies:
- "syntax" - "syntax"
- "json" - "json"
- "yaml" - "yaml"
- "bash"
inject_types: ["snippet", "reference", "source"] inject_types: ["snippet", "reference", "source"]
prompt_template: "technical_template" prompt_template: "technical_template"
prepend_instruction: null prepend_instruction: null
# 5. Interview / Datenerfassung # 5. Interview / Datenerfassung (WP-07)
# HINWEIS: Spezifische Typen (Projekt, Ziel etc.) werden automatisch
# über die types.yaml erkannt. Hier stehen nur generische Trigger.
INTERVIEW: INTERVIEW:
description: "Der User möchte Wissen erfassen." description: "Der User möchte strukturiertes Wissen erfassen (Projekt, Notiz, Idee)."
trigger_keywords: trigger_keywords:
- "neue notiz" - "neue notiz"
- "etwas notieren" - "neues projekt"
- "neue entscheidung"
- "neues ziel"
- "festhalten" - "festhalten"
- "erstellen" - "entwurf erstellen"
- "dokumentieren"
- "anlegen"
- "interview" - "interview"
- "dokumentieren"
- "erfassen" - "erfassen"
- "idee speichern" - "idee speichern"
- "draft" inject_types: [] # Keine RAG-Suche, reiner Kontext-Dialog
inject_types: []
prompt_template: "interview_template" prompt_template: "interview_template"
prepend_instruction: null prepend_instruction: null
# Schemas: Hier nur der Fallback. # LATE BINDING SCHEMAS:
# Spezifische Schemas (Project, Experience) kommen jetzt aus types.yaml! # Definition der Pflichtfelder pro Typ (korrespondiert mit types.yaml)
# Wenn ein Typ hier fehlt, wird 'default' genutzt.
schemas: schemas:
default: default:
fields: fields: ["Titel", "Thema/Inhalt", "Tags"]
- "Titel"
- "Thema/Inhalt"
- "Tags"
hint: "Halte es einfach und übersichtlich." hint: "Halte es einfach und übersichtlich."
project:
fields: ["Titel", "Zielsetzung (Goal)", "Status (draft/active)", "Wichtige Stakeholder", "Nächste Schritte"]
hint: "Achte darauf, Abhängigkeiten zu anderen Projekten mit [[rel:depends_on]] zu erfragen."
decision:
fields: ["Titel", "Kontext (Warum entscheiden wir?)", "Getroffene Entscheidung", "Betrachtete Alternativen", "Status (proposed/final)"]
hint: "Wichtig: Frage explizit nach den Gründen gegen die Alternativen."
goal:
fields: ["Titel", "Zeitrahmen (Deadline)", "Messkriterien (KPIs)", "Verbundene Werte"]
hint: "Ziele sollten SMART formuliert sein."
experience:
fields: ["Titel", "Situation (Kontext)", "Erkenntnis (Learning)", "Emotionale Keywords (für Empathie-Suche)"]
hint: "Fokussiere dich auf die persönliche Lektion."
value:
fields: ["Titel (Name des Werts)", "Definition (Was bedeutet das für uns?)", "Anti-Beispiel (Was ist es nicht?)"]
hint: "Werte dienen als Entscheidungsgrundlage."
principle:
fields: ["Titel", "Handlungsanweisung", "Begründung"]
hint: "Prinzipien sind härter als Werte."

View File

@ -97,66 +97,44 @@ technical_template: |
# --------------------------------------------------------- # ---------------------------------------------------------
# 5. INTERVIEW: Der "One-Shot Extractor" (Performance Mode) # 5. INTERVIEW: Der "One-Shot Extractor" (Performance Mode)
# --------------------------------------------------------- # ---------------------------------------------------------
interview_template: | interview_template: |
TASK: TASK:
Du bist ein professioneller Ghostwriter. Verwandle den "USER INPUT" in eine strukturierte Notiz vom Typ '{target_type}'. Erstelle einen Markdown-Entwurf für eine Notiz vom Typ '{target_type}'.
STRUKTUR (Nutze EXAKT diese Überschriften): SCHEMA (Inhaltliche Pflichtfelder für den Body):
{schema_fields} {schema_fields}
USER INPUT: USER INPUT:
"{query}" "{query}"
ANWEISUNG ZUM INHALT: ANWEISUNG:
1. Analysiere den Input genau. 1. Extrahiere Informationen aus dem Input.
2. Schreibe die Inhalte unter die passenden Überschriften aus der STRUKTUR-Liste oben. 2. Generiere validen Markdown.
3. STIL: Schreibe flüssig, professionell und in der Ich-Perspektive. Korrigiere Grammatikfehler, aber behalte den persönlichen Ton bei.
4. Wenn Informationen für einen Abschnitt fehlen, schreibe nur: "[TODO: Ergänzen]". Erfinde nichts dazu.
OUTPUT FORMAT (YAML + MARKDOWN): OUTPUT REGELN (STRIKT BEACHTEN):
A. FRONTMATTER (YAML):
- Darf NUR folgende Felder enthalten: [type, status, title, tags].
- Schreibe KEINE inhaltlichen Sätze (wie 'Situation', 'Ziel') in das YAML!
- Setze 'status: draft'.
B. BODY (Markdown):
- Nutze für jedes Schema-Feld eine Markdown-Überschrift (## Feldname).
- Schreibe den Inhalt DARUNTER.
- Nutze "[TODO: Ergänzen]", wenn Infos fehlen.
HINWEIS ZUM TYP:
{schema_hint}
OUTPUT FORMAT BEISPIEL:
```markdown
--- ---
type: {target_type} type: {target_type}
status: draft status: draft
title: (Erstelle einen treffenden, kurzen Titel für den Inhalt) title: ...
tags: [Tag1, Tag2] tags: [...]
--- ---
# Titel der Notiz
# (Wiederhole den Titel hier) ## Erstes Schema Feld
Der Inhalt hier...
## (Erster Begriff aus STRUKTUR)
(Text...)
## (Zweiter Begriff aus STRUKTUR)
(Text...)
(usw.)
# ---------------------------------------------------------
# 6. EDGE_ALLOCATION: Kantenfilter (Intent: OFFLINE_FILTER)
# ---------------------------------------------------------
edge_allocation_template: |
TASK:
Du bist ein strikter Selektor. Du erhältst eine Liste von "Kandidaten-Kanten" (Strings).
Wähle jene aus, die inhaltlich im "Textabschnitt" vorkommen oder relevant sind.
TEXTABSCHNITT:
"""
{chunk_text}
"""
KANDIDATEN (Auswahl-Pool):
{edge_list}
REGELN:
1. Die Kanten haben das Format "typ:ziel". Der "typ" ist variabel und kann ALLES sein (z.B. uses, blocks, inspired_by, loves, etc.).
2. Gib NUR die Strings aus der Kandidaten-Liste zurück, die zum Text passen.
3. Erfinde KEINE neuen Kanten. Nutze exakt die Schreibweise aus der Liste.
4. Antworte als flache JSON-Liste.
BEISPIEL (Zur Demonstration der Logik):
Input Text: "Das Projekt Alpha scheitert, weil Budget fehlt."
Input Kandidaten: ["blocks:Projekt Alpha", "inspired_by:Buch der Weisen", "needs:Budget"]
Output: ["blocks:Projekt Alpha", "needs:Budget"]
DEIN OUTPUT (JSON):

View File

@ -1,201 +1,86 @@
version: 2.4.0 # Optimized for Async Intelligence & Hybrid Router version: 1.1 # Update auf v1.1 für Mindnet v2.4
# ==============================================================================
# 1. CHUNKING PROFILES
# ==============================================================================
chunking_profiles:
# A. SHORT & FAST
# Für Glossar, Tasks, Risiken. Kleine Schnipsel.
sliding_short:
strategy: sliding_window
enable_smart_edge_allocation: false
target: 200
max: 350
overlap: [30, 50]
# B. STANDARD & FAST
# Der "Traktor": Robust für Quellen, Journal, Daily Logs.
sliding_standard:
strategy: sliding_window
enable_smart_edge_allocation: false
target: 450
max: 650
overlap: [50, 100]
# C. SMART FLOW (Performance-Safe Mode)
# Für Konzepte, Projekte, Erfahrungen.
# HINWEIS: 'enable_smart_edge_allocation' ist vorerst FALSE, um Ollama
# bei der Generierung nicht zu überlasten. Später wieder aktivieren.
sliding_smart_edges:
strategy: sliding_window
enable_smart_edge_allocation: true
target: 400
max: 600
overlap: [50, 80]
# D. SMART STRUCTURE
# Für Profile, Werte, Prinzipien. Trennt hart an Überschriften (H2).
structured_smart_edges:
strategy: by_heading
enable_smart_edge_allocation: true
split_level: 2
max: 600
target: 400
overlap: [50, 80]
# ==============================================================================
# 2. DEFAULTS
# ==============================================================================
defaults: defaults:
retriever_weight: 1.0 retriever_weight: 1.0
chunking_profile: sliding_standard chunk_profile: default
edge_defaults: [] edge_defaults: []
# ==============================================================================
# 3. TYPE DEFINITIONS
# ==============================================================================
types: types:
# --- WISSENSBAUSTEINE ---
# --- KERNTYPEN (Hoch priorisiert & Smart) ---
experience:
chunking_profile: sliding_smart_edges
retriever_weight: 0.90
edge_defaults: ["derived_from", "references"]
# Hybrid Classifier: Wenn diese Worte fallen, ist es eine Experience
detection_keywords:
- "passiert"
- "erlebt"
- "gefühl"
- "situation"
- "stolz"
- "geärgert"
- "reaktion"
- "moment"
- "konflikt"
# Ghostwriter Schema: Sprechende Anweisungen für besseren Textfluss
schema:
- "Situation (Was ist passiert?)"
- "Meine Reaktion (Was habe ich getan?)"
- "Ergebnis & Auswirkung"
- "Reflexion & Learning (Was lerne ich daraus?)"
project:
chunking_profile: sliding_smart_edges
retriever_weight: 0.97
edge_defaults: ["references", "depends_on"]
detection_keywords:
- "projekt"
- "vorhaben"
- "ziel ist"
- "meilenstein"
- "planen"
- "starten"
- "mission"
schema:
- "Mission & Zielsetzung"
- "Aktueller Status & Blockaden"
- "Nächste konkrete Schritte"
- "Stakeholder & Ressourcen"
decision:
chunking_profile: structured_smart_edges
retriever_weight: 1.00 # MAX: Entscheidungen sind Gesetz
edge_defaults: ["caused_by", "references"]
detection_keywords:
- "entschieden"
- "wahl"
- "optionen"
- "alternativen"
- "beschluss"
- "adr"
schema:
- "Kontext & Problemstellung"
- "Betrachtete Optionen (Alternativen)"
- "Die Entscheidung"
- "Begründung (Warum diese Wahl?)"
# --- PERSÖNLICHKEIT & IDENTITÄT ---
value:
chunking_profile: structured_smart_edges
retriever_weight: 1.00
edge_defaults: ["related_to"]
detection_keywords: ["wert", "wichtig ist", "moral", "ethik"]
schema: ["Definition", "Warum mir das wichtig ist", "Leitsätze für den Alltag"]
principle:
chunking_profile: structured_smart_edges
retriever_weight: 0.95
edge_defaults: ["derived_from", "references"]
detection_keywords: ["prinzip", "regel", "grundsatz", "leitlinie"]
schema: ["Das Prinzip", "Anwendung & Beispiele"]
belief:
chunking_profile: sliding_short
retriever_weight: 0.90
edge_defaults: ["related_to"]
detection_keywords: ["glaube", "überzeugung", "denke dass", "meinung"]
schema: ["Der Glaubenssatz", "Ursprung & Reflexion"]
profile:
chunking_profile: structured_smart_edges
retriever_weight: 0.70
edge_defaults: ["references", "related_to"]
schema: ["Rolle / Identität", "Fakten & Daten", "Historie"]
# --- STRATEGIE & RISIKO ---
goal:
chunking_profile: sliding_smart_edges
retriever_weight: 0.95
edge_defaults: ["depends_on", "related_to"]
schema: ["Zielzustand", "Zeitrahmen & KPIs", "Motivation"]
risk:
chunking_profile: sliding_short
retriever_weight: 0.85
edge_defaults: ["related_to", "blocks"]
detection_keywords: ["risiko", "gefahr", "bedrohung", "problem", "angst"]
schema: ["Beschreibung des Risikos", "Mögliche Auswirkungen", "Gegenmaßnahmen"]
# --- BASIS & WISSEN ---
concept: concept:
chunking_profile: sliding_smart_edges chunk_profile: medium
retriever_weight: 0.60 retriever_weight: 0.60
edge_defaults: ["references", "related_to"] edge_defaults: ["references", "related_to"]
schema:
- "Definition"
- "Kontext & Hintergrund"
- "Verwandte Konzepte"
task:
chunking_profile: sliding_short
retriever_weight: 0.80
edge_defaults: ["depends_on", "part_of"]
schema: ["Aufgabe", "Kontext", "Definition of Done"]
journal:
chunking_profile: sliding_standard
retriever_weight: 0.80
edge_defaults: ["references", "related_to"]
schema: ["Log-Eintrag", "Gedanken & Erkenntnisse"]
source: source:
chunking_profile: sliding_standard chunk_profile: short
retriever_weight: 0.50 retriever_weight: 0.50
edge_defaults: [] edge_defaults: [] # Quellen sind passiv
schema:
- "Metadaten (Autor, URL, Datum)"
- "Kernaussage / Zusammenfassung"
- "Zitate & Notizen"
glossary: glossary:
chunking_profile: sliding_short chunk_profile: short
retriever_weight: 0.40 retriever_weight: 0.40
edge_defaults: ["related_to"] edge_defaults: ["related_to"]
schema: ["Begriff", "Definition"]
# --- IDENTITÄT & PERSÖNLICHKEIT (Decision Engine Core) ---
profile:
chunk_profile: long
retriever_weight: 0.70
edge_defaults: ["references", "related_to"]
value:
chunk_profile: short
retriever_weight: 1.00 # MAX: Werte stechen Fakten im Decision-Mode
edge_defaults: ["related_to"]
principle:
chunk_profile: short
retriever_weight: 0.95 # Sehr hoch: Handlungsleitlinien
edge_defaults: ["derived_from", "references"] # Prinzipien leiten sich oft woraus ab
belief: # NEU: Glaubenssätze für Empathie-Modus
chunk_profile: short
retriever_weight: 0.90
edge_defaults: ["related_to"]
experience:
chunk_profile: medium
retriever_weight: 0.90
edge_defaults: ["derived_from", "references"] # Erfahrungen haben einen Ursprung
# --- STRATEGIE & ENTSCHEIDUNG ---
goal:
chunk_profile: medium
retriever_weight: 0.95
edge_defaults: ["depends_on", "related_to"]
decision: # ADRs (Architecture Decision Records)
chunk_profile: long # Entscheidungen brauchen oft viel Kontext (Begründung)
retriever_weight: 1.00 # MAX: Getroffene Entscheidungen sind Gesetz
edge_defaults: ["caused_by", "references"] # Entscheidungen haben Gründe
risk: # NEU: Risikomanagement
chunk_profile: short
retriever_weight: 0.85
edge_defaults: ["related_to", "blocks"] # Risiken blockieren ggf. Projekte
milestone:
chunk_profile: short
retriever_weight: 0.70
edge_defaults: ["related_to", "part_of"]
# --- OPERATIV ---
project:
chunk_profile: long
retriever_weight: 0.97 # Projekte sind der Kontext für alles
edge_defaults: ["references", "depends_on"]
task:
chunk_profile: short
retriever_weight: 0.80
edge_defaults: ["depends_on", "part_of"]
journal:
chunk_profile: medium
retriever_weight: 0.80
edge_defaults: ["references", "related_to"]

View File

@ -1,7 +1,7 @@
# mindnet v2.4 Knowledge Design Manual # mindnet v2.4 Knowledge Design Manual
**Datei:** `docs/mindnet_knowledge_design_manual_v2.6.md` **Datei:** `docs/mindnet_knowledge_design_manual_v2.4.md`
**Stand:** 2025-12-12 **Stand:** 2025-12-11
**Status:** **FINAL** (Integrierter Stand WP01WP15) **Status:** **FINAL** (Integrierter Stand WP01WP11)
**Quellen:** `knowledge_design.md`, `TYPE_REGISTRY_MANUAL.md`, `chunking_strategy.md`, `mindnet_functional_architecture.md`. **Quellen:** `knowledge_design.md`, `TYPE_REGISTRY_MANUAL.md`, `chunking_strategy.md`, `mindnet_functional_architecture.md`.
--- ---
@ -24,9 +24,9 @@ Dieses Handbuch ist die **primäre Arbeitsanweisung** für dich als Mindmaster (
### 1.1 Zielsetzung ### 1.1 Zielsetzung
Mindnet ist mehr als eine Dokumentablage. Es ist ein vernetztes System, das deine Persönlichkeit, Entscheidungen und Erfahrungen abbildet. Mindnet ist mehr als eine Dokumentablage. Es ist ein vernetztes System, das deine Persönlichkeit, Entscheidungen und Erfahrungen abbildet.
Seit Version 2.6 verfügt Mindnet über: Seit Version 2.4 verfügt Mindnet über:
* **Hybrid Router:** Das System erkennt, ob du Fakten, Entscheidungen oder Empathie brauchst. * **Hybrid Router:** Das System erkennt, ob du Fakten, Entscheidungen oder Empathie brauchst.
* **Smart Edge Allocation:** Das System prüft deine Links intelligent und bindet sie nur an die Textabschnitte (Chunks), wo sie wirklich relevant sind. * **Context Intelligence:** Das System lädt je nach Situation unterschiedliche Notiz-Typen (z.B. Werte bei Entscheidungen).
* **Web UI (WP10):** Du kannst direkt sehen, welche Quellen genutzt wurden. * **Web UI (WP10):** Du kannst direkt sehen, welche Quellen genutzt wurden.
* **Active Intelligence (WP11):** Das System hilft dir beim Schreiben und Vernetzen (Link-Vorschläge). * **Active Intelligence (WP11):** Das System hilft dir beim Schreiben und Vernetzen (Link-Vorschläge).
@ -69,7 +69,7 @@ Diese Felder sind technisch nicht zwingend, aber für bestimmte Typen sinnvoll:
**Die ID (Identifikator):** **Die ID (Identifikator):**
* Muss global eindeutig und **stabil** sein. * Muss global eindeutig und **stabil** sein.
* Darf sich nicht ändern, wenn die Datei umbenannt oder verschoben wird. * Darf sich nicht ändern, wenn die Datei umbenannt oder verschoben wird.
* **Empfehlung:** `YYYYMMDD-slug` (z. B. `20231027-vektor-db`). Dies garantiert Eindeutigkeit und Chronologie. * **Empfehlung:** `YYYYMMDD-slug-hash` (z. B. `20231027-vektor-db-a1b2`). Dies garantiert Eindeutigkeit und Chronologie.
**Dateinamen & Pfade:** **Dateinamen & Pfade:**
* Pfade dienen der menschlichen Ordnung (Ordnerstruktur), sind für Mindnet aber sekundär. * Pfade dienen der menschlichen Ordnung (Ordnerstruktur), sind für Mindnet aber sekundär.
@ -80,7 +80,7 @@ Diese Felder sind technisch nicht zwingend, aber für bestimmte Typen sinnvoll:
## 3. Note-Typen & Typ-Registry ## 3. Note-Typen & Typ-Registry
Der `type` ist der wichtigste Hebel im Knowledge Design. Er steuert nicht nur das Gewicht bei der Suche, sondern seit WP-06 auch, **wann** eine Notiz aktiv in den Chat geholt wird ("Strategic Retrieval") und seit WP-15, **ob Smart Edges aktiviert sind**. Der `type` ist der wichtigste Hebel im Knowledge Design. Er steuert nicht nur das Gewicht bei der Suche, sondern seit WP-06 auch, **wann** eine Notiz aktiv in den Chat geholt wird ("Strategic Retrieval") und **welche Felder** im Interview abgefragt werden.
### 3.1 Übersicht der Kern-Typen ### 3.1 Übersicht der Kern-Typen
@ -95,8 +95,8 @@ Mindnet unterscheidet verschiedene Wissensarten. Wähle den Typ, der die **Rolle
| **`value`** | Ein persönlicher Wert oder ein Prinzip. | **DECISION** | Definition, Anti-Beispiel | Ziel für `based_on` | | **`value`** | Ein persönlicher Wert oder ein Prinzip. | **DECISION** | Definition, Anti-Beispiel | Ziel für `based_on` |
| **`principle`** | Handlungsleitlinie. | **DECISION** | Regel, Ausnahme | Quelle für `derived_from` | | **`principle`** | Handlungsleitlinie. | **DECISION** | Regel, Ausnahme | Quelle für `derived_from` |
| **`goal`** | Ein strategisches Ziel (kurz- oder langfristig). | **DECISION** | Zeitrahmen, KPIs, Werte | Ziel für `related_to` | | **`goal`** | Ein strategisches Ziel (kurz- oder langfristig). | **DECISION** | Zeitrahmen, KPIs, Werte | Ziel für `related_to` |
| **`risk`** | Ein identifiziertes Risiko oder eine Gefahr. | **DECISION** | Auswirkung, Wahrscheinlichkeit | Quelle für `blocks` | | **`risk`** | **NEU:** Ein identifiziertes Risiko oder eine Gefahr. | **DECISION** | Auswirkung, Wahrscheinlichkeit | Quelle für `blocks` |
| **`belief`** | Glaubenssatz / Überzeugung. | **EMPATHY** | Ursprung, Mantra | - | | **`belief`** | **NEU:** Glaubenssatz / Überzeugung. | **EMPATHY** | Ursprung, Mantra | - |
| **`person`** | Eine reale Person (Netzwerk, Autor). | **FACT** | Rolle, Kontext | - | | **`person`** | Eine reale Person (Netzwerk, Autor). | **FACT** | Rolle, Kontext | - |
| **`journal`** | Zeitbezogener Log-Eintrag, Daily Note. | **FACT** | Datum, Tags | - | | **`journal`** | Zeitbezogener Log-Eintrag, Daily Note. | **FACT** | Datum, Tags | - |
| **`source`** | Externe Quelle (Buch, PDF, Artikel). | **FACT** | Autor, URL | - | | **`source`** | Externe Quelle (Buch, PDF, Artikel). | **FACT** | Autor, URL | - |
@ -107,11 +107,13 @@ Der `type` steuert im Hintergrund drei technische Mechanismen:
1. **`retriever_weight` (Wichtigkeit):** 1. **`retriever_weight` (Wichtigkeit):**
* Ein `concept` (0.6) wiegt weniger als ein `project` (0.97) oder eine `decision` (1.0). * Ein `concept` (0.6) wiegt weniger als ein `project` (0.97) oder eine `decision` (1.0).
* **Warum?** Bei einer Suche nach "Datenbank" soll Mindnet bevorzugt deine *Entscheidung* ("Warum wir X nutzen") anzeigen.
2. **`chunk_profile` (Textzerlegung):** 2. **`chunk_profile` (Textzerlegung):**
* `journal` (short): Wird fein zerlegt. * `journal` (short): Wird fein zerlegt.
* `experience` (sliding_smart_edges): Wird intelligent analysiert. * `project` (long): Längere Kontext-Fenster.
3. **`enable_smart_edge_allocation` (WP15):** 3. **`edge_defaults` (Automatische Vernetzung):**
* Wenn `true`: Das System analysiert jeden Abschnitt mit KI, um zu prüfen, ob verlinkte Themen dort wirklich relevant sind. Das erhöht die Präzision massiv. * Mindnet ergänzt automatisch Kanten.
* Beispiel: Ein Link in einem `project` wird automatisch als `depends_on` (Abhängigkeit) interpretiert.
--- ---
@ -289,4 +291,4 @@ Wir vermeiden es, Logik in den Markdown-Dateien hart zu kodieren.
### 7.2 Was bedeutet das für dich? ### 7.2 Was bedeutet das für dich?
* Du kannst dich auf den Inhalt konzentrieren. * Du kannst dich auf den Inhalt konzentrieren.
* Wenn wir in Zukunft basierend auf Feedback lernen, dass "Projekte" noch wichtiger sind, ändern wir **eine Zeile** in der Konfiguration, und das gesamte System passt sich beim nächsten Import an. * Wenn wir in Zukunft (WP08) basierend auf Feedback lernen, dass "Projekte" noch wichtiger sind, ändern wir **eine Zeile** in der Konfiguration, und das gesamte System passt sich beim nächsten Import an.

View File

@ -1,8 +1,8 @@
# Mindnet v2.4 Overview & Einstieg # Mindnet v2.4 Overview & Einstieg
**Datei:** `docs/mindnet_overview_v2.6.md` **Datei:** `docs/mindnet_overview_v2.4.md`
**Stand:** 2025-12-12 **Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. Smart Edges, Traffic Control & Healing UI) **Status:** **FINAL** (Inkl. Async Intelligence & Editor)
**Version:** 2.6.0 **Version:** 2.4.0
--- ---
@ -28,14 +28,14 @@ Mindnet arbeitet auf drei Schichten, die aufeinander aufbauen:
### Ebene 1: Content (Das Gedächtnis) ### Ebene 1: Content (Das Gedächtnis)
* **Quelle:** Dein lokaler Obsidian-Vault (Markdown). * **Quelle:** Dein lokaler Obsidian-Vault (Markdown).
* **Funktion:** Speicherung von Fakten, Projekten und Logs. * **Funktion:** Speicherung von Fakten, Projekten und Logs.
* **Technik:** Async Import-Pipeline, Smart Chunking (LLM-gestützte Kantenzuweisung), Vektor-Datenbank (Qdrant). * **Technik:** Async Import-Pipeline, Chunking, Vektor-Datenbank (Qdrant).
* **Status:** 🟢 Live (WP01WP03, WP11, WP15). * **Status:** 🟢 Live (WP01WP03, WP11).
### Ebene 2: Semantik (Das Verstehen) ### Ebene 2: Semantik (Das Verstehen)
* **Funktion:** Verknüpfung von isolierten Notizen zu einem Netzwerk. * **Funktion:** Verknüpfung von isolierten Notizen zu einem Netzwerk.
* **Logik:** "Projekt A *hängt ab von* Entscheidung B". * **Logik:** "Projekt A *hängt ab von* Entscheidung B".
* **Technik:** Hybrider Retriever (Graph + Nomic Embeddings), Explanation Engine. * **Technik:** Hybrider Retriever (Graph + Nomic Embeddings), Explanation Engine.
* **Status:** 🟢 Live (WP04, WP11, WP15). * **Status:** 🟢 Live (WP04, WP11).
### Ebene 3: Identität & Interaktion (Die Persönlichkeit) ### Ebene 3: Identität & Interaktion (Die Persönlichkeit)
* **Funktion:** Interaktion, Bewertung und Co-Creation. * **Funktion:** Interaktion, Bewertung und Co-Creation.
@ -43,11 +43,11 @@ Mindnet arbeitet auf drei Schichten, die aufeinander aufbauen:
* "Ich empfehle Lösung X, weil sie unserem Wert 'Datensparsamkeit' entspricht." * "Ich empfehle Lösung X, weil sie unserem Wert 'Datensparsamkeit' entspricht."
* "Ich sehe, du willst ein Projekt starten. Lass uns die Eckdaten erfassen." * "Ich sehe, du willst ein Projekt starten. Lass uns die Eckdaten erfassen."
* **Technik:** * **Technik:**
* **Hybrid Router v5:** Erkennt Absichten (Frage vs. Befehl) und unterscheidet Objekte (`types.yaml`) von Handlungen (`decision_engine.yaml`). * **Intent Router:** Erkennt Absichten (Fakt vs. Gefühl vs. Entscheidung vs. Interview).
* **Traffic Control:** Priorisiert Chat-Anfragen ("Realtime") vor Hintergrund-Jobs ("Background"). * **Strategic Retrieval:** Lädt gezielt Werte oder Erfahrungen nach.
* **One-Shot Extraction:** Generiert Entwürfe für neue Notizen. * **One-Shot Extraction:** Generiert Entwürfe für neue Notizen.
* **Active Intelligence:** Schlägt Links während des Schreibens vor. * **Active Intelligence:** Schlägt Links während des Schreibens vor.
* **Status:** 🟢 Live (WP05WP07, WP10, WP15). * **Status:** 🟢 Live (WP05WP07, WP10).
--- ---
@ -57,9 +57,8 @@ Der Datenfluss in Mindnet ist zyklisch ("Data Flywheel"):
1. **Input:** Du schreibst Notizen in Obsidian **ODER** lässt sie von Mindnet im Chat entwerfen. 1. **Input:** Du schreibst Notizen in Obsidian **ODER** lässt sie von Mindnet im Chat entwerfen.
2. **Intelligence (Live):** Während du schreibst, analysiert Mindnet den Text und schlägt Verknüpfungen vor (Sliding Window Analyse). 2. **Intelligence (Live):** Während du schreibst, analysiert Mindnet den Text und schlägt Verknüpfungen vor (Sliding Window Analyse).
3. **Ingest:** Ein asynchrones Python-Skript importiert und zerlegt die Daten. 3. **Ingest:** Ein asynchrones Python-Skript importiert, zerlegt (Chunking) und vernetzt (Edges) die Daten in Qdrant.
* **Neu (Smart Edges):** Ein LLM analysiert jeden Textabschnitt und entscheidet, welche Kanten relevant sind. 4. **Intent Recognition:** Der Router analysiert deine Frage: Willst du Fakten, Code, Empathie oder etwas dokumentieren?
4. **Intent Recognition:** Der Router analysiert deine Eingabe: Ist es eine Frage (RAG) oder ein Befehl (Interview)?
5. **Retrieval / Action:** 5. **Retrieval / Action:**
* Bei Fragen: Das System sucht Inhalte passend zum Intent. * Bei Fragen: Das System sucht Inhalte passend zum Intent.
* Bei Interviews: Das System wählt das passende Schema (z.B. Projekt-Vorlage). * Bei Interviews: Das System wählt das passende Schema (z.B. Projekt-Vorlage).
@ -70,8 +69,7 @@ Der Datenfluss in Mindnet ist zyklisch ("Data Flywheel"):
* **Backend:** Python 3.10+, FastAPI (Async). * **Backend:** Python 3.10+, FastAPI (Async).
* **Datenbank:** Qdrant (Vektor & Graph, 768 Dim). * **Datenbank:** Qdrant (Vektor & Graph, 768 Dim).
* **KI:** Ollama (Phi-3 Mini für Chat, Nomic für Embeddings) 100% lokal. * **KI:** Ollama (Phi-3 Mini für Chat, Nomic für Embeddings) 100% lokal.
* **Traffic Control:** Semaphore-Logik zur Lastverteilung zwischen Chat und Import. * **Frontend:** Streamlit Web-UI (v2.4).
* **Frontend:** Streamlit Web-UI (v2.5) mit Healing Parser.
--- ---
@ -81,13 +79,13 @@ Wo findest du was?
| Wenn du... | ...lies dieses Dokument | | Wenn du... | ...lies dieses Dokument |
| :--- | :--- | | :--- | :--- |
| **...wissen willst, wie man Notizen schreibt.** | `mindnet_knowledge_design_manual_v2.6.md` | | **...wissen willst, wie man Notizen schreibt.** | `mindnet_knowledge_design_manual_v2.4.md` |
| **...das System installieren oder betreiben musst.** | `mindnet_admin_guide_v2.6.md` | | **...das System installieren oder betreiben musst.** | `mindnet_admin_guide_v2.4.md` |
| **...am Python-Code entwickeln willst.** | `mindnet_developer_guide_v2.6.md` | | **...am Python-Code entwickeln willst.** | `mindnet_developer_guide_v2.4.md` |
| **...die Pipeline (Import -> RAG) verstehen willst.** | `mindnet_pipeline_playbook_v2.6.md` | | **...die Pipeline (Import -> RAG) verstehen willst.** | `mindnet_pipeline_playbook_v2.4.md` |
| **...die genaue JSON-Struktur oder APIs suchst.** | `mindnet_technical_architecture_v2.6.md` | | **...die genaue JSON-Struktur oder APIs suchst.** | `mindnet_technical_architecture.md` |
| **...verstehen willst, was fachlich passiert.** | `mindnet_functional_architecture_v2.6.md` | | **...verstehen willst, was fachlich passiert.** | `mindnet_functional_architecture.md` |
| **...den aktuellen Projektstatus suchst.** | `mindnet_appendices_v2.6.md` | | **...den aktuellen Projektstatus suchst.** | `mindnet_appendices_v2.4.md` |
--- ---
@ -101,5 +99,5 @@ Wo findest du was?
## 6. Aktueller Fokus ## 6. Aktueller Fokus
Wir haben die **Smart Edge Allocation (WP15)** und die **System-Stabilisierung (Traffic Control)** erfolgreich abgeschlossen. Wir haben den **Interview-Assistenten (WP07)** und die **Backend Intelligence (WP11)** erfolgreich integriert.
Das System kann nun aktiv helfen, Wissen zu strukturieren, ohne dabei unter Last zusammenzubrechen. Der Fokus verschiebt sich nun in Richtung **Conversational Memory (WP17)** und **MCP Integration (WP13)**. Das System kann nun aktiv helfen, Wissen zu strukturieren und zu vernetzen. Der Fokus verschiebt sich nun in Richtung **Self-Tuning (WP08)**, um aus dem gesammelten Feedback automatisch zu lernen.

View File

@ -1,8 +1,8 @@
# Mindnet v2.4 Admin Guide # Mindnet v2.4 Admin Guide
**Datei:** `docs/mindnet_admin_guide_v2.6.md` **Datei:** `docs/mindnet_admin_guide_v2.4.md`
**Stand:** 2025-12-12 **Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. Traffic Control & Smart Edge Config) **Status:** **FINAL** (Inkl. Async Architecture & Nomic Model)
**Quellen:** `Handbuch.md`, `mindnet_developer_guide_v2.6.md`. **Quellen:** `Handbuch.md`, `mindnet_developer_guide_v2.4.md`.
> Dieses Handbuch richtet sich an **Administratoren**. Es beschreibt Installation, Konfiguration, Backup-Strategien, Monitoring und den sicheren Betrieb der Mindnet-Instanz (API + UI + DB). > Dieses Handbuch richtet sich an **Administratoren**. Es beschreibt Installation, Konfiguration, Backup-Strategien, Monitoring und den sicheren Betrieb der Mindnet-Instanz (API + UI + DB).
@ -55,7 +55,7 @@ Wir nutzen Qdrant als Vektor-Datenbank. Persistenz ist wichtig.
### 2.4 Ollama Setup (LLM & Embeddings) ### 2.4 Ollama Setup (LLM & Embeddings)
Mindnet benötigt einen lokalen LLM-Server für Chat UND Embeddings. Mindnet benötigt einen lokalen LLM-Server für Chat UND Embeddings.
**WICHTIG (Update v2.4):** Es muss zwingend `nomic-embed-text` installiert sein, sonst startet der Import nicht. **WICHTIG (Update v2.3.10):** Es muss zwingend `nomic-embed-text` installiert sein, sonst startet der Import nicht.
# 1. Installieren (Linux Script) # 1. Installieren (Linux Script)
curl -fsSL https://ollama.com/install.sh | sh curl -fsSL https://ollama.com/install.sh | sh
@ -68,7 +68,7 @@ Mindnet benötigt einen lokalen LLM-Server für Chat UND Embeddings.
curl http://localhost:11434/api/generate -d '{"model": "phi3:mini", "prompt":"Hi"}' curl http://localhost:11434/api/generate -d '{"model": "phi3:mini", "prompt":"Hi"}'
### 2.5 Konfiguration (ENV) ### 2.5 Konfiguration (ENV)
Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM` und das neue `MINDNET_LLM_BACKGROUND_LIMIT`. Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM` und `MINDNET_EMBEDDING_MODEL`.
# Server Config # Server Config
UVICORN_HOST=0.0.0.0 UVICORN_HOST=0.0.0.0
@ -87,15 +87,11 @@ Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM`
# AI Modelle (Ollama) # AI Modelle (Ollama)
MINDNET_OLLAMA_URL="http://127.0.0.1:11434" MINDNET_OLLAMA_URL="http://127.0.0.1:11434"
MINDNET_LLM_MODEL="phi3:mini" MINDNET_LLM_MODEL="phi3:mini"
MINDNET_EMBEDDING_MODEL="nomic-embed-text" MINDNET_EMBEDDING_MODEL="nomic-embed-text" # NEU
# Timeouts (Erhöht für Async/Nomic/Smart Edges) # Timeouts (Erhöht für Async/Nomic)
MINDNET_LLM_TIMEOUT=300.0 MINDNET_LLM_TIMEOUT=300.0
MINDNET_API_TIMEOUT=300.0 # Wichtig: Frontend muss warten können MINDNET_API_TIMEOUT=60.0
# Traffic Control (Neu in v2.6)
# Begrenzt parallele LLM-Calls im Import, um Chat stabil zu halten.
MINDNET_LLM_BACKGROUND_LIMIT=2
# Configs # Configs
MINDNET_PROMPTS_PATH="./config/prompts.yaml" MINDNET_PROMPTS_PATH="./config/prompts.yaml"
@ -159,15 +155,12 @@ Mindnet benötigt zwei Services pro Umgebung: API (Uvicorn) und UI (Streamlit).
## 3. Betrieb im Alltag ## 3. Betrieb im Alltag
### 3.1 Regelmäßige Importe ### 3.1 Regelmäßige Importe
Der Vault-Zustand sollte regelmäßig (z.B. stündlich per Cronjob) nach Qdrant synchronisiert werden. Das Skript nutzt nun **AsyncIO**, **Smart Edges** und **Traffic Control**. Der Vault-Zustand sollte regelmäßig (z.B. stündlich per Cronjob) nach Qdrant synchronisiert werden. Das Skript nutzt nun **AsyncIO** und eine Semaphore, um Ollama nicht zu überlasten.
**Cronjob-Beispiel (stündlich):** **Cronjob-Beispiel (stündlich):**
0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1 0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1
**Hinweis zu Smart Edges:**
Der Import dauert nun länger (ca. 2-3 Min pro Datei), da das LLM intensiv genutzt wird. Dank `MINDNET_LLM_BACKGROUND_LIMIT=2` bleibt der Server dabei aber für den Chat responsive.
### 3.2 Health-Checks ### 3.2 Health-Checks
Prüfe regelmäßig, ob alle Komponenten laufen. Prüfe regelmäßig, ob alle Komponenten laufen.
@ -183,7 +176,7 @@ Prüfe regelmäßig, ob alle Komponenten laufen.
--- ---
## 4. Troubleshooting (Update v2.6) ## 4. Troubleshooting (Update v2.4)
### "Vector dimension error: expected dim: 768, got 384" ### "Vector dimension error: expected dim: 768, got 384"
* **Ursache:** Du versuchst, in eine alte Qdrant-Collection (mit 384 Dim aus v2.2) neue Embeddings (mit 768 Dim von Nomic) zu schreiben. * **Ursache:** Du versuchst, in eine alte Qdrant-Collection (mit 384 Dim aus v2.2) neue Embeddings (mit 768 Dim von Nomic) zu schreiben.
@ -191,17 +184,15 @@ Prüfe regelmäßig, ob alle Komponenten laufen.
1. `python -m scripts.reset_qdrant --mode wipe --prefix mindnet --yes` (Löscht DB). 1. `python -m scripts.reset_qdrant --mode wipe --prefix mindnet --yes` (Löscht DB).
2. `python -m scripts.import_markdown ...` (Baut neu auf). 2. `python -m scripts.import_markdown ...` (Baut neu auf).
### "500 Internal Server Error" beim Speichern/Chatten ### "500 Internal Server Error" beim Speichern
* **Ursache:** Oft Timeout bei Ollama, wenn `nomic-embed-text` noch nicht im RAM geladen ist ("Cold Start") oder der Import die GPU blockiert. * **Ursache:** Oft Timeout bei Ollama, wenn `nomic-embed-text` noch nicht im RAM geladen ist ("Cold Start").
* **Lösung:** * **Lösung:**
1. `MINDNET_LLM_TIMEOUT` in `.env` auf `300.0` setzen. 1. Sicherstellen, dass Modell existiert: `ollama list`.
2. `MINDNET_LLM_BACKGROUND_LIMIT` auf `1` reduzieren (falls Hardware schwach). 2. API neustarten (re-initialisiert Async Clients).
### Import ist sehr langsam ### "NameError: name 'os' is not defined"
* **Ursache:** Smart Edges sind aktiv (`types.yaml`) und analysieren jeden Chunk. * **Ursache:** Fehlender Import in Skripten nach Updates.
* **Lösung:** * **Lösung:** `git pull` (Fix wurde in v2.3.10 deployed).
* Akzeptieren (für bessere Qualität).
* Oder für Massen-Initial-Import in `types.yaml` temporär `enable_smart_edge_allocation: false` setzen.
--- ---
@ -224,7 +215,7 @@ Wenn die Datenbank korrupt ist oder Modelle gewechselt werden:
# 1. DB komplett leeren (Wipe) # 1. DB komplett leeren (Wipe)
python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes
# 2. Alles neu importieren (Force re-calculates Hashes) # 2. Alles neu importieren
python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
--- ---

View File

@ -1,7 +1,7 @@
# Mindnet v2.4 Appendices & Referenzen # Mindnet v2.4 Appendices & Referenzen
**Datei:** `docs/mindnet_appendices_v2.6.md` **Datei:** `docs/mindnet_appendices_v2.4.md`
**Stand:** 2025-12-12 **Stand:** 2025-12-11
**Status:** **FINAL** (Integrierter Stand WP01WP15) **Status:** **FINAL** (Integrierter Stand WP01WP11)
**Quellen:** `TYPE_REGISTRY_MANUAL.md`, `chunking_strategy.md`, `mindnet_technical_architecture.md`, `Handbuch.md`. **Quellen:** `TYPE_REGISTRY_MANUAL.md`, `chunking_strategy.md`, `mindnet_technical_architecture.md`, `Handbuch.md`.
> Dieses Dokument bündelt Tabellen, Schemata und technische Referenzen, die in den Prozess-Dokumenten (Playbook, Guides) den Lesefluss stören würden. > Dieses Dokument bündelt Tabellen, Schemata und technische Referenzen, die in den Prozess-Dokumenten (Playbook, Guides) den Lesefluss stören würden.
@ -10,27 +10,22 @@
## Anhang A: Typ-Registry Referenz (Default-Werte) ## Anhang A: Typ-Registry Referenz (Default-Werte)
Diese Tabelle zeigt die Standard-Konfiguration der `types.yaml` (Stand v2.6 / Config v1.6). Diese Tabelle zeigt die Standard-Konfiguration der `types.yaml` (Stand v2.4).
| Typ (`type`) | Chunk Profile | Retriever Weight | Smart Edges? | Beschreibung | | Typ (`type`) | Chunk Profile | Retriever Weight | Edge Defaults (Auto-Kanten) | Beschreibung |
| :--- | :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- | :--- |
| **concept** | `sliding_smart_edges` | 0.60 | **Ja** | Abstrakte Begriffe, Theorien. | | **concept** | `medium` | 0.60 | `references`, `related_to` | Abstrakte Begriffe, Theorien. |
| **project** | `sliding_smart_edges` | 0.97 | **Ja** | Aktive Vorhaben. Hohe Priorität. | | **project** | `long` | 0.97 | `references`, `depends_on` | Aktive Vorhaben. Hohe Priorität. |
| **decision** | `structured_smart_edges` | 1.00 | **Ja** | Entscheidungen (ADRs). Höchste Prio. | | **decision** | `long` | 1.00 | `caused_by`, `references` | Entscheidungen (ADRs). Höchste Prio. |
| **experience** | `sliding_smart_edges` | 0.90 | **Ja** | Persönliche Learnings. Intensiv analysiert. | | **experience** | `medium` | 0.90 | `derived_from`, `inspired_by` | Persönliche Learnings. |
| **journal** | `sliding_standard` | 0.80 | Nein | Zeitgebundene Logs. Performance-optimiert. | | **journal** | `short` | 0.80 | `related_to` | Zeitgebundene Logs. Fein granular. |
| **person** | `sliding_standard` | 0.50 | Nein | Personen-Profile. | | **person** | `short` | 0.50 | `related_to` | Personen-Profile. |
| **source** | `sliding_standard` | 0.50 | Nein | Externe Quellen (Bücher, PDFs). | | **source** | `long` | 0.50 | *(keine)* | Externe Quellen (Bücher, PDFs). |
| **event** | `sliding_standard` | 0.60 | Nein | Meetings, Konferenzen. | | **event** | `short` | 0.60 | `related_to` | Meetings, Konferenzen. |
| **value** | `structured_smart_edges` | 1.00 | **Ja** | Persönliche Werte/Prinzipien. | | **value** | `medium` | 1.00 | `related_to` | Persönliche Werte/Prinzipien. |
| **principle** | `structured_smart_edges` | 1.00 | **Ja** | Handlungsleitlinien. | | **goal** | `medium` | 0.95 | `depends_on` | Strategische Ziele. |
| **profile** | `structured_smart_edges` | 0.80 | **Ja** | Eigene Identitäts-Beschreibungen. | | **belief** | `medium` | 0.90 | `related_to` | Glaubenssätze. |
| **goal** | `sliding_standard` | 0.95 | Nein | Strategische Ziele. | | **default** | `medium` | 1.00 | `references` | Fallback, wenn Typ unbekannt. |
| **belief** | `sliding_short` | 0.90 | Nein | Glaubenssätze. |
| **risk** | `sliding_short` | 0.90 | Nein | Risiken & Gefahren. |
| **task** | `sliding_short` | 0.70 | Nein | Aufgaben. |
| **glossary** | `sliding_short` | 0.50 | Nein | Begriffsdefinitionen. |
| **default** | `sliding_standard` | 1.00 | Nein | Fallback, wenn Typ unbekannt. |
--- ---
@ -48,7 +43,6 @@ Referenz aller implementierten Kantenarten (`kind`).
| `similar_to` | Inline | Ja | Inhaltliche Ähnlichkeit. "Ist wie X". | | `similar_to` | Inline | Ja | Inhaltliche Ähnlichkeit. "Ist wie X". |
| `caused_by` | Inline | Nein | Kausalität. "X ist der Grund für Y". | | `caused_by` | Inline | Nein | Kausalität. "X ist der Grund für Y". |
| `solves` | Inline | Nein | Lösung. "Tool X löst Problem Y". | | `solves` | Inline | Nein | Lösung. "Tool X löst Problem Y". |
| `blocks` | Inline | Nein | **NEU:** Blockade. "Risiko X blockiert Projekt Y". |
| `derived_from` | Matrix / Default | Nein | Herkunft. "Erkenntnis stammt aus Prinzip X". | | `derived_from` | Matrix / Default | Nein | Herkunft. "Erkenntnis stammt aus Prinzip X". |
| `based_on` | Matrix | Nein | Fundament. "Erfahrung basiert auf Wert Y". | | `based_on` | Matrix | Nein | Fundament. "Erfahrung basiert auf Wert Y". |
| `uses` | Matrix | Nein | Nutzung. "Projekt nutzt Konzept Z". | | `uses` | Matrix | Nein | Nutzung. "Projekt nutzt Konzept Z". |
@ -66,7 +60,7 @@ Diese sind die Felder, die effektiv in Qdrant gespeichert werden.
"title": "string (text)", // Titel aus Frontmatter "title": "string (text)", // Titel aus Frontmatter
"type": "string (keyword)", // Typ (z.B. 'project') "type": "string (keyword)", // Typ (z.B. 'project')
"retriever_weight": "float", // Numerische Wichtigkeit (0.0-1.0) "retriever_weight": "float", // Numerische Wichtigkeit (0.0-1.0)
"chunk_profile": "string", // Genutztes Profil (z.B. 'sliding_smart_edges') "chunk_profile": "string", // Genutztes Profil (z.B. 'long')
"edge_defaults": ["string"], // Liste der aktiven Default-Kanten "edge_defaults": ["string"], // Liste der aktiven Default-Kanten
"tags": ["string"], // Liste von Tags "tags": ["string"], // Liste von Tags
"created": "string (iso-date)", // Erstellungsdatum "created": "string (iso-date)", // Erstellungsdatum
@ -84,7 +78,6 @@ Diese sind die Felder, die effektiv in Qdrant gespeichert werden.
"window": "string (text)", // Text + Overlap (für Embedding) "window": "string (text)", // Text + Overlap (für Embedding)
"ord": "integer", // Laufende Nummer (1..N) "ord": "integer", // Laufende Nummer (1..N)
"retriever_weight": "float", // Kopie aus Note (für Query-Speed) "retriever_weight": "float", // Kopie aus Note (für Query-Speed)
"chunk_profile": "string", // Vererbt von Note
"neighbors_prev": ["string"], // ID des Vorgängers "neighbors_prev": ["string"], // ID des Vorgängers
"neighbors_next": ["string"] // ID des Nachfolgers "neighbors_next": ["string"] // ID des Nachfolgers
} }
@ -99,8 +92,7 @@ Diese sind die Felder, die effektiv in Qdrant gespeichert werden.
"scope": "string (keyword)", // Immer 'chunk' "scope": "string (keyword)", // Immer 'chunk'
"rule_id": "string (keyword)", // Traceability: 'inline:rel', 'explicit:wikilink' "rule_id": "string (keyword)", // Traceability: 'inline:rel', 'explicit:wikilink'
"confidence": "float", // 0.0 - 1.0 "confidence": "float", // 0.0 - 1.0
"note_id": "string (keyword)", // Owner Note ID "note_id": "string (keyword)" // Owner Note ID
"provenance": "keyword" // 'explicit', 'rule', 'smart' (NEU)
} }
--- ---
@ -123,8 +115,7 @@ Diese Variablen steuern das Verhalten der Skripte und Container.
| `MINDNET_EMBEDDING_MODEL` | `nomic-embed-text` | **NEU:** Name des Vektor-Modells. | | `MINDNET_EMBEDDING_MODEL` | `nomic-embed-text` | **NEU:** Name des Vektor-Modells. |
| `MINDNET_OLLAMA_URL` | `http://127.0.0.1:11434`| URL zum LLM-Server (Neu in v2.2). | | `MINDNET_OLLAMA_URL` | `http://127.0.0.1:11434`| URL zum LLM-Server (Neu in v2.2). |
| `MINDNET_LLM_TIMEOUT` | `300.0` | Timeout für Ollama (Erhöht für CPU-Inference). | | `MINDNET_LLM_TIMEOUT` | `300.0` | Timeout für Ollama (Erhöht für CPU-Inference). |
| `MINDNET_API_TIMEOUT` | `300.0` | **NEU:** Frontend Timeout (Erhöht für Smart Edges). | | `MINDNET_API_TIMEOUT` | `60.0` | **NEU:** Frontend Timeout (Streamlit). |
| `MINDNET_LLM_BACKGROUND_LIMIT`| `2` | **NEU:** Max. parallele Import-Tasks (Traffic Control). |
| `MINDNET_VAULT_ROOT` | `./vault` | **NEU:** Pfad für Write-Back Operationen. | | `MINDNET_VAULT_ROOT` | `./vault` | **NEU:** Pfad für Write-Back Operationen. |
| `MINDNET_HASH_COMPARE` | `Body` | Vergleichsmodus für Import (`Body`, `Frontmatter`, `Full`). | | `MINDNET_HASH_COMPARE` | `Body` | Vergleichsmodus für Import (`Body`, `Frontmatter`, `Full`). |
| `MINDNET_HASH_SOURCE` | `parsed` | Quelle für Hash (`parsed`, `raw`, `file`). | | `MINDNET_HASH_SOURCE` | `parsed` | Quelle für Hash (`parsed`, `raw`, `file`). |
@ -138,19 +129,16 @@ Diese Variablen steuern das Verhalten der Skripte und Container.
* **Decision Engine:** Komponente, die den Intent prüft und Strategien wählt (WP06). * **Decision Engine:** Komponente, die den Intent prüft und Strategien wählt (WP06).
* **Draft Editor:** Web-Komponente zur Bearbeitung generierter Notizen (WP10a). * **Draft Editor:** Web-Komponente zur Bearbeitung generierter Notizen (WP10a).
* **Explanation Layer:** Komponente, die Scores und Graphen als Begründung liefert. * **Explanation Layer:** Komponente, die Scores und Graphen als Begründung liefert.
* **Healing Parser:** UI-Funktion, die defektes YAML von LLMs repariert (v2.5).
* **Hybrid Router:** Kombination aus Keyword-Matching und LLM-Klassifizierung für Intents. * **Hybrid Router:** Kombination aus Keyword-Matching und LLM-Klassifizierung für Intents.
* **Matrix Logic:** Regelwerk, das Kanten-Typen basierend auf Quell- und Ziel-Typ bestimmt. * **Matrix Logic:** Regelwerk, das Kanten-Typen basierend auf Quell- und Ziel-Typ bestimmt.
* **Nomic:** Das neue, hochpräzise Embedding-Modell (768 Dim). * **Nomic:** Das neue, hochpräzise Embedding-Modell (768 Dim).
* **One-Shot Extractor:** LLM-Strategie zur sofortigen Generierung von Drafts ohne Rückfragen (WP07). * **One-Shot Extractor:** LLM-Strategie zur sofortigen Generierung von Drafts ohne Rückfragen (WP07).
* **RAG (Retrieval Augmented Generation):** Kombination aus Suche und Text-Generierung. * **RAG (Retrieval Augmented Generation):** Kombination aus Suche und Text-Generierung.
* **Resurrection Pattern:** UI-Technik, um Eingaben bei Tab-Wechseln zu erhalten. * **Resurrection Pattern:** UI-Technik, um Eingaben bei Tab-Wechseln zu erhalten.
* **Smart Edge Allocation:** LLM-basierte Prüfung, ob ein Link zu einem Chunk passt (WP15).
* **Traffic Control:** Priorisierung von Chat-Anfragen ("Realtime") vor Import-Last ("Background") im LLM-Service (v2.6).
--- ---
## Anhang F: Workpackage Status (v2.6.0) ## Anhang F: Workpackage Status (v2.4.0)
Aktueller Implementierungsstand der Module. Aktueller Implementierungsstand der Module.
@ -163,12 +151,9 @@ Aktueller Implementierungsstand der Module.
| **WP04b**| Explanation Layer | 🟢 Live | API liefert Reasons & Breakdown. | | **WP04b**| Explanation Layer | 🟢 Live | API liefert Reasons & Breakdown. |
| **WP04c**| Feedback Loop | 🟢 Live | Logging (JSONL) & Traceability aktiv. | | **WP04c**| Feedback Loop | 🟢 Live | Logging (JSONL) & Traceability aktiv. |
| **WP05** | Persönlichkeit / Chat | 🟢 Live | RAG-Integration mit Context Enrichment. | | **WP05** | Persönlichkeit / Chat | 🟢 Live | RAG-Integration mit Context Enrichment. |
| **WP06** | Decision Engine | 🟢 Live | Hybrid Router v5, Strategic Retrieval. | | **WP06** | Decision Engine | 🟢 Live | Intent-Router & Strategic Retrieval. |
| **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** | | **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** |
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. | | **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). | | **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI & Healing Parser.** | | **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic, Matrix.** | | **WP11** | Backend Intelligence | 🟢 Live | **Async Core, Nomic, Matrix.** |
| **WP15** | Smart Edge Allocation | 🟢 Live | **LLM-Filter & Traffic Control.** |
| **WP16** | Auto-Discovery | 🟡 Geplant | UX & Retrieval Tuning. |
| **WP17** | Conversational Memory | 🟡 Geplant | Dialog-Gedächtnis. |

View File

@ -1,6 +1,6 @@
# Mindnet v2.4 Entwickler-Workflow # Mindnet v2.4 Entwickler-Workflow
**Datei:** `docs/DEV_WORKFLOW.md` **Datei:** `docs/DEV_WORKFLOW.md`
**Stand:** 2025-12-12 (Aktualisiert: v2.6 mit Traffic Control) **Stand:** 2025-12-11 (Aktualisiert: Inkl. Async Intelligence & Nomic)
Dieses Handbuch beschreibt den Entwicklungszyklus zwischen **Windows PC** (IDE), **Raspberry Pi** (Gitea) und **Beelink** (Runtime/Server). Dieses Handbuch beschreibt den Entwicklungszyklus zwischen **Windows PC** (IDE), **Raspberry Pi** (Gitea) und **Beelink** (Runtime/Server).
@ -35,14 +35,14 @@ Hier erstellst du die neue Funktion in einer sicheren Umgebung.
2. **Branch erstellen:** 2. **Branch erstellen:**
* Klicke wieder unten links auf `main`. * Klicke wieder unten links auf `main`.
* Wähle `+ Create new branch...`. * Wähle `+ Create new branch...`.
* Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp15-traffic-control`). * Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp11-async-fix`).
* Drücke **Enter**. * Drücke **Enter**.
3. **Sicherheits-Check:** 3. **Sicherheits-Check:**
* Steht unten links jetzt dein Feature-Branch? **Nur dann darfst du Code ändern!** * Steht unten links jetzt dein Feature-Branch? **Nur dann darfst du Code ändern!**
4. **Coden:** 4. **Coden:**
* Nimm deine Änderungen vor (z.B. neue Schemas in `types.yaml` oder Async-Logik in `ingestion.py`). * Nimm deine Änderungen vor (z.B. neue Schemas in `decision_engine.yaml` oder Async-Logik in `ingestion.py`).
5. **Sichern & Hochladen:** 5. **Sichern & Hochladen:**
* **Source Control** Icon (Gabel-Symbol) -> Nachricht eingeben -> **Commit**. * **Source Control** Icon (Gabel-Symbol) -> Nachricht eingeben -> **Commit**.
@ -64,27 +64,15 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft.
```bash ```bash
git fetch git fetch
# Tipp: 'git branch -r' zeigt alle verfügbaren Branches an # Tipp: 'git branch -r' zeigt alle verfügbaren Branches an
git checkout feature/wp15-traffic-control git checkout feature/wp11-async-fix
git pull git pull
``` ```
4. **Umgebung vorbereiten (WICHTIG für v2.6):** 4. **Umgebung vorbereiten (WICHTIG für v2.4):**
Prüfe deine `.env` Datei. Sie benötigt jetzt Einträge für die Traffic Control.
```bash
nano .env
```
**Ergänze/Prüfe:**
```ini
# Traffic Control (Neu in v2.6)
MINDNET_LLM_BACKGROUND_LIMIT=2 # Max. parallele Import-Tasks
MINDNET_API_TIMEOUT=300.0 # Frontend wartet länger
MINDNET_EMBEDDING_MODEL="nomic-embed-text"
```
**Dependencies aktualisieren:**
```bash ```bash
source .venv/bin/activate source .venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt # HTTPX usw.
# Sicherstellen, dass das neue Embedding-Modell da ist:
ollama pull nomic-embed-text ollama pull nomic-embed-text
``` ```
@ -114,15 +102,18 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft.
uvicorn app.main:app --host 0.0.0.0 --port 8002 --env-file .env uvicorn app.main:app --host 0.0.0.0 --port 8002 --env-file .env
``` ```
6. **Validieren (Smoke Tests v2.6):** 6. **Validieren (Smoke Tests):**
* **Test A: Last-Test (Traffic Control):** * **Browser:** Öffne `http://<IP>:8502` um die UI zu testen (Intent Badge prüfen!).
1. Starte einen Import im Terminal: `python3 -m scripts.import_markdown ...` * **CLI:** Führe Testskripte in einem **zweiten Terminal** aus:
2. Öffne **gleichzeitig** `http://<IP>:8502` im Browser.
3. Stelle eine Chat-Frage ("Was ist Mindnet?").
4. **Erwartung:** Der Chat antwortet sofort (Realtime Lane), während der Import im Hintergrund weiterläuft (Background Lane).
* **Test B: API Check** **Test A: Intelligence / Aliases (Neu in WP11)**
```bash
python debug_analysis.py
# Erwartung: "✅ ALIAS GEFUNDEN"
```
**Test B: API Check**
```bash ```bash
curl -X POST "http://localhost:8002/ingest/analyze" -d '{"text": "mindnet", "type": "journal"}' curl -X POST "http://localhost:8002/ingest/analyze" -d '{"text": "mindnet", "type": "journal"}'
``` ```
@ -156,9 +147,6 @@ Jetzt bringen wir die Änderung in das Live-System (Port 8001 / 8501).
pip install -r requirements.txt pip install -r requirements.txt
ollama pull nomic-embed-text ollama pull nomic-embed-text
# .env prüfen (Traffic Control)
# Ggf. MINDNET_LLM_BACKGROUND_LIMIT=2 hinzufügen
# Falls sich die Vektor-Dimension geändert hat (v2.4 Upgrade): # Falls sich die Vektor-Dimension geändert hat (v2.4 Upgrade):
# python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes # python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes
# python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force # python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
@ -180,7 +168,7 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch.
cd ~/mindnet_dev cd ~/mindnet_dev
git checkout main git checkout main
git pull git pull
git branch -d feature/wp15-traffic-control git branch -d feature/wp11-async-fix
``` ```
3. **VS Code:** 3. **VS Code:**
* Auf `main` wechseln. * Auf `main` wechseln.
@ -202,15 +190,15 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch.
## 4. Troubleshooting ## 4. Troubleshooting
**"Read timed out" im Frontend** **"Vector dimension error: expected 768, got 384"**
* **Ursache:** Backend braucht für Smart Edges länger als 60s. * **Ursache:** Du hast `nomic-embed-text` (768) aktiviert, aber die DB ist noch alt (384).
* **Lösung:** `MINDNET_API_TIMEOUT=300.0` in `.env` setzen und Services neustarten. * **Lösung:** `scripts.reset_qdrant` ausführen und neu importieren.
**Import ist extrem langsam** **"Read timed out (300s)" / 500 Error beim Interview**
* **Ursache:** Smart Edges analysieren jeden Chunk mit LLM. * **Ursache:** Das LLM (Ollama) braucht für den One-Shot Draft länger als das Timeout erlaubt.
* **Lösung:** * **Lösung:**
* Akzeptieren (Qualität vor Speed). 1. Erhöhe in `.env` den Wert: `MINDNET_LLM_TIMEOUT=300.0`.
* Oder temporär in `config/types.yaml`: `enable_smart_edge_allocation: false`. 2. Starte die Server neu.
**"UnicodeDecodeError in .env"** **"UnicodeDecodeError in .env"**
* **Ursache:** Umlaute oder Sonderzeichen in der `.env` Datei. * **Ursache:** Umlaute oder Sonderzeichen in der `.env` Datei.

View File

@ -1,7 +1,7 @@
# Mindnet v2.4 Developer Guide # Mindnet v2.4 Developer Guide
**Datei:** `docs/mindnet_developer_guide_v2.6.md` **Datei:** `docs/mindnet_developer_guide_v2.4.md`
**Stand:** 2025-12-12 **Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. Async Core, Nomic, Traffic Control & Frontend State) **Status:** **FINAL** (Inkl. Async Core, Nomic & Frontend State)
**Quellen:** `mindnet_technical_architecture.md`, `Handbuch.md`, `DEV_WORKFLOW.md`. **Quellen:** `mindnet_technical_architecture.md`, `Handbuch.md`, `DEV_WORKFLOW.md`.
> **Zielgruppe:** Entwickler:innen. > **Zielgruppe:** Entwickler:innen.
@ -9,7 +9,7 @@
--- ---
- [Mindnet v2.4 Developer Guide](#mindnet-v24--developer-guide) - [Mindnet v2.4 Developer Guide](#mindnet-v24--developer-guide)
- [1. Projektstruktur (Post-WP15)](#1-projektstruktur-post-wp15) - [1. Projektstruktur (Post-WP10)](#1-projektstruktur-post-wp10)
- [2. Lokales Setup (Development)](#2-lokales-setup-development) - [2. Lokales Setup (Development)](#2-lokales-setup-development)
- [2.1 Voraussetzungen](#21-voraussetzungen) - [2.1 Voraussetzungen](#21-voraussetzungen)
- [2.2 Installation](#22-installation) - [2.2 Installation](#22-installation)
@ -21,7 +21,6 @@
- [3.3 Der Retriever (`app.core.retriever`)](#33-der-retriever-appcoreretriever) - [3.3 Der Retriever (`app.core.retriever`)](#33-der-retriever-appcoreretriever)
- [3.4 Das Frontend (`app.frontend.ui`)](#34-das-frontend-appfrontendui) - [3.4 Das Frontend (`app.frontend.ui`)](#34-das-frontend-appfrontendui)
- [3.5 Embedding Service (`app.services.embeddings_client`)](#35-embedding-service-appservicesembeddings_client) - [3.5 Embedding Service (`app.services.embeddings_client`)](#35-embedding-service-appservicesembeddings_client)
- [3.6 Traffic Control (`app.services.llm_service`)](#36-traffic-control-appservicesllm_service)
- [4. Tests \& Debugging](#4-tests--debugging) - [4. Tests \& Debugging](#4-tests--debugging)
- [4.1 Unit Tests (Pytest)](#41-unit-tests-pytest) - [4.1 Unit Tests (Pytest)](#41-unit-tests-pytest)
- [4.2 Integration / Pipeline Tests](#42-integration--pipeline-tests) - [4.2 Integration / Pipeline Tests](#42-integration--pipeline-tests)
@ -35,15 +34,15 @@
--- ---
## 1. Projektstruktur (Post-WP15) ## 1. Projektstruktur (Post-WP10)
Der Code ist modular in `app` (Logik), `scripts` (CLI) und `config` (Steuerung) getrennt. Der Code ist modular in `app` (Logik), `scripts` (CLI) und `config` (Steuerung) getrennt.
mindnet/ mindnet/
├── app/ ├── app/
│ ├── core/ # Kernlogik │ ├── core/ # Kernlogik
│ │ ├── ingestion.py # NEU: Async Ingestion Service mit Change Detection │ │ ├── ingestion.py # NEU: Async Ingestion Service (WP11)
│ │ ├── chunker.py # Smart Chunker Orchestrator │ │ ├── chunker.py # Text-Zerlegung
│ │ ├── derive_edges.py # Edge-Erzeugung (WP03 Logik) │ │ ├── derive_edges.py # Edge-Erzeugung (WP03 Logik)
│ │ ├── retriever.py # Scoring & Hybrid Search │ │ ├── retriever.py # Scoring & Hybrid Search
│ │ ├── qdrant.py # DB-Verbindung │ │ ├── qdrant.py # DB-Verbindung
@ -53,22 +52,21 @@ Der Code ist modular in `app` (Logik), `scripts` (CLI) und `config` (Steuerung)
│ ├── routers/ # FastAPI Endpoints │ ├── routers/ # FastAPI Endpoints
│ │ ├── query.py # Suche │ │ ├── query.py # Suche
│ │ ├── ingest.py # NEU: Save/Analyze (WP11) │ │ ├── ingest.py # NEU: Save/Analyze (WP11)
│ │ ├── chat.py # Hybrid Router v5 & Interview Logic │ │ ├── chat.py # Hybrid Router & Interview Logic (WP06/WP07)
│ │ ├── feedback.py # Feedback (WP04c) │ │ ├── feedback.py # Feedback (WP04c)
│ │ └── ... │ │ └── ...
│ ├── services/ # Interne & Externe Dienste │ ├── services/ # Interne & Externe Dienste
│ │ ├── llm_service.py # Ollama Client mit Traffic Control │ │ ├── llm_service.py # Ollama Client (Mit Timeout & Raw-Mode)
│ │ ├── semantic_analyzer.py# NEU: LLM-Filter für Edges (WP15) │ │ ├── embeddings_client.py# NEU: Async Embeddings (HTTPX)
│ │ ├── embeddings_client.py# Async Embeddings (HTTPX)
│ │ ├── feedback_service.py # Logging (JSONL Writer) │ │ ├── feedback_service.py # Logging (JSONL Writer)
│ │ └── discovery.py # NEU: Intelligence Logic (WP11) │ │ └── discovery.py # NEU: Intelligence Logic (WP11)
│ ├── frontend/ # NEU (WP10) │ ├── frontend/ # NEU (WP10)
│ │ └── ui.py # Streamlit Application inkl. Healing Parser │ │ └── ui.py # Streamlit Application inkl. Draft-Editor
│ └── main.py # Entrypoint der API │ └── main.py # Entrypoint der API
├── config/ # YAML-Konfigurationen (Single Source of Truth) ├── config/ # YAML-Konfigurationen (Single Source of Truth)
│ ├── types.yaml # Import-Regeln & Smart-Edge Config │ ├── types.yaml # Import-Regeln
│ ├── prompts.yaml # LLM Prompts & Interview Templates │ ├── prompts.yaml # LLM Prompts & Interview Templates (WP06/07)
│ ├── decision_engine.yaml # Router-Strategien (Actions only) │ ├── decision_engine.yaml # Router-Strategien & Schemas (WP06/07)
│ └── retriever.yaml # Scoring-Regeln & Kantengewichte │ └── retriever.yaml # Scoring-Regeln & Kantengewichte
├── data/ ├── data/
│ └── logs/ # Lokale Logs (search_history.jsonl, feedback.jsonl) │ └── logs/ # Lokale Logs (search_history.jsonl, feedback.jsonl)
@ -123,13 +121,12 @@ Erstelle eine `.env` Datei im Root-Verzeichnis.
MINDNET_LLM_MODEL="phi3:mini" MINDNET_LLM_MODEL="phi3:mini"
MINDNET_EMBEDDING_MODEL="nomic-embed-text" # NEU MINDNET_EMBEDDING_MODEL="nomic-embed-text" # NEU
MINDNET_OLLAMA_URL="http://127.0.0.1:11434" MINDNET_OLLAMA_URL="http://127.0.0.1:11434"
MINDNET_LLM_TIMEOUT=300.0 # Timeout für CPU-Inference Cold-Starts MINDNET_LLM_TIMEOUT=300.0
MINDNET_DECISION_CONFIG="./config/decision_engine.yaml" MINDNET_DECISION_CONFIG="./config/decision_engine.yaml"
MINDNET_LLM_BACKGROUND_LIMIT=2 # NEU: Limit für parallele Import-Tasks
# Frontend Settings (WP10) # Frontend Settings (WP10)
MINDNET_API_URL="http://localhost:8002" MINDNET_API_URL="http://localhost:8002"
MINDNET_API_TIMEOUT=300.0 # Erhöht wegen Smart Edge Berechnung MINDNET_API_TIMEOUT=60.0
# Import-Strategie # Import-Strategie
MINDNET_HASH_COMPARE="Body" MINDNET_HASH_COMPARE="Body"
@ -157,15 +154,15 @@ Wir entwickeln mit zwei Services. Du kannst sie manuell in zwei Terminals starte
### 3.1 Der Importer (`scripts.import_markdown`) ### 3.1 Der Importer (`scripts.import_markdown`)
Dies ist das komplexeste Modul. Dies ist das komplexeste Modul.
* **Einstieg:** `scripts/import_markdown.py` -> `main_async()`. * **Einstieg:** `scripts/import_markdown.py` -> `main_async()`.
* **Smart Edges:** Nutzt `app.core.chunker` und `app.services.semantic_analyzer` zur Kanten-Filterung. * **Async & Semaphore:** Das Skript nutzt nun `asyncio` und eine Semaphore (Limit: 5), um parallele Embeddings zu erzeugen, ohne Ollama zu überlasten.
* **Idempotenz:** Der Importer muss mehrfach laufen können, ohne Duplikate zu erzeugen. Wir nutzen deterministische IDs (UUIDv5). * **Idempotenz:** Der Importer muss mehrfach laufen können, ohne Duplikate zu erzeugen. Wir nutzen deterministische IDs (UUIDv5).
* **Robustheit:** In `ingestion.py` sind Mechanismen wie Change Detection und Robust File I/O (fsync) implementiert. * **Debugging:** Nutze `--dry-run` oder `scripts/payload_dryrun.py`.
### 3.2 Der Hybrid Router (`app.routers.chat`) ### 3.2 Der Hybrid Router (`app.routers.chat`)
Hier liegt die Logik für Intent Detection (WP06) und Interview-Modus (WP07). Hier liegt die Logik für Intent Detection (WP06) und Interview-Modus (WP07).
* **Question Detection:** Prüft zuerst, ob der Input eine Frage ist. Falls ja -> RAG. * **Logic:** `_classify_intent` prüft zuerst Keywords (Fast Path) und fällt auf `llm_service.generate_raw_response` zurück (Slow Path), wenn konfiguriert.
* **Keyword Match:** Prüft Keywords in `decision_engine.yaml` und `types.yaml`. * **One-Shot:** Wenn Intent `INTERVIEW` erkannt wird, wird **kein Retrieval** ausgeführt. Stattdessen wird ein Draft generiert.
* **Priority:** Ruft `llm_service` mit `priority="realtime"` auf. * **Erweiterung:** Um neue Intents hinzuzufügen, editiere nur die YAML, nicht den Python-Code (Late Binding).
### 3.3 Der Retriever (`app.core.retriever`) ### 3.3 Der Retriever (`app.core.retriever`)
Hier passiert das Scoring. Hier passiert das Scoring.
@ -175,20 +172,14 @@ Hier passiert das Scoring.
### 3.4 Das Frontend (`app.frontend.ui`) ### 3.4 Das Frontend (`app.frontend.ui`)
Eine Streamlit-App (WP10). Eine Streamlit-App (WP10).
* **Resurrection Pattern:** Das UI nutzt ein spezielles State-Management, um Eingaben bei Tab-Wechseln (Chat <-> Editor) zu erhalten. Widgets synchronisieren sich mit `st.session_state`. * **Resurrection Pattern:** Das UI nutzt ein spezielles State-Management, um Eingaben bei Tab-Wechseln (Chat <-> Editor) zu erhalten. Widgets synchronisieren sich mit `st.session_state`.
* **Healing Parser:** Die Funktion `parse_markdown_draft` repariert defekte YAML-Frontmatter (fehlendes `---`) automatisch. * **Draft Editor:** Enthält einen YAML-Sanitizer (`normalize_meta_and_body`), der sicherstellt, dass LLM-Halluzinationen im Frontmatter nicht das File zerstören.
* **Logik:** Ruft `/chat`, `/feedback` und `/ingest/analyze` Endpoints der API auf. * **Logik:** Ruft `/chat` und `/feedback` und `/ingest/analyze` Endpoints der API auf.
### 3.5 Embedding Service (`app.services.embeddings_client`) ### 3.5 Embedding Service (`app.services.embeddings_client`)
**Neu in v2.4:** **Neu in v2.4:**
* Nutzt `httpx.AsyncClient` für non-blocking Calls an Ollama (Primary Mode). * Nutzt `httpx.AsyncClient` für non-blocking Calls an Ollama.
* Besitzt einen **Fallback** auf `requests` (Synchron), falls Legacy-Skripte ihn nutzen.
* Unterstützt dediziertes Embedding-Modell (`nomic-embed-text`) getrennt vom Chat-Modell. * Unterstützt dediziertes Embedding-Modell (`nomic-embed-text`) getrennt vom Chat-Modell.
* Enthält Legacy-Funktion `embed_text` für synchrone Skripte.
### 3.6 Traffic Control (`app.services.llm_service`)
**Neu in v2.6 (Version 2.8.0):**
* Stellt sicher, dass Batch-Prozesse (Import) den Live-Chat nicht ausbremsen.
* **Methode:** `generate_raw_response(..., priority="background")` aktiviert eine Semaphore.
* **Limit:** Konfigurierbar über `MINDNET_LLM_BACKGROUND_LIMIT` (Default: 2) in der `.env`.
--- ---
@ -241,10 +232,9 @@ Mindnet lernt nicht durch Training (Fine-Tuning), sondern durch **Konfiguration*
Definiere die "Physik" des Typs (Import-Regeln und Basis-Wichtigkeit). Definiere die "Physik" des Typs (Import-Regeln und Basis-Wichtigkeit).
risk: risk:
chunk_profile: sliding_short # Risiken sind oft kurze Statements chunk_profile: short # Risiken sind oft kurze Statements
retriever_weight: 0.90 # Sehr wichtig retriever_weight: 0.90 # Sehr wichtig, fast so hoch wie Decisions
edge_defaults: ["blocks"] # Automatische Kante zu verlinkten Projekten edge_defaults: ["blocks"] # Automatische Kante zu verlinkten Projekten
detection_keywords: ["gefahr", "risiko"]
**2. Strategie-Ebene (`config/decision_engine.yaml`)** **2. Strategie-Ebene (`config/decision_engine.yaml`)**
Damit dieser Typ aktiv geladen wird, musst du ihn einer Strategie zuordnen. Damit dieser Typ aktiv geladen wird, musst du ihn einer Strategie zuordnen.
@ -272,17 +262,14 @@ Konfiguriere `edge_weights`, wenn Kausalität wichtiger ist als Ähnlichkeit.
Wenn Mindnet neue Fragen stellen soll: Wenn Mindnet neue Fragen stellen soll:
**1. Schema erweitern (`config/types.yaml`)** **1. Schema erweitern (`config/decision_engine.yaml`)**
Füge das Feld in die Liste ein (Neu: Schemas liegen jetzt hier). Füge das Feld in die Liste ein.
project: project:
schema: fields: ["Titel", "Ziel", "Budget"] # <--- Budget neu
- "Titel"
- "Ziel"
- "Budget (Neu)"
**2. Keine Code-Änderung nötig** **2. Keine Code-Änderung nötig**
Der `One-Shot Extractor` (Prompt Template) liest diese Liste dynamisch. Der `One-Shot Extractor` (Prompt Template) liest diese Liste dynamisch und weist das LLM an, das Budget zu extrahieren oder `[TODO]` zu setzen.
### Fazit ### Fazit
* **Vault:** Liefert das Wissen. * **Vault:** Liefert das Wissen.

View File

@ -1,9 +1,9 @@
# Mindnet v2.4 Fachliche Architektur # Mindnet v2.4 Fachliche Architektur
**Datei:** `docs/mindnet_functional_architecture_v2.6.md` **Datei:** `docs/mindnet_functional_architecture_v2.4.md`
**Stand:** 2025-12-12 **Stand:** 2025-12-11
**Status:** **FINAL** (Integrierter Stand WP01WP15: Smart Edges & Traffic Control) **Status:** **FINAL** (Integrierter Stand WP01WP11: Async Intelligence)
> Dieses Dokument beschreibt **was** Mindnet fachlich tut und **warum** mit Fokus auf die Erzeugung und Nutzung von **Smart Edges** (Kanten), die Logik des Retrievers und den **RAG-Chat** (Decision Engine, Interview-Modus & Persönlichkeit). > Dieses Dokument beschreibt **was** Mindnet fachlich tut und **warum** mit Fokus auf die Erzeugung und Nutzung von **Edges** (Kanten), die Logik des Retrievers und den **RAG-Chat** (Decision Engine, Interview-Modus & Persönlichkeit).
--- ---
<details> <details>
@ -19,8 +19,7 @@
- [2.1 Struktur-Kanten (Das Skelett)](#21-struktur-kanten-das-skelett) - [2.1 Struktur-Kanten (Das Skelett)](#21-struktur-kanten-das-skelett)
- [2.2 Inhalts-Kanten (explizit)](#22-inhalts-kanten-explizit) - [2.2 Inhalts-Kanten (explizit)](#22-inhalts-kanten-explizit)
- [2.3 Typ-basierte Default-Kanten (Regelbasiert)](#23-typ-basierte-default-kanten-regelbasiert) - [2.3 Typ-basierte Default-Kanten (Regelbasiert)](#23-typ-basierte-default-kanten-regelbasiert)
- [2.4 Smart Edge Allocation (LLM-gefiltert) Neu in v2.6](#24-smart-edge-allocation-llm-gefiltert--neu-in-v26) - [2.4 Matrix-Logik (Kontextsensitive Kanten) Neu in v2.4](#24-matrix-logik-kontextsensitive-kanten--neu-in-v24)
- [2.5 Matrix-Logik (Kontextsensitive Kanten)](#25-matrix-logik-kontextsensitive-kanten)
- [3) Edge-Payload Felder \& Semantik](#3-edge-payload--felder--semantik) - [3) Edge-Payload Felder \& Semantik](#3-edge-payload--felder--semantik)
- [4) Typ-Registry (`config/types.yaml`)](#4-typ-registry-configtypesyaml) - [4) Typ-Registry (`config/types.yaml`)](#4-typ-registry-configtypesyaml)
- [4.1 Zweck](#41-zweck) - [4.1 Zweck](#41-zweck)
@ -28,15 +27,13 @@
- [5) Der Retriever (Funktionaler Layer)](#5-der-retriever-funktionaler-layer) - [5) Der Retriever (Funktionaler Layer)](#5-der-retriever-funktionaler-layer)
- [5.1 Scoring-Modell](#51-scoring-modell) - [5.1 Scoring-Modell](#51-scoring-modell)
- [5.2 Erklärbarkeit (Explainability) WP04b](#52-erklärbarkeit-explainability--wp04b) - [5.2 Erklärbarkeit (Explainability) WP04b](#52-erklärbarkeit-explainability--wp04b)
- [5.3 Graph-Expansion](#53-graph-expansion) - [6) Context Intelligence \& Intent Router (WP06WP11)](#6-context-intelligence--intent-router-wp06wp11)
- [6) Context Intelligence \& Intent Router (WP06WP15)](#6-context-intelligence--intent-router-wp06wp15)
- [6.1 Das Problem: Statische vs. Dynamische Antworten](#61-das-problem-statische-vs-dynamische-antworten) - [6.1 Das Problem: Statische vs. Dynamische Antworten](#61-das-problem-statische-vs-dynamische-antworten)
- [6.2 Der Hybrid Router v5 (Action vs. Question)](#62-der-hybrid-router-v5-action-vs-question) - [6.2 Der Intent-Router (Keyword \& Semantik)](#62-der-intent-router-keyword--semantik)
- [6.3 Traffic Control (Realtime vs. Background)](#63-traffic-control-realtime-vs-background) - [6.3 Strategic Retrieval (Injektion von Werten)](#63-strategic-retrieval-injektion-von-werten)
- [6.4 Strategic Retrieval (Injektion von Werten)](#64-strategic-retrieval-injektion-von-werten) - [6.4 Reasoning (Das Gewissen)](#64-reasoning-das-gewissen)
- [6.5 Reasoning (Das Gewissen)](#65-reasoning-das-gewissen) - [6.5 Der Interview-Modus (One-Shot Extraction)](#65-der-interview-modus-one-shot-extraction)
- [6.6 Der Interview-Modus (One-Shot Extraction)](#66-der-interview-modus-one-shot-extraction) - [6.6 Active Intelligence (Link Suggestions) Neu in v2.4](#66-active-intelligence-link-suggestions--neu-in-v24)
- [6.7 Active Intelligence (Link Suggestions)](#67-active-intelligence-link-suggestions)
- [7) Future Concepts: The Empathic Digital Twin (Ausblick)](#7-future-concepts-the-empathic-digital-twin-ausblick) - [7) Future Concepts: The Empathic Digital Twin (Ausblick)](#7-future-concepts-the-empathic-digital-twin-ausblick)
- [7.1 Antizipation durch Erfahrung](#71-antizipation-durch-erfahrung) - [7.1 Antizipation durch Erfahrung](#71-antizipation-durch-erfahrung)
- [7.2 Empathie \& "Ich"-Modus](#72-empathie--ich-modus) - [7.2 Empathie \& "Ich"-Modus](#72-empathie--ich-modus)
@ -52,7 +49,7 @@
- [13) Lösch-/Update-Garantien (Idempotenz)](#13-lösch-update-garantien-idempotenz) - [13) Lösch-/Update-Garantien (Idempotenz)](#13-lösch-update-garantien-idempotenz)
- [14) Beispiel Von Markdown zu Kanten](#14-beispiel--von-markdown-zu-kanten) - [14) Beispiel Von Markdown zu Kanten](#14-beispiel--von-markdown-zu-kanten)
- [15) Referenzen (Projektdateien \& Leitlinien)](#15-referenzen-projektdateien--leitlinien) - [15) Referenzen (Projektdateien \& Leitlinien)](#15-referenzen-projektdateien--leitlinien)
- [16) Workpackage Status (v2.6.0)](#16-workpackage-status-v260) - [16) Workpackage Status (v2.4.0)](#16-workpackage-status-v240)
</details> </details>
--- ---
@ -66,7 +63,7 @@ Die drei zentralen Artefakt-Sammlungen lauten:
- `mindnet_chunks` semantische Teilstücke einer Note (Fenster/„Chunks“) - `mindnet_chunks` semantische Teilstücke einer Note (Fenster/„Chunks“)
- `mindnet_edges` gerichtete Beziehungen zwischen Knoten (Chunks/Notes) - `mindnet_edges` gerichtete Beziehungen zwischen Knoten (Chunks/Notes)
Die Import-Pipeline (seit v2.6 mit **Traffic Control** und **Smart Edges**) erzeugt diese Artefakte **deterministisch** und **idempotent** (erneute Läufe überschreiben konsistent statt zu duplizieren). Die Import-Schritte sind: *parse → chunk → embed → smart-edge-allocation → upsert*. Die Import-Pipeline (seit v2.3.10 asynchron) erzeugt diese Artefakte **deterministisch** und **idempotent** (erneute Läufe überschreiben konsistent statt zu duplizieren). Die Import-Schritte sind: *parse → chunk → embed → edge → upsert*.
--- ---
@ -83,9 +80,9 @@ Die Import-Pipeline (seit v2.6 mit **Traffic Control** und **Smart Edges**) erze
- Jeder Chunk gehört **genau einer** Note. - Jeder Chunk gehört **genau einer** Note.
- Chunks bilden eine Sequenz (1…N) das ermöglicht *next/prev*. - Chunks bilden eine Sequenz (1…N) das ermöglicht *next/prev*.
- **Update v2.4:** Chunks werden jetzt durch das Modell `nomic-embed-text` in **768-dimensionale Vektoren** umgewandelt. Dies erlaubt eine deutlich höhere semantische Auflösung als frühere Modelle (384 Dim). - **Update v2.4:** Chunks werden jetzt durch das Modell `nomic-embed-text` in **768-dimensionale Vektoren** umgewandelt. Dies erlaubt eine deutlich höhere semantische Auflösung als frühere Modelle (384 Dim).
- **Update v2.6 (Smart Chunking):** Ein Chunk ist nicht mehr nur ein dummer Textblock, der alle Links der Mutter-Notiz erbt. Durch die **Smart Edge Allocation (WP15)** "weiß" jeder Chunk genau, welche Kanten inhaltlich zu ihm gehören (siehe 2.4). - **Neu in v2.2:** Alle Kanten entstehen ausschließlich zwischen Chunks (Scope="chunk"), nie zwischen Notes direkt. Notes dienen nur noch als Metadatencontainer.
> **Wichtig:** Chunking-Profile (short/medium/long/sliding_smart_edges) kommen aus `types.yaml` (per Note-Typ), können aber lokal überschrieben werden. Die effektiven Werte werden bei der Payload-Erzeugung bestimmt. > **Wichtig:** Chunking-Profile (short/medium/long) kommen aus `types.yaml` (per Note-Typ), können aber lokal überschrieben werden. Die effektiven Werte werden bei der Payload-Erzeugung bestimmt.
--- ---
@ -100,7 +97,7 @@ Edges kodieren Beziehungen. Sie sind **gerichtet** und werden in `mindnet_edges`
Diese Kanten entstehen immer, unabhängig von Inhalten. Diese Kanten entstehen immer, unabhängig von Inhalten.
### 2.2 Inhalts-Kanten (explizit) ### 2.2 Inhalts-Kanten (explizit)
Hier unterscheidet v2.6 präzise zwischen verschiedenen Quellen der Evidenz: Hier unterscheidet v2.2 präzise zwischen verschiedenen Quellen der Evidenz:
1. **Explizite Inline-Relationen (Höchste Priorität):** 1. **Explizite Inline-Relationen (Höchste Priorität):**
Im Fließtext notierte, semantisch qualifizierte Relationen. Im Fließtext notierte, semantisch qualifizierte Relationen.
@ -134,19 +131,7 @@ Regel: **Für jede gefundene explizite Referenz** (s. o.) werden **zusätzliche*
Beispiel: Ein *project* mit `edge_defaults=["depends_on"]` erzeugt zu *jedem* explizit referenzierten Ziel **zusätzlich** eine `depends_on`-Kante. Beispiel: Ein *project* mit `edge_defaults=["depends_on"]` erzeugt zu *jedem* explizit referenzierten Ziel **zusätzlich** eine `depends_on`-Kante.
Diese Kanten tragen *provenance=rule* und eine **rule_id** der Form `edge_defaults:{note_type}:{relation}` sowie eine geringere Confidence (~0.7). Diese Kanten tragen *provenance=rule* und eine **rule_id** der Form `edge_defaults:{note_type}:{relation}` sowie eine geringere Confidence (~0.7).
### 2.4 Smart Edge Allocation (LLM-gefiltert) Neu in v2.6 ### 2.4 Matrix-Logik (Kontextsensitive Kanten) Neu in v2.4
Mit **WP15** wurde ein intelligenter Filter eingeführt, um das Problem des "Broadcasting" zu lösen. Früher erbte jeder Chunk einer Notiz *alle* Links der Notiz, was zu unpräzisem Retrieval führte.
**Der neue Prozess:**
1. **Scan:** Das System sammelt alle expliziten Links der gesamten Notiz.
2. **Chunking:** Der Text wird in Abschnitte zerlegt.
3. **Analyse (LLM):** Ein lokales LLM (Semantic Analyzer) liest jeden Chunk einzeln.
4. **Entscheidung:** Das LLM entscheidet für jeden Link aus Schritt 1: *"Ist dieser Link im Kontext dieses spezifischen Textabschnitts relevant?"*
5. **Zuweisung:** Nur wenn das LLM zustimmt, wird die Kante am Chunk erstellt.
*Steuerung:* Dies ist pro Typ in der `types.yaml` konfigurierbar (`enable_smart_edge_allocation: true`).
### 2.5 Matrix-Logik (Kontextsensitive Kanten)
Mit WP-11 wurde eine Intelligenz eingeführt, die Kanten-Typen nicht nur anhand des Quell-Typs, sondern auch anhand des Ziel-Typs bestimmt ("Matrix"). Mit WP-11 wurde eine Intelligenz eingeführt, die Kanten-Typen nicht nur anhand des Quell-Typs, sondern auch anhand des Ziel-Typs bestimmt ("Matrix").
**Beispiel für `Source Type: experience`:** **Beispiel für `Source Type: experience`:**
@ -169,7 +154,7 @@ Jede Kante hat mindestens:
Erweiterte/abgeleitete Felder (WP03 Superset): Erweiterte/abgeleitete Felder (WP03 Superset):
- `provenance` `"explicit"` (Wikilink/Inline/Callout), `"rule"` (Typ-Defaults) oder neu `"smart"` (vom LLM validiert). - `provenance` `"explicit"` (Wikilink/Inline/Callout) oder `"rule"` (Typ-Defaults)
- `rule_id` maschinenlesbare Regelquelle (z.B. `inline:rel`, `edge_defaults:project:depends_on`) - `rule_id` maschinenlesbare Regelquelle (z.B. `inline:rel`, `edge_defaults:project:depends_on`)
- `confidence` 0.01.0; Heuristik zur Gewichtung im Scoring. - `confidence` 0.01.0; Heuristik zur Gewichtung im Scoring.
@ -183,24 +168,20 @@ Erweiterte/abgeleitete Felder (WP03 Superset):
- Steuert **Chunking-Profile** (`short|medium|long`) **pro Typ** - Steuert **Chunking-Profile** (`short|medium|long`) **pro Typ**
- Liefert **retriever_weight** (Note-/Chunk-Gewichtung im Ranking) **pro Typ** - Liefert **retriever_weight** (Note-/Chunk-Gewichtung im Ranking) **pro Typ**
- Definiert **edge_defaults** je Typ (s. o.) - Definiert **edge_defaults** je Typ (s. o.)
- **Neu in v2.6:** Steuert `enable_smart_edge_allocation` (LLM-Filter) und `detection_keywords` für den Hybrid Router.
Der Importer lädt die Registry aus `MINDNET_TYPES_FILE` oder nutzt Fallbacks. **Frontmatter-Overrides** für Profile werden in v2.2 weitgehend ignoriert zugunsten einer zentralen Governance in der YAML-Datei. Der Importer lädt die Registry aus `MINDNET_TYPES_FILE` oder nutzt Fallbacks. **Frontmatter-Overrides** für Profile werden in v2.2 weitgehend ignoriert zugunsten einer zentralen Governance in der YAML-Datei.
### 4.2 Beispiel (auslieferungsnah) ### 4.2 Beispiel (auslieferungsnah)
version: 2.6.0 version: 1.0
types: types:
concept: concept:
chunk_profile: medium chunk_profile: medium
edge_defaults: ["references", "related_to"] edge_defaults: ["references", "related_to"]
retriever_weight: 0.60 retriever_weight: 0.60
experience: project:
chunk_profile: sliding_smart_edges chunk_profile: long
enable_smart_edge_allocation: true # WP15: LLM prüft Kanten edge_defaults: ["references", "depends_on"]
detection_keywords: ["passiert", "erlebt", "situation"] # WP06: Router-Trigger retriever_weight: 0.97
schema: # WP07: Interview-Struktur
- "Situation (Was ist passiert?)"
- "Meine Reaktion (Was habe ich getan?)"
**Auflösung im Importer** **Auflösung im Importer**
- `effective_chunk_profile(note_type, registry)``"short|medium|long"|None` - `effective_chunk_profile(note_type, registry)``"short|medium|long"|None`
@ -239,12 +220,9 @@ Der Retriever ist keine Blackbox mehr. Er liefert auf Wunsch (`explain=True`) ei
Die API gibt diese Analysen als menschenlesbare Sätze (`reasons`) und als Datenstruktur (`score_breakdown`) zurück. Die API gibt diese Analysen als menschenlesbare Sätze (`reasons`) und als Datenstruktur (`score_breakdown`) zurück.
### 5.3 Graph-Expansion
Der Hybrid-Modus lädt dynamisch die Nachbarschaft der Top-K Vektor-Treffer ("Seeds") über `graph_adapter.expand`. Dies baut einen temporären `NetworkX`-artigen Graphen im Speicher (Klasse `Subgraph`), auf dem Boni berechnet werden.
--- ---
## 6) Context Intelligence & Intent Router (WP06WP15) ## 6) Context Intelligence & Intent Router (WP06WP11)
Seit WP06 agiert Mindnet nicht mehr statisch, sondern passt seine Suchstrategie dem **Intent** (der Absicht) des Nutzers an. Dies ist die Transformation vom reinen Wissens-Abrufer zum strategischen Partner. Seit WP06 agiert Mindnet nicht mehr statisch, sondern passt seine Suchstrategie dem **Intent** (der Absicht) des Nutzers an. Dies ist die Transformation vom reinen Wissens-Abrufer zum strategischen Partner.
@ -252,51 +230,40 @@ Seit WP06 agiert Mindnet nicht mehr statisch, sondern passt seine Suchstrategie
* **Früher (Pre-WP06):** Jede Frage ("Was ist X?" oder "Soll ich X?") wurde gleich behandelt -> Fakten-Retrieval. * **Früher (Pre-WP06):** Jede Frage ("Was ist X?" oder "Soll ich X?") wurde gleich behandelt -> Fakten-Retrieval.
* **Heute (WP06):** Das System erkennt, *was* der User will (Rat, Fakten oder Datenerfassung) und wechselt den Modus. * **Heute (WP06):** Das System erkennt, *was* der User will (Rat, Fakten oder Datenerfassung) und wechselt den Modus.
### 6.2 Der Hybrid Router v5 (Action vs. Question) ### 6.2 Der Intent-Router (Keyword & Semantik)
Der Router wurde in v2.6 (WP15) weiterentwickelt, um Fehlalarme zu vermeiden. Der Router prüft vor jeder Antwort die Absicht über konfigurierbare Strategien (`config/decision_engine.yaml`):
1. **Frage-Erkennung:** 1. **FACT:** Reine Wissensfrage ("Was ist Qdrant?"). → Standard RAG.
* Das System prüft zuerst: Enthält der Satz ein `?` oder typische W-Wörter (Wer, Wie, Was)? 2. **DECISION:** Frage nach Rat oder Strategie ("Soll ich Qdrant nutzen?"). → Aktiviert die Decision Engine.
* Wenn **JA** -> Gehe in den **RAG Modus** (Intent `FACT` oder `DECISION`). Interviews werden hier blockiert. 3. **EMPATHY:** Emotionale Zustände ("Ich bin gestresst"). → Aktiviert den empathischen Modus.
4. **INTERVIEW (WP07):** Wunsch, Wissen zu erfassen ("Neues Projekt anlegen"). → Aktiviert den Draft-Generator.
5. **CODING:** Technische Anfragen.
2. **Befehls-Erkennung (Fast Path):** ### 6.3 Strategic Retrieval (Injektion von Werten)
* Wenn **NEIN** (keine Frage), prüft das System auf Keywords in `types.yaml` ("Projekt", "Erfahrung").
* Treffer -> **INTERVIEW Modus** (Erfassen). Das Schema wird aus `types.yaml` geladen.
3. **LLM Fallback (Slow Path):**
* Wenn beides fehlschlägt, entscheidet das LLM über den Intent.
### 6.3 Traffic Control (Realtime vs. Background)
Um den Live-Betrieb (Chat) nicht durch den ressourcenintensiven Smart-Import (LLM-Analyse) zu gefährden, implementiert v2.6 ein **Traffic Control System** im `LLMService`.
* **Realtime Lane (Chat):** Anfragen aus dem Chat erhalten `priority="realtime"`. Sie umgehen alle Warteschlangen und werden sofort bearbeitet.
* **Background Lane (Import):** Anfragen zur Kantenanalyse erhalten `priority="background"`. Sie werden durch eine **Semaphore** gedrosselt (Standard: max 2 parallel), um die Hardware nicht zu überlasten.
### 6.4 Strategic Retrieval (Injektion von Werten)
Im Modus `DECISION` führt das System eine **zweite Suchstufe** aus. Es sucht nicht nur nach semantisch passenden Texten zur Frage, sondern erzwingt das Laden von strategischen Notizen wie: Im Modus `DECISION` führt das System eine **zweite Suchstufe** aus. Es sucht nicht nur nach semantisch passenden Texten zur Frage, sondern erzwingt das Laden von strategischen Notizen wie:
* **Values (`type: value`):** Moralische Werte (z.B. "Privacy First"). * **Values (`type: value`):** Moralische Werte (z.B. "Privacy First").
* **Principles (`type: principle`):** Handlungsanweisungen. * **Principles (`type: principle`):** Handlungsanweisungen.
* **Goals (`type: goal`):** Strategische Ziele. * **Goals (`type: goal`):** Strategische Ziele.
### 6.5 Reasoning (Das Gewissen) ### 6.4 Reasoning (Das Gewissen)
Das LLM erhält im Prompt die explizite Anweisung: *"Wäge die Fakten (aus der Suche) gegen die injizierten Werte ab."* Das LLM erhält im Prompt die explizite Anweisung: *"Wäge die Fakten (aus der Suche) gegen die injizierten Werte ab."*
Dadurch entstehen Antworten, die nicht nur technisch korrekt sind, sondern subjektiv passend ("Tool X passt nicht zu deinem Ziel Z"). Dadurch entstehen Antworten, die nicht nur technisch korrekt sind, sondern subjektiv passend ("Tool X passt nicht zu deinem Ziel Z").
### 6.6 Der Interview-Modus (One-Shot Extraction) ### 6.5 Der Interview-Modus (One-Shot Extraction)
Wenn der User Wissen erfassen will ("Ich möchte ein neues Projekt anlegen"), wechselt Mindnet in den **Interview-Modus**. Wenn der User Wissen erfassen will ("Ich möchte ein neues Projekt anlegen"), wechselt Mindnet in den **Interview-Modus**.
* **Late Binding Schema:** Das System lädt ein konfiguriertes Schema für den Ziel-Typ (z.B. `project`: Pflichtfelder sind Titel, Ziel, Status). * **Late Binding Schema:** Das System lädt ein konfiguriertes Schema für den Ziel-Typ (z.B. `project`: Pflichtfelder sind Titel, Ziel, Status).
* **One-Shot Extraction:** Statt eines langen Dialogs extrahiert das LLM **sofort** alle verfügbaren Infos aus dem Prompt und generiert einen validen Markdown-Draft mit Frontmatter. * **One-Shot Extraction:** Statt eines langen Dialogs extrahiert das LLM **sofort** alle verfügbaren Infos aus dem Prompt und generiert einen validen Markdown-Draft mit Frontmatter.
* **Healing Parser (v2.5):** Falls das LLM die YAML-Syntax beschädigt (z.B. fehlendes Ende), repariert das Frontend den Entwurf automatisch. * **Draft-Status:** Fehlende Pflichtfelder werden mit `[TODO]` markiert.
* **UI-Integration:** Das Frontend rendert statt einer Chat-Antwort einen **interaktiven Editor** (WP10), in dem der Entwurf finalisiert werden kann. * **UI-Integration:** Das Frontend rendert statt einer Chat-Antwort einen **interaktiven Editor** (WP10), in dem der Entwurf finalisiert werden kann.
### 6.7 Active Intelligence (Link Suggestions) ### 6.6 Active Intelligence (Link Suggestions) Neu in v2.4
Im **Draft Editor** (Frontend) unterstützt das System den Autor aktiv. Im **Draft Editor** (Frontend) unterstützt das System den Autor aktiv.
* **Analyse:** Ein "Sliding Window" scannt den Text im Hintergrund (auch lange Entwürfe). * **Analyse:** Ein "Sliding Window" scannt den Text im Hintergrund (auch lange Entwürfe).
* **Erkennung:** Es findet Begriffe ("Mindnet") und semantische Konzepte ("Autofahrt in Italien"). * **Erkennung:** Es findet Begriffe ("Mindnet") und semantische Konzepte ("Autofahrt in Italien").
* **Matching:** Es prüft gegen den Index (Aliases und Vektoren). * **Matching:** Es prüft gegen den Index (Aliases und Vektoren).
* **Vorschlag:** Es bietet fertige Markdown-Links an (z.B. `[[rel:related_to ...]]`), die per Klick eingefügt werden. * **Vorschlag:** Es bietet fertige Markdown-Links an (z.B. `[[rel:related_to ...]]`), die per Klick eingefügt werden.
* **Logik:** Dabei kommt die in 2.5 beschriebene **Matrix-Logik** zum Einsatz, um den korrekten Kanten-Typ vorzuschlagen. * **Logik:** Dabei kommt die in 2.4 beschriebene **Matrix-Logik** zum Einsatz, um den korrekten Kanten-Typ vorzuschlagen.
--- ---
@ -334,9 +301,8 @@ Mindnet lernt nicht durch klassisches Training (Fine-Tuning), sondern durch **Ko
chunk_profile: short # Risiken sind oft kurze Statements chunk_profile: short # Risiken sind oft kurze Statements
retriever_weight: 0.90 # Hohe Priorität im Ranking retriever_weight: 0.90 # Hohe Priorität im Ranking
edge_defaults: ["blocks"] # Automatische Kante zu verlinkten Projekten edge_defaults: ["blocks"] # Automatische Kante zu verlinkten Projekten
detection_keywords: ["gefahr", "risiko"] # Für den Hybrid Router
``` ```
*Effekt:* Der Router erkennt das Wort "Risiko" und bietet ein Interview an. Der Retriever spült diese Notizen bei relevanten Anfragen nach oben. *Effekt:* Der Retriever spült diese Notizen bei relevanten Anfragen nach oben.
2. **Semantik erklären (`config/prompts.yaml` / `decision_engine.yaml`)** 2. **Semantik erklären (`config/prompts.yaml` / `decision_engine.yaml`)**
Bringe dem LLM bei, wie es mit diesem Typ umgehen soll. Füge `risk` zur `DECISION`-Strategie hinzu (`inject_types`), damit es bei Entscheidungen geladen wird. Bringe dem LLM bei, wie es mit diesem Typ umgehen soll. Füge `risk` zur `DECISION`-Strategie hinzu (`inject_types`), damit es bei Entscheidungen geladen wird.
@ -393,12 +359,12 @@ Die Interaktion erfolgt primär über das **Web-Interface (WP10)**.
## 10) Confidence & Provenance wozu? ## 10) Confidence & Provenance wozu?
Der Retriever kann Edges gewichten: Der Retriever kann Edges gewichten:
- **provenance**: *explicit* > *smart* (Neu) > *rule* - **provenance**: *explicit* > *rule*
- **confidence**: numerische Feinsteuerung - **confidence**: numerische Feinsteuerung
- **retriever_weight (Note/Chunk)**: skaliert die Relevanz des gesamten Knotens - **retriever_weight (Note/Chunk)**: skaliert die Relevanz des gesamten Knotens
Eine typische Gewichtung (konfigurierbar in `retriever.yaml`) ist: Eine typische Gewichtung (konfigurierbar in `retriever.yaml`) ist:
`explicit: 1.0`, `smart: 0.9`, `rule: 0.8`. Damit bevorzugt der Graph **kuratiertes Wissen** (explizit notierte Links) vor „erweiterten“ Default-Ableitungen. `explicit: 1.0`, `rule: 0.8`. Damit bevorzugt der Graph **kuratiertes Wissen** (explizit notierte Links) vor „erweiterten“ Default-Ableitungen.
--- ---
@ -447,11 +413,11 @@ Frontmatter-Eigenschaften (Properties) bleiben **minimiert**:
> [!edge] related_to: [[Vector DB Basics]] > [!edge] related_to: [[Vector DB Basics]]
**Ergebnis (fachlich - Smart Edges)** **Ergebnis (fachlich)**
Das LLM analysiert jeden Chunk. 1. `depends_on(Chunk→Qdrant)` mit `rule_id=inline:rel`, `confidence≈0.95`.
1. Chunk 1 ("Wir nutzen..."): Enthält `depends_on(Chunk→Qdrant)`. Das LLM bestätigt: Relevant. -> Kante wird erstellt. 2. `references(Chunk→Embeddings 101)` mit `rule_id=explicit:wikilink`, `confidence=1.0`.
2. Chunk 2 ("Siehe auch..."): Enthält `references(Chunk→Embeddings)`. Das LLM bestätigt. 3. `related_to(Chunk→Vector DB Basics)` via Callout; `rule_id=callout:edge`, `confidence≈0.90`.
3. **Wichtig:** Ein Chunk 3 ("Die Benutzeroberfläche ist blau..."), der keine Erwähnung von Qdrant hat, bekommt **keine** `depends_on` Kante zu Qdrant, auch wenn die Note global verlinkt ist. Das ist der Gewinn von WP15. 4. **Typ-Defaults:** Falls die Note vom Typ `project` ist, entstehen zusätzlich `depends_on`-Kanten zu den Zielen aus (2) und (3).
--- ---
@ -459,7 +425,6 @@ Das LLM analysiert jeden Chunk.
- Import-Pipeline & Registry-Auflösung: `scripts/import_markdown.py`. - Import-Pipeline & Registry-Auflösung: `scripts/import_markdown.py`.
- Kantenbildung (V2-Logic): `app/core/derive_edges.py`. - Kantenbildung (V2-Logic): `app/core/derive_edges.py`.
- Smart Chunking & Traffic Control: `app/core/chunker.py` & `app/services/llm_service.py`.
- Typ-Registry: `config/types.yaml` & `TYPE_REGISTRY_MANUAL.md`. - Typ-Registry: `config/types.yaml` & `TYPE_REGISTRY_MANUAL.md`.
- Retriever-Scoring & Explanation: `app/core/retriever.py`. - Retriever-Scoring & Explanation: `app/core/retriever.py`.
- Persönlichkeit & Chat: `config/prompts.yaml` & `app/routers/chat.py`. - Persönlichkeit & Chat: `config/prompts.yaml` & `app/routers/chat.py`.
@ -470,7 +435,7 @@ Das LLM analysiert jeden Chunk.
--- ---
## 16) Workpackage Status (v2.6.0) ## 16) Workpackage Status (v2.4.0)
Aktueller Implementierungsstand der Module. Aktueller Implementierungsstand der Module.
@ -483,12 +448,9 @@ Aktueller Implementierungsstand der Module.
| **WP04b**| Explanation Layer | 🟢 Live | API liefert Reasons & Breakdown. | | **WP04b**| Explanation Layer | 🟢 Live | API liefert Reasons & Breakdown. |
| **WP04c**| Feedback Loop | 🟢 Live | Logging (JSONL) & Traceability aktiv. | | **WP04c**| Feedback Loop | 🟢 Live | Logging (JSONL) & Traceability aktiv. |
| **WP05** | Persönlichkeit / Chat | 🟢 Live | RAG-Integration mit Context Enrichment. | | **WP05** | Persönlichkeit / Chat | 🟢 Live | RAG-Integration mit Context Enrichment. |
| **WP06** | Decision Engine | 🟢 Live | Hybrid Router, Strategic Retrieval. | | **WP06** | Decision Engine | 🟢 Live | Intent-Router & Strategic Retrieval. |
| **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** | | **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** |
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. | | **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). | | **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI & Healing Parser.** | | **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** | | **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** |
| **WP15** | Smart Edge Allocation | 🟢 Live | **LLM-Filter & Traffic Control aktiv.** |
| **WP16** | Auto-Discovery | 🟡 Geplant | UX & Retrieval Tuning. |
| **WP17** | Conversational Memory | 🟡 Geplant | Dialog-Gedächtnis. |

View File

@ -1,21 +1,21 @@
# Mindnet v2.6 Technische Architektur # Mindnet v2.4 Technische Architektur
**Datei:** `docs/mindnet_technical_architecture_v2.6.md` **Datei:** `docs/mindnet_technical_architecture_v2.4.md`
**Stand:** 2025-12-12 **Stand:** 2025-12-11
**Status:** **FINAL** (Integrierter Stand WP01WP15: Smart Edges & Traffic Control) **Status:** **FINAL** (Integrierter Stand WP01WP11: Async Intelligence)
**Quellen:** `Programmplan_V2.6.md`, `Handbuch.md`, `chunking_strategy.md`, `wp04_retriever_scoring.md`. **Quellen:** `Programmplan_V2.2.md`, `Handbuch.md`, `chunking_strategy.md`, `wp04_retriever_scoring.md`.
> **Ziel dieses Dokuments:** > **Ziel dieses Dokuments:**
> Vollständige Beschreibung der technischen Architektur inkl. Graph-Datenbank, Retrieval-Logik, der **RAG-Komponenten (Decision Engine & Hybrid Router)**, des **Traffic Control Systems** und dem **Frontend (Streamlit)**. > Vollständige Beschreibung der technischen Architektur inkl. Graph-Datenbank, Retrieval-Logik, der **RAG-Komponenten (Decision Engine & Hybrid Router)**, des **Interview-Modus** und dem **Frontend (Streamlit)**.
--- ---
<details> <details>
<summary>📖 <b>Inhaltsverzeichnis (Klicken zum Öffnen)</b></summary> <summary>📖 <b>Inhaltsverzeichnis (Klicken zum Öffnen)</b></summary>
- [Mindnet v2.6 Technische Architektur](#mindnet-v26--technische-architektur) - [Mindnet v2.4 Technische Architektur](#mindnet-v24--technische-architektur)
- [](#) - [](#)
- [1. Systemüberblick](#1-systemüberblick) - [1. Systemüberblick](#1-systemüberblick)
- [1.1 Architektur-Zielbild](#11-architektur-zielbild) - [1.1 Architektur-Zielbild](#11-architektur-zielbild)
- [1.2 Verzeichnisstruktur \& Komponenten (Post-WP15)](#12-verzeichnisstruktur--komponenten-post-wp15) - [1.2 Verzeichnisstruktur \& Komponenten (Post-WP10)](#12-verzeichnisstruktur--komponenten-post-wp10)
- [2. Datenmodell \& Collections (Qdrant)](#2-datenmodell--collections-qdrant) - [2. Datenmodell \& Collections (Qdrant)](#2-datenmodell--collections-qdrant)
- [2.1 Notes Collection (`<prefix>_notes`)](#21-notes-collection-prefix_notes) - [2.1 Notes Collection (`<prefix>_notes`)](#21-notes-collection-prefix_notes)
- [2.2 Chunks Collection (`<prefix>_chunks`)](#22-chunks-collection-prefix_chunks) - [2.2 Chunks Collection (`<prefix>_chunks`)](#22-chunks-collection-prefix_chunks)
@ -27,23 +27,22 @@
- [3.4 Prompts (`config/prompts.yaml`)](#34-prompts-configpromptsyaml) - [3.4 Prompts (`config/prompts.yaml`)](#34-prompts-configpromptsyaml)
- [3.5 Environment (`.env`)](#35-environment-env) - [3.5 Environment (`.env`)](#35-environment-env)
- [4. Import-Pipeline (Markdown → Qdrant)](#4-import-pipeline-markdown--qdrant) - [4. Import-Pipeline (Markdown → Qdrant)](#4-import-pipeline-markdown--qdrant)
- [4.1 Verarbeitungsschritte (Async + Smart)](#41-verarbeitungsschritte-async--smart) - [4.1 Verarbeitungsschritte (Async)](#41-verarbeitungsschritte-async)
- [5. Retriever-Architektur \& Scoring](#5-retriever-architektur--scoring) - [5. Retriever-Architektur \& Scoring](#5-retriever-architektur--scoring)
- [5.1 Betriebsmodi](#51-betriebsmodi) - [5.1 Betriebsmodi](#51-betriebsmodi)
- [5.2 Scoring-Formel (WP04a)](#52-scoring-formel-wp04a) - [5.2 Scoring-Formel (WP04a)](#52-scoring-formel-wp04a)
- [5.3 Explanation Layer (WP04b)](#53-explanation-layer-wp04b) - [5.3 Explanation Layer (WP04b)](#53-explanation-layer-wp04b)
- [5.4 Graph-Expansion](#54-graph-expansion) - [5.4 Graph-Expansion](#54-graph-expansion)
- [6. RAG \& Chat Architektur (WP06 Hybrid Router + WP07 Interview)](#6-rag--chat-architektur-wp06-hybrid-router--wp07-interview) - [6. RAG \& Chat Architektur (WP06 Hybrid Router + WP07 Interview)](#6-rag--chat-architektur-wp06-hybrid-router--wp07-interview)
- [6.1 Architektur-Pattern: Intent Router v5](#61-architektur-pattern-intent-router-v5) - [6.1 Architektur-Pattern: Intent Router](#61-architektur-pattern-intent-router)
- [6.2 Traffic Control (Priorisierung)](#62-traffic-control-priorisierung) - [6.2 Schritt 1: Intent Detection (Hybrid)](#62-schritt-1-intent-detection-hybrid)
- [6.3 Schritt 1: Intent Detection (Question vs. Action)](#63-schritt-1-intent-detection-question-vs-action) - [6.3 Schritt 2: Strategy Resolution (Late Binding)](#63-schritt-2-strategy-resolution-late-binding)
- [6.4 Schritt 2: Strategy Resolution](#64-schritt-2-strategy-resolution) - [6.4 Schritt 3: Retrieval vs. Extraction](#64-schritt-3-retrieval-vs-extraction)
- [6.5 Schritt 3: Retrieval vs. Extraction](#65-schritt-3-retrieval-vs-extraction) - [6.5 Schritt 4: Generation \& Response](#65-schritt-4-generation--response)
- [6.6 Schritt 4: Generation \& Response](#66-schritt-4-generation--response)
- [7. Frontend Architektur (WP10)](#7-frontend-architektur-wp10) - [7. Frontend Architektur (WP10)](#7-frontend-architektur-wp10)
- [7.1 Kommunikation](#71-kommunikation) - [7.1 Kommunikation](#71-kommunikation)
- [7.2 Features \& UI-Logik](#72-features--ui-logik) - [7.2 Features \& UI-Logik](#72-features--ui-logik)
- [7.3 Draft-Editor \& Healing Parser (Neu in WP10a/v2.5)](#73-draft-editor--healing-parser-neu-in-wp10av25) - [7.3 Draft-Editor \& Sanitizer (Neu in WP10a)](#73-draft-editor--sanitizer-neu-in-wp10a)
- [7.4 State Management (Resurrection Pattern)](#74-state-management-resurrection-pattern) - [7.4 State Management (Resurrection Pattern)](#74-state-management-resurrection-pattern)
- [7.5 Deployment Ports](#75-deployment-ports) - [7.5 Deployment Ports](#75-deployment-ports)
- [8. Feedback \& Logging Architektur (WP04c)](#8-feedback--logging-architektur-wp04c) - [8. Feedback \& Logging Architektur (WP04c)](#8-feedback--logging-architektur-wp04c)
@ -63,63 +62,57 @@
Mindnet ist ein **lokales RAG-System (Retrieval Augmented Generation)** mit Web-Interface. Mindnet ist ein **lokales RAG-System (Retrieval Augmented Generation)** mit Web-Interface.
1. **Source:** Markdown-Notizen in einem Vault (Obsidian-kompatibel). 1. **Source:** Markdown-Notizen in einem Vault (Obsidian-kompatibel).
2. **Pipeline:** Ein Python-Importer transformiert diese in **Notes**, **Chunks** und **Edges**. 2. **Pipeline:** Ein Python-Importer transformiert diese in **Notes**, **Chunks** und **Edges**.
* **Neu in v2.6:** Intelligente Kanten-Zuweisung durch lokale LLMs (**Smart Edge Allocation**).
3. **Storage:** 3. **Storage:**
* **Qdrant:** Vektor-Datenbank für Graph und Semantik (Collections: notes, chunks, edges). * **Qdrant:** Vektor-Datenbank für Graph und Semantik (Collections: notes, chunks, edges).
* **Local Files (JSONL):** Append-Only Logs für Feedback und Search-History (Data Flywheel). * **Local Files (JSONL):** Append-Only Logs für Feedback und Search-History (Data Flywheel).
4. **Backend:** Eine FastAPI-Anwendung stellt Endpunkte für **Semantische** und **Hybride Suche** sowie **Feedback** bereit. 4. **Backend:** Eine FastAPI-Anwendung stellt Endpunkte für **Semantische** und **Hybride Suche** sowie **Feedback** bereit.
* **Update v2.6:** Der Core arbeitet nun vollständig **asynchron (AsyncIO)** mit **Traffic Control** (Semaphore zur Lastverteilung). * **Update v2.3.10:** Der Core arbeitet nun vollständig **asynchron (AsyncIO)**, um Blockaden bei Embedding-Requests zu vermeiden.
5. **Frontend:** Streamlit-App (`ui.py`) für Interaktion und Visualisierung inkl. **Draft Editor**, **Active Intelligence** und **Healing Parser**. 5. **Frontend:** Streamlit-App (`ui.py`) für Interaktion und Visualisierung inkl. **Draft Editor** und **Intelligence-Features**.
6. **Inference:** Lokales LLM (Ollama: Phi-3 Mini) für RAG-Chat und Antwortgenerierung. Embedding via `nomic-embed-text`. 6. **Inference:** Lokales LLM (Ollama: Phi-3 Mini) für RAG-Chat und Antwortgenerierung. Embedding via `nomic-embed-text`.
Das System arbeitet **deterministisch** (stabile IDs) und ist **konfigurationsgetrieben** (`types.yaml`, `retriever.yaml`, `decision_engine.yaml`, `prompts.yaml`). Das System arbeitet **deterministisch** (stabile IDs) und ist **konfigurationsgetrieben** (`types.yaml`, `retriever.yaml`, `decision_engine.yaml`, `prompts.yaml`).
### 1.2 Verzeichnisstruktur & Komponenten (Post-WP15) ### 1.2 Verzeichnisstruktur & Komponenten (Post-WP10)
/mindnet/ /mindnet/
├── app/ ├── app/
│ ├── main.py # FastAPI Einstiegspunkt │ ├── main.py # FastAPI Einstiegspunkt
│ ├── core/ │ ├── core/
│ │ ├── ingestion.py # NEU: Async Ingestion mit Change Detection │ │ ├── ingestion.py # NEU: Async Ingestion Service (WP11)
│ │ ├── qdrant.py # Client-Factory & Connection │ │ ├── qdrant.py # Client-Factory & Connection
│ │ ├── qdrant_points.py # Low-Level Point Operations (Upsert/Delete) │ │ ├── qdrant_points.py # Low-Level Point Operations (Upsert/Delete)
│ │ ├── note_payload.py # Bau der Note-Objekte │ │ ├── note_payload.py # Bau der Note-Objekte
│ │ ├── chunk_payload.py # Bau der Chunk-Objekte │ │ ├── chunk_payload.py # Bau der Chunk-Objekte
│ │ ├── chunker.py # Smart Chunker Orchestrator (WP15) │ │ ├── chunker.py # Text-Zerlegung (Profiling)
│ │ ├── edges.py # Edge-Datenstrukturen │ │ ├── edges.py # Edge-Datenstrukturen
│ │ ├── derive_edges.py # Logik der Kantenableitung (WP03) │ │ ├── derive_edges.py # Logik der Kantenableitung (WP03)
│ │ ├── graph_adapter.py # Subgraph & Reverse-Lookup (WP04b) │ │ ├── graph_adapter.py # Subgraph & Reverse-Lookup (WP04b)
│ │ └── retriever.py # Scoring, Expansion & Explanation (WP04a/b) │ │ └── retriever.py # Scoring, Expansion & Explanation (WP04a/b)
│ ├── models/ # Pydantic DTOs │ ├── models/ # Pydantic DTOs
│ │ └── dto.py # Zentrale DTO-Definition
│ ├── routers/ │ ├── routers/
│ │ ├── query.py # Such-Endpunkt │ │ ├── query.py # Such-Endpunkt
│ │ ├── ingest.py # NEU: API für Save & Analyze (WP11) │ │ ├── ingest.py # NEU: API für Save & Analyze (WP11)
│ │ ├── chat.py # Hybrid Router v5 & Interview Logic (WP06/07) │ │ ├── chat.py # Hybrid Router & Interview Logic (WP06/07)
│ │ ├── feedback.py # Feedback-Endpunkt (WP04c) │ │ ├── feedback.py # Feedback-Endpunkt (WP04c)
│ │ └── ... │ │ └── ...
│ ├── services/ │ ├── services/
│ │ ├── llm_service.py # Ollama Chat Client mit Traffic Control (v2.8.0) │ │ ├── llm_service.py # Ollama Chat Client
│ │ ├── semantic_analyzer.py# NEU: LLM-Filter für Edges (WP15) │ │ ├── embeddings_client.py# NEU: Async Embedding Client (HTTPX)
│ │ ├── embeddings_client.py# NEU: Async Embeddings (HTTPX) │ │ └── feedback_service.py # JSONL Logging (WP04c)
│ │ ├── feedback_service.py # Logging (JSONL Writer)
│ │ └── discovery.py # NEU: Intelligence Logic (WP11)
│ ├── frontend/ # NEU (WP10) │ ├── frontend/ # NEU (WP10)
│ │ └── ui.py # Streamlit Application inkl. Healing Parser │ └── ui.py # Streamlit Application inkl. Sanitizer
│ └── main.py # Entrypoint der API ├── config/
├── config/ # YAML-Konfigurationen (Single Source of Truth) │ ├── types.yaml # Typ-Definitionen (Import-Zeit)
│ ├── types.yaml # Import-Regeln & Smart-Edge Config │ ├── retriever.yaml # Scoring-Gewichte (Laufzeit)
│ ├── prompts.yaml # LLM Prompts & Interview Templates (WP06/07) │ ├── decision_engine.yaml # Strategien & Schemas (WP06/WP07)
│ ├── decision_engine.yaml # Router-Strategien (Actions only) │ └── prompts.yaml # LLM System-Prompts & Templates (WP06)
│ └── retriever.yaml # Scoring-Regeln & Kantengewichte
├── data/ ├── data/
│ └── logs/ # Lokale Logs (search_history.jsonl, feedback.jsonl) │ └── logs/ # Lokale JSONL-Logs (WP04c)
├── scripts/ # CLI-Tools (Import, Diagnose, Reset) ├── scripts/
│ ├── import_markdown.py # Haupt-Importer CLI (Async) │ ├── import_markdown.py # Haupt-Importer CLI (Async)
│ ├── payload_dryrun.py # Diagnose: JSON-Generierung ohne DB │ ├── payload_dryrun.py # Diagnose: JSON-Generierung ohne DB
│ └── edges_full_check.py # Diagnose: Graph-Integrität │ └── edges_full_check.py # Diagnose: Graph-Integrität
├── tests/ # Pytest Suite & Smoke Scripts └── tests/ # Pytest Suite
└── vault/ # Dein lokaler Markdown-Content (Git-ignored)
--- ---
@ -147,7 +140,7 @@ Repräsentiert die Metadaten einer Datei.
### 2.2 Chunks Collection (`<prefix>_chunks`) ### 2.2 Chunks Collection (`<prefix>_chunks`)
Die atomaren Sucheinheiten. Die atomaren Sucheinheiten.
* **Zweck:** Vektorsuche (Embeddings), Granulares Ergebnis. * **Zweck:** Vektorsuche (Embeddings), Granulares Ergebnis.
* **Update v2.4:** Vektor-Dimension ist jetzt **768** (für `nomic-embed-text`). * **Update v2.3.10:** Vektor-Dimension ist jetzt **768** (für `nomic-embed-text`).
* **Schema (Payload):** * **Schema (Payload):**
| Feld | Datentyp | Beschreibung | | Feld | Datentyp | Beschreibung |
@ -178,7 +171,6 @@ Gerichtete Kanten. Massiv erweitert in WP03 für Provenienz-Tracking.
| `scope` | Keyword | Geltungsbereich. | Immer `"chunk"` (v2.2). | | `scope` | Keyword | Geltungsbereich. | Immer `"chunk"` (v2.2). |
| `rule_id` | Keyword | Herkunftsregel. | `explicit:wikilink`, `inline:rel` | | `rule_id` | Keyword | Herkunftsregel. | `explicit:wikilink`, `inline:rel` |
| `confidence` | Float | Vertrauenswürdigkeit (0.0-1.0). | 1.0, 0.95, 0.7 | | `confidence` | Float | Vertrauenswürdigkeit (0.0-1.0). | 1.0, 0.95, 0.7 |
| `provenance` | Keyword | Quelle der Kante (Neu in v2.6). | `explicit`, `rule`, `smart` |
--- ---
@ -189,20 +181,13 @@ Die Logik ist ausgelagert in YAML-Dateien.
### 3.1 Typ-Registry (`config/types.yaml`) ### 3.1 Typ-Registry (`config/types.yaml`)
Steuert den Import-Prozess. Steuert den Import-Prozess.
* **Priorität:** Frontmatter > Pfad > Default. * **Priorität:** Frontmatter > Pfad > Default.
* **Neu in v2.6:**
* `enable_smart_edge_allocation`: (Bool) Aktiviert den LLM-Filter für diesen Typ.
* `detection_keywords`: (List) Keywords für den Hybrid Router ("Objekterkennung").
* **Inhalt (Beispiel):** * **Inhalt (Beispiel):**
types: types:
concept: concept:
chunk_profile: sliding_standard chunk_profile: medium
edge_defaults: ["references", "related_to"] edge_defaults: ["references", "related_to"]
retriever_weight: 0.60 retriever_weight: 0.60
experience:
chunk_profile: sliding_smart_edges
enable_smart_edge_allocation: true
detection_keywords: ["erfahrung", "erlebt"]
### 3.2 Retriever-Config (`config/retriever.yaml`) ### 3.2 Retriever-Config (`config/retriever.yaml`)
Steuert das Scoring zur Laufzeit (WP04a). Steuert das Scoring zur Laufzeit (WP04a).
@ -214,14 +199,14 @@ Steuert das Scoring zur Laufzeit (WP04a).
centrality_weight: 0.5 centrality_weight: 0.5
### 3.3 Decision Engine (`config/decision_engine.yaml`) ### 3.3 Decision Engine (`config/decision_engine.yaml`)
**Neu in WP06/07:** Steuert den Intent-Router. **Neu in WP06/07:** Steuert den Intent-Router und die Interview-Schemas.
* Definiert Strategien (`DECISION`, `INTERVIEW`, etc.). * Definiert Strategien (`DECISION`, `INTERVIEW`, etc.).
* **Update v2.6:** Enthält nur noch **Handlungs-Keywords** (Verben wie "neu", "erstellen"). Objektnamen ("Projekt") werden nun über `types.yaml` aufgelöst. * Definiert `schemas` für den Interview-Modus (Pflichtfelder pro Typ).
* Definiert LLM-Router-Settings (`llm_fallback_enabled`). * Definiert LLM-Router-Settings (`llm_fallback_enabled`).
### 3.4 Prompts (`config/prompts.yaml`) ### 3.4 Prompts (`config/prompts.yaml`)
Steuert die LLM-Persönlichkeit und Templates. Steuert die LLM-Persönlichkeit und Templates.
* Enthält Templates für alle Strategien inkl. `interview_template` und neu `router_prompt`. * Enthält Templates für alle Strategien inkl. `interview_template` mit One-Shot Logik.
### 3.5 Environment (`.env`) ### 3.5 Environment (`.env`)
Erweiterung für LLM-Steuerung und Embedding-Modell: Erweiterung für LLM-Steuerung und Embedding-Modell:
@ -230,33 +215,32 @@ Erweiterung für LLM-Steuerung und Embedding-Modell:
MINDNET_EMBEDDING_MODEL=nomic-embed-text # NEU in v2.3.10 MINDNET_EMBEDDING_MODEL=nomic-embed-text # NEU in v2.3.10
MINDNET_OLLAMA_URL=http://127.0.0.1:11434 MINDNET_OLLAMA_URL=http://127.0.0.1:11434
MINDNET_LLM_TIMEOUT=300.0 # Neu: Erhöht für CPU-Inference Cold-Starts MINDNET_LLM_TIMEOUT=300.0 # Neu: Erhöht für CPU-Inference Cold-Starts
MINDNET_API_TIMEOUT=300.0 # Neu: Timeout für Frontend-API (Smart Edges brauchen Zeit) MINDNET_API_TIMEOUT=60.0 # Neu: Timeout für Frontend-API Calls
MINDNET_DECISION_CONFIG="config/decision_engine.yaml" MINDNET_DECISION_CONFIG="config/decision_engine.yaml"
MINDNET_VAULT_ROOT="./vault" # Neu: Pfad für Write-Back MINDNET_VAULT_ROOT="./vault" # Neu: Pfad für Write-Back
MINDNET_LLM_BACKGROUND_LIMIT=2 # Neu: Limit für parallele Hintergrund-Jobs (Traffic Control)
--- ---
## 4. Import-Pipeline (Markdown → Qdrant) ## 4. Import-Pipeline (Markdown → Qdrant)
Das Skript `scripts/import_markdown.py` orchestriert den Prozess. Das Skript `scripts/import_markdown.py` orchestriert den Prozess.
**Neu in v2.6:** Der Import nutzt `asyncio` und **Traffic Control**, um Ollama nicht zu überlasten. **Neu in v2.3.10:** Der Import nutzt `asyncio` und eine **Semaphore**, um Ollama nicht zu überlasten.
### 4.1 Verarbeitungsschritte (Async + Smart) ### 4.1 Verarbeitungsschritte (Async)
1. **Discovery & Parsing:** Hash-Vergleich zur Erkennung von Änderungen. 1. **Discovery & Parsing:**
2. **Typauflösung:** Bestimmung des `type` via `types.yaml`. * Einlesen der `.md` Dateien. Hash-Vergleich (Body/Frontmatter) zur Erkennung von Änderungen.
3. **Config Check:** Laden des `chunk_profile` und `enable_smart_edge_allocation`. 2. **Typauflösung:**
4. **Chunking & Smart Edges (WP15):** * Bestimmung des `type` via `types.yaml`.
* Zerlegung des Textes via `chunker.py`. 3. **Chunking:**
* Wenn Smart Edges aktiv: Der `SemanticAnalyzer` sendet Chunks an das LLM. * Zerlegung via `chunker.py` basierend auf `chunk_profile`.
* **Traffic Control:** Der Request nutzt `priority="background"`. Die Semaphore (Limit: 2) drosselt die Parallelität. 4. **Embedding (Async):**
5. **Kantenableitung (Edge Derivation):**
* `derive_edges.py` erzeugt Inline-, Callout- und Default-Edges.
6. **Embedding (Async):**
* Der `EmbeddingsClient` (`app/services/embeddings_client.py`) sendet Text-Chunks asynchron an Ollama. * Der `EmbeddingsClient` (`app/services/embeddings_client.py`) sendet Text-Chunks asynchron an Ollama.
* Modell: `nomic-embed-text` (768d). * Modell: `nomic-embed-text` (768d).
7. **Upsert:** * Semaphore: Max. 5 gleichzeitige Files, um OOM (Out-of-Memory) zu verhindern.
5. **Kantenableitung (Edge Derivation):**
* `derive_edges.py` erzeugt Inline-, Callout- und Default-Edges.
6. **Upsert:**
* Schreiben in Qdrant. Nutzung von `--purge-before-upsert`. * Schreiben in Qdrant. Nutzung von `--purge-before-upsert`.
* **Strict Mode:** Der Prozess bricht ab, wenn Embeddings leer sind oder Dimension `0` haben. * **Strict Mode:** Der Prozess bricht ab, wenn Embeddings leer sind oder Dimension `0` haben.
@ -285,55 +269,49 @@ $$
* **Gewichte ($W$):** Stammen aus `retriever.yaml`. * **Gewichte ($W$):** Stammen aus `retriever.yaml`.
### 5.3 Explanation Layer (WP04b) ### 5.3 Explanation Layer (WP04b)
Der Retriever ist keine Blackbox mehr. Er liefert auf Wunsch (`explain=True`) eine strukturierte Begründung (`Explanation`-Objekt). Der Retriever kann Ergebnisse erklären (`explain=True`).
* **Logik:**
**Die "Warum"-Logik:** * Berechnung des `ScoreBreakdown` (Anteile von Semantik, Graph, Typ).
1. **Semantik:** Prüfung der Cosine-Similarity ("Sehr hohe textuelle Übereinstimmung"). * Analyse des lokalen Subgraphen mittels `graph_adapter.py`.
2. **Typ:** Prüfung des `retriever_weight` ("Bevorzugt, da Entscheidung"). * **Incoming Edges (Authority):** Wer zeigt auf diesen Treffer? (z.B. "Referenziert von...")
3. **Graph (Kontext):** * **Outgoing Edges (Hub):** Worauf zeigt dieser Treffer? (z.B. "Verweist auf...")
* **Hub (Outgoing):** Worauf verweist dieser Treffer? ("Verweist auf Qdrant"). * **Output:** `QueryHit` enthält ein `explanation` Objekt mit menschenlesbaren `reasons` und `related_edges`.
* **Authority (Incoming):** Wer verweist auf diesen Treffer? ("Wird referenziert von Projekt Alpha").
Die API gibt diese Analysen als menschenlesbare Sätze (`reasons`) und als Datenstruktur (`score_breakdown`) zurück.
### 5.4 Graph-Expansion ### 5.4 Graph-Expansion
Der Hybrid-Modus lädt dynamisch die Nachbarschaft der Top-K Vektor-Treffer ("Seeds") über `graph_adapter.expand`. Dies baut einen temporären `NetworkX`-artigen Graphen im Speicher (Klasse `Subgraph`), auf dem Boni berechnet werden. Der Hybrid-Modus lädt dynamisch die Nachbarschaft der Top-K Vektor-Treffer ("Seeds") über `graph_adapter.expand`. Dies baut einen temporären `NetworkX`-artigen Graphen im Speicher (Klasse `Subgraph`), auf dem Boni berechnet werden.
--- ---
## 6. RAG & Chat Architektur (WP06 Hybrid Router + WP07 Interview) ## 6. RAG \& Chat Architektur (WP06 Hybrid Router + WP07 Interview)
Der Flow für eine Chat-Anfrage (`/chat`) wurde in WP06 auf eine **Configuration-Driven Architecture** umgestellt und in WP15 (v2.6) verfeinert. Der Flow für eine Chat-Anfrage (`/chat`) wurde in WP06 auf eine **Configuration-Driven Architecture** umgestellt. Der `ChatRouter` (`app/routers/chat.py`) fungiert als zentraler Dispatcher.
### 6.1 Architektur-Pattern: Intent Router v5 ### 6.1 Architektur-Pattern: Intent Router
Die Behandlung einer Anfrage ist nicht mehr hartkodiert, sondern wird dynamisch zur Laufzeit entschieden. Die Behandlung einer Anfrage ist nicht mehr hartkodiert, sondern wird dynamisch zur Laufzeit entschieden.
* **Input:** User Message. * **Input:** User Message.
* **Config:** `decision_engine.yaml` (Strategien) + `types.yaml` (Objekte). * **Config:** `config/decision_engine.yaml` (Strategien & Keywords).
* **Komponenten:** * **Komponenten:**
* **Traffic Control:** `LLMService` priorisiert Chat-Anfragen. * **Fast Path:** Keyword Matching (CPU-schonend).
* **Question Detection:** Unterscheidung Frage vs. Befehl. * **Slow Path:** LLM-basierter Semantic Router (für subtile Intents).
### 6.2 Traffic Control (Priorisierung) ### 6.2 Schritt 1: Intent Detection (Hybrid)
Der `LLMService` implementiert zwei Spuren basierend auf `app/services/llm_service.py` (v2.8.0):
* **Realtime (Chat):** `priority="realtime"`. Umgeht die Semaphore. Antwortet sofort.
* **Background (Import/Analyse):** `priority="background"`. Wartet in der Semaphore (`asyncio.Semaphore`), die lazy mit `MINDNET_LLM_BACKGROUND_LIMIT` initialisiert wird.
### 6.3 Schritt 1: Intent Detection (Question vs. Action)
Der Router ermittelt die Absicht (`Intent`) des Nutzers. Der Router ermittelt die Absicht (`Intent`) des Nutzers.
1. **Frage-Check:** Wenn der Input ein `?` enthält oder mit W-Wörtern beginnt, wird der **RAG-Modus** erzwungen (oder LLM-Entscheidung). Interviews werden hier unterdrückt. 1. **Keyword Scan (Fast Path):**
2. **Keyword Scan (Fast Path):** * Iteration über alle Strategien in `decision_engine.yaml`.
* Prüfung auf `trigger_keywords` (Handlung) in `decision_engine.yaml`. * Prüfung auf `trigger_keywords`.
* Prüfung auf `detection_keywords` (Objekt) in `types.yaml`. * **Best Match:** Bei mehreren Treffern gewinnt das längste/spezifischste Keyword (Robustheit gegen Shadowing).
* Treffer -> **INTERVIEW Modus** (Erfassen). 2. **LLM Fallback (Slow Path):**
3. **LLM Fallback (Slow Path):** * Nur aktiv, wenn `llm_fallback_enabled: true`.
* Greift, wenn keine Keywords passen. Sendet Query an LLM Router. * Greift, wenn keine Keywords gefunden wurden.
* Sendet die Query an das LLM mit einem Klassifizierungs-Prompt (`llm_router_prompt`).
* Ergebnis: `EMPATHY`, `DECISION`, `INTERVIEW`, `CODING` oder `FACT`.
### 6.4 Schritt 2: Strategy Resolution ### 6.3 Schritt 2: Strategy Resolution (Late Binding)
Basierend auf dem Intent lädt der Router die Parameter: Basierend auf dem Intent lädt der Router die Parameter:
* **Bei RAG (FACT/DECISION):** `inject_types` für Strategic Retrieval. * **Bei RAG (FACT/DECISION):** `inject_types` für Strategic Retrieval.
* **Bei INTERVIEW (WP07):** `schemas` (Pflichtfelder) basierend auf der erkannten Ziel-Entität (`_detect_target_type`). * **Bei INTERVIEW (WP07):** `schemas` (Pflichtfelder) basierend auf der erkannten Ziel-Entität (`_detect_target_type`).
### 6.5 Schritt 3: Retrieval vs. Extraction ### 6.4 Schritt 3: Retrieval vs. Extraction
Der Router verzweigt hier: Der Router verzweigt hier:
**A) RAG Modus (FACT, DECISION, EMPATHY):** **A) RAG Modus (FACT, DECISION, EMPATHY):**
@ -346,9 +324,9 @@ Der Router verzweigt hier:
2. **Schema Injection:** Das Schema für den erkannten Typ (z.B. "Project") wird geladen. 2. **Schema Injection:** Das Schema für den erkannten Typ (z.B. "Project") wird geladen.
3. **Prompt Assembly:** Der `interview_template` Prompt wird mit der Schema-Definition ("Ziel", "Status") gefüllt. 3. **Prompt Assembly:** Der `interview_template` Prompt wird mit der Schema-Definition ("Ziel", "Status") gefüllt.
### 6.6 Schritt 4: Generation & Response ### 6.5 Schritt 4: Generation & Response
* **Templating:** Das LLM erhält den Prompt basierend auf dem gewählten Template. * **Templating:** Das LLM erhält den Prompt basierend auf dem gewählten Template.
* **Execution:** Der `LLMService` führt den Call mit `priority="realtime"` aus. * **Execution:** Der `LLMService` führt den Call aus. Ein konfigurierbarer Timeout (`MINDNET_LLM_TIMEOUT`) fängt Cold-Start-Verzögerungen auf CPU-Hardware ab.
* **Response:** Rückgabe enthält Antworttext (im Interview-Modus: Markdown Codeblock), Quellenliste und den erkannten `intent`. * **Response:** Rückgabe enthält Antworttext (im Interview-Modus: Markdown Codeblock), Quellenliste und den erkannten `intent`.
--- ---
@ -359,12 +337,8 @@ Das Frontend ist eine **Streamlit-Anwendung** (`app/frontend/ui.py`), die als se
### 7.1 Kommunikation ### 7.1 Kommunikation
* **Backend-URL:** Konfiguriert via `MINDNET_API_URL` (Default: `http://localhost:8002`). * **Backend-URL:** Konfiguriert via `MINDNET_API_URL` (Default: `http://localhost:8002`).
* **Endpoints:** * **Endpoints:** Nutzt `/chat` für Interaktion, `/feedback` für Bewertungen und `/ingest/analyze` für Intelligence.
* `/chat` (Interaktion) * **Resilienz:** Das Frontend implementiert eigene Timeouts (`MINDNET_API_TIMEOUT`, Default 300s).
* `/feedback` (Bewertungen)
* `/ingest/analyze` (Active Intelligence / Link-Vorschläge)
* `/ingest/save` (Persistence / Speichern im Vault)
* **Resilienz:** Das Frontend nutzt `MINDNET_API_TIMEOUT` (300s), um auf langsame Backend-Prozesse (Smart Edges) zu warten.
### 7.2 Features & UI-Logik ### 7.2 Features & UI-Logik
* **State Management:** Session-State speichert Chat-Verlauf und `query_id`. * **State Management:** Session-State speichert Chat-Verlauf und `query_id`.
@ -373,16 +347,16 @@ Das Frontend ist eine **Streamlit-Anwendung** (`app/frontend/ui.py`), die als se
* **Source Expanders:** Zeigt verwendete Chunks inkl. Score und "Why"-Explanation. * **Source Expanders:** Zeigt verwendete Chunks inkl. Score und "Why"-Explanation.
* **Sidebar:** Zeigt Suchhistorie (Log-basiert) und Konfiguration. * **Sidebar:** Zeigt Suchhistorie (Log-basiert) und Konfiguration.
### 7.3 Draft-Editor & Healing Parser (Neu in WP10a/v2.5) ### 7.3 Draft-Editor & Sanitizer (Neu in WP10a)
Wenn der Intent `INTERVIEW` ist, rendert die UI statt einer Textblase den **Draft-Editor**. Wenn der Intent `INTERVIEW` ist, rendert die UI statt einer Textblase den **Draft-Editor**.
1. **Parsing:** Die Funktion `parse_markdown_draft` extrahiert den Codeblock aus der LLM-Antwort. 1. **Parsing:** Die Funktion `parse_markdown_draft` extrahiert den Codeblock aus der LLM-Antwort.
* **Healing:** Erkennt und repariert defekte YAML-Frontmatter (z.B. fehlendes schließendes `---`), falls das LLM unter Last Fehler macht.
2. **Sanitization (`normalize_meta_and_body`):** 2. **Sanitization (`normalize_meta_and_body`):**
* Prüft den YAML-Frontmatter auf unerlaubte Felder (Halluzinationen des LLMs). * Prüft den YAML-Frontmatter auf unerlaubte Felder (Halluzinationen des LLMs).
* Verschiebt ungültige Felder (z.B. "Situation") in den Body der Notiz. * Verschiebt ungültige Felder (z.B. "Situation") in den Body der Notiz.
* Stellt sicher, dass das Markdown valide bleibt.
3. **Editor Widget:** `st.text_area` erlaubt das Bearbeiten des Inhalts vor dem Speichern. 3. **Editor Widget:** `st.text_area` erlaubt das Bearbeiten des Inhalts vor dem Speichern.
4. **Save Action:** Speichert über `/ingest/save` atomar in Vault und DB. Erzeugt intelligente Dateinamen via `slugify`. 4. **Action:** Buttons zum Download oder Kopieren des fertigen Markdowns.
### 7.4 State Management (Resurrection Pattern) ### 7.4 State Management (Resurrection Pattern)
Um Datenverlust bei Tab-Wechseln (Chat <-> Editor) zu verhindern, nutzt `ui.py` ein Persistenz-Muster: Um Datenverlust bei Tab-Wechseln (Chat <-> Editor) zu verhindern, nutzt `ui.py` ein Persistenz-Muster:
@ -446,5 +420,5 @@ Validierung erfolgt über `tests/ensure_indexes_and_show.py`.
2. **Unresolved Targets:** 2. **Unresolved Targets:**
* Kanten zu Notizen, die noch nicht existieren, werden mit `target_id="Titel"` angelegt. * Kanten zu Notizen, die noch nicht existieren, werden mit `target_id="Titel"` angelegt.
* Heilung durch `scripts/resolve_unresolved_references.py` möglich. * Heilung durch `scripts/resolve_unresolved_references.py` möglich.
3. **Hardware-Last bei Smart Import:** 3. **Vektor-Konfiguration für Edges:**
* Der "Smart Edge" Import ist rechenintensiv. Trotz Traffic Control kann die Antwortzeit im Chat leicht steigen, wenn die GPU am VRAM-Limit arbeitet. * `mindnet_edges` hat aktuell keine Vektoren (`vectors = null`). Eine semantische Suche *auf Kanten* ist noch nicht möglich.

View File

@ -1,7 +1,7 @@
# mindnet v2.4 Pipeline Playbook # mindnet v2.4 Pipeline Playbook
**Datei:** `docs/mindnet_pipeline_playbook_v2.6.md` **Datei:** `docs/mindnet_pipeline_playbook_v2.4.md`
**Stand:** 2025-12-12 **Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. Smart Edges & Traffic Control) **Status:** **FINAL** (Inkl. Async Ingestion & Active Intelligence)
**Quellen:** `mindnet_v2_implementation_playbook.md`, `Handbuch.md`, `chunking_strategy.md`, `docs_mindnet_retriever.md`, `mindnet_admin_guide_v2.4.md`. **Quellen:** `mindnet_v2_implementation_playbook.md`, `Handbuch.md`, `chunking_strategy.md`, `docs_mindnet_retriever.md`, `mindnet_admin_guide_v2.4.md`.
--- ---
@ -12,7 +12,7 @@
- [](#) - [](#)
- [1. Zweck \& Einordnung](#1-zweck--einordnung) - [1. Zweck \& Einordnung](#1-zweck--einordnung)
- [2. Die Import-Pipeline (Runbook)](#2-die-import-pipeline-runbook) - [2. Die Import-Pipeline (Runbook)](#2-die-import-pipeline-runbook)
- [2.1 Der 13-Schritte-Prozess (Async + Smart)](#21-der-13-schritte-prozess-async--smart) - [2.1 Der 12-Schritte-Prozess (Async)](#21-der-12-schritte-prozess-async)
- [2.2 Standard-Betrieb (Inkrementell)](#22-standard-betrieb-inkrementell) - [2.2 Standard-Betrieb (Inkrementell)](#22-standard-betrieb-inkrementell)
- [2.3 Deployment \& Restart (Systemd)](#23-deployment--restart-systemd) - [2.3 Deployment \& Restart (Systemd)](#23-deployment--restart-systemd)
- [2.4 Full Rebuild (Clean Slate)](#24-full-rebuild-clean-slate) - [2.4 Full Rebuild (Clean Slate)](#24-full-rebuild-clean-slate)
@ -21,13 +21,12 @@
- [3.2 Payload-Felder](#32-payload-felder) - [3.2 Payload-Felder](#32-payload-felder)
- [4. Edge-Erzeugung (Die V2-Logik)](#4-edge-erzeugung-die-v2-logik) - [4. Edge-Erzeugung (Die V2-Logik)](#4-edge-erzeugung-die-v2-logik)
- [4.1 Prioritäten \& Provenance](#41-prioritäten--provenance) - [4.1 Prioritäten \& Provenance](#41-prioritäten--provenance)
- [4.2 Smart Edge Allocation (WP15)](#42-smart-edge-allocation-wp15) - [4.2 Typ-Defaults](#42-typ-defaults)
- [4.3 Typ-Defaults](#43-typ-defaults)
- [5. Retriever, Chat \& Generation (RAG Pipeline)](#5-retriever-chat--generation-rag-pipeline) - [5. Retriever, Chat \& Generation (RAG Pipeline)](#5-retriever-chat--generation-rag-pipeline)
- [5.1 Retrieval (Hybrid)](#51-retrieval-hybrid) - [5.1 Retrieval (Hybrid)](#51-retrieval-hybrid)
- [5.2 Intent Router (WP06/07)](#52-intent-router-wp0607) - [5.2 Intent Router (WP06/07)](#52-intent-router-wp0607)
- [5.3 Context Enrichment](#53-context-enrichment) - [5.3 Context Enrichment](#53-context-enrichment)
- [5.4 Generation (LLM) mit Traffic Control](#54-generation-llm-mit-traffic-control) - [5.4 Generation (LLM)](#54-generation-llm)
- [5.5 Active Intelligence Pipeline (Neu in v2.4)](#55-active-intelligence-pipeline-neu-in-v24) - [5.5 Active Intelligence Pipeline (Neu in v2.4)](#55-active-intelligence-pipeline-neu-in-v24)
- [6. Feedback \& Lernen (WP04c)](#6-feedback--lernen-wp04c) - [6. Feedback \& Lernen (WP04c)](#6-feedback--lernen-wp04c)
- [7. Quality Gates \& Tests](#7-quality-gates--tests) - [7. Quality Gates \& Tests](#7-quality-gates--tests)
@ -35,7 +34,7 @@
- [7.2 Smoke-Test (E2E)](#72-smoke-test-e2e) - [7.2 Smoke-Test (E2E)](#72-smoke-test-e2e)
- [8. Ausblick \& Roadmap (Technische Skizzen)](#8-ausblick--roadmap-technische-skizzen) - [8. Ausblick \& Roadmap (Technische Skizzen)](#8-ausblick--roadmap-technische-skizzen)
- [8.1 WP-08: Self-Tuning (Skizze)](#81-wp-08-self-tuning-skizze) - [8.1 WP-08: Self-Tuning (Skizze)](#81-wp-08-self-tuning-skizze)
- [16. Workpackage Status (v2.6.0)](#16-workpackage-status-v260) - [16. Workpackage Status (v2.4.0)](#16-workpackage-status-v240)
</details> </details>
--- ---
@ -46,7 +45,7 @@ Dieses Playbook ist das zentrale operative Handbuch für die **mindnet-Pipeline*
**Zielgruppe:** Dev/Ops, Tech-Leads. **Zielgruppe:** Dev/Ops, Tech-Leads.
**Scope:** **Scope:**
* **Ist-Stand (WP01WP15):** Async Import, Smart Chunking, Edge-Erzeugung, Hybrider Retriever, RAG-Chat (Hybrid Router), Feedback Loop, Frontend, Draft Editor, Active Intelligence. * **Ist-Stand (WP01WP11):** Async Import, Chunking, Edge-Erzeugung, Hybrider Retriever, RAG-Chat (Hybrid Router), Feedback Loop, Frontend, Draft Editor, Active Intelligence.
* **Roadmap (Ausblick):** Technische Skizze für Self-Tuning (WP08). * **Roadmap (Ausblick):** Technische Skizze für Self-Tuning (WP08).
--- ---
@ -55,32 +54,23 @@ Dieses Playbook ist das zentrale operative Handbuch für die **mindnet-Pipeline*
Der Import ist der kritischste Prozess ("Data Ingestion"). Er muss **deterministisch** und **idempotent** sein. Wir nutzen `scripts/import_markdown.py` als zentralen Entrypoint. Der Import ist der kritischste Prozess ("Data Ingestion"). Er muss **deterministisch** und **idempotent** sein. Wir nutzen `scripts/import_markdown.py` als zentralen Entrypoint.
### 2.1 Der 13-Schritte-Prozess (Async + Smart) ### 2.1 Der 12-Schritte-Prozess (Async)
Seit v2.6 läuft der Import vollständig asynchron, nutzt intelligente Kantenvalidierung (Smart Edges) und drosselt sich selbst ("Traffic Control"). Seit v2.3.10 läuft der Import **asynchron**, um Netzwerk-Blockaden bei der Embedding-Generierung zu vermeiden.
1. **Markdown lesen:** Rekursives Scannen des Vaults. 1. **Markdown lesen:** Rekursives Scannen des Vaults.
2. **Frontmatter extrahieren:** Validierung von Pflichtfeldern (`id`, `type`, `title`). 2. **Frontmatter extrahieren:** Validierung von Pflichtfeldern (`id`, `type`, `title`).
3. **Typauflösung:** Bestimmung des `type` via `types.yaml` (Prio: Frontmatter > Pfad > Default). 3. **Typauflösung:** Bestimmung des `type` via `types.yaml` (Prio: Frontmatter > Pfad > Default).
4. **Note-Payload generieren:** Erstellen des JSON-Objekts für `mindnet_notes`. 4. **Note-Payload generieren:** Erstellen des JSON-Objekts für `mindnet_notes`.
5. **Chunking anwenden:** Zerlegung des Textes basierend auf dem `chunk_profile` des Typs (z.B. `sliding_smart_edges`). 5. **Chunking anwenden:** Zerlegung des Textes basierend auf dem `chunk_profile` des Typs.
6. **Smart Edge Allocation (Neu in WP15):** 6. **Inline-Kanten finden:** Parsing von `[[rel:...]]` im Fließtext.
* Wenn in `types.yaml` aktiviert (`enable_smart_edge_allocation`): 7. **Callout-Kanten finden:** Parsing von `> [!edge]` Blöcken.
* Der `SemanticAnalyzer` sendet jeden Chunk an das LLM. 8. **Default-Edges erzeugen:** Anwendung der `edge_defaults` aus der Typ-Registry.
* **Resilienz & Traffic Control:** 9. **Strukturkanten erzeugen:** `belongs_to` (Chunk->Note), `next`/`prev` (Sequenz).
* **Priority:** Der Request nutzt `priority="background"`. 10. **Embedding & Upsert (Async):**
* **Semaphore:** Eine globale Semaphore (Limit: 2, konfigurierbar) verhindert System-Überlastung. * Das System nutzt eine **Semaphore** (Limit: 5 Files concurrent), um Ollama nicht zu überlasten.
* **Retry & Backoff:** Bei Fehlern (Timeouts) wird bis zu 5-mal mit exponentieller Wartezeit wiederholt.
* **JSON Repair:** Der Analyzer erkennt fehlerhaftes JSON (z.B. Dict statt Liste) und repariert es automatisch.
* **Safety Fallback:** Schlagen alle Versuche fehl, werden die Kanten *allen* Chunks zugewiesen, um Datenverlust zu vermeiden.
* Ergebnis: Das LLM filtert irrelevante Links aus dem Chunk ("Broadcasting" verhindern).
7. **Inline-Kanten finden:** Parsing von `[[rel:...]]` im Fließtext.
8. **Callout-Kanten finden:** Parsing von `> [!edge]` Blöcken.
9. **Default-Edges erzeugen:** Anwendung der `edge_defaults` aus der Typ-Registry.
10. **Strukturkanten erzeugen:** `belongs_to`, `next`, `prev` (Sequenz).
11. **Embedding & Upsert (Async):**
* Generierung der Vektoren via `nomic-embed-text` (768 Dim). * Generierung der Vektoren via `nomic-embed-text` (768 Dim).
12. **Strict Mode:** Der Prozess bricht sofort ab, wenn ein Embedding leer ist oder die Dimension `0` hat. 11. **Strict Mode:** Der Prozess bricht sofort ab, wenn ein Embedding leer ist oder die Dimension `0` hat.
13. **Diagnose:** Automatischer Check der Integrität nach dem Lauf. 12. **Diagnose:** Automatischer Check der Integrität nach dem Lauf.
### 2.2 Standard-Betrieb (Inkrementell) ### 2.2 Standard-Betrieb (Inkrementell)
Für regelmäßige Updates (z.B. Cronjob). Erkennt Änderungen via Hash. Für regelmäßige Updates (z.B. Cronjob). Erkennt Änderungen via Hash.
@ -112,7 +102,7 @@ Nach einem Import oder Code-Update müssen die API-Prozesse neu gestartet werden
sudo systemctl status mindnet-prod sudo systemctl status mindnet-prod
### 2.4 Full Rebuild (Clean Slate) ### 2.4 Full Rebuild (Clean Slate)
Notwendig bei Änderungen an `types.yaml` (z.B. Smart Edges an/aus) oder beim Wechsel des Embedding-Modells. Notwendig bei Änderungen an `types.yaml` (z.B. neue Chunk-Größen) oder beim Wechsel des Embedding-Modells (z.B. Update auf `nomic-embed-text`).
**WICHTIG:** Vorher das Modell pullen, sonst schlägt der Import fehl! **WICHTIG:** Vorher das Modell pullen, sonst schlägt der Import fehl!
@ -133,10 +123,10 @@ Das Chunking ist profilbasiert und typgesteuert.
### 3.1 Chunk-Profile ### 3.1 Chunk-Profile
In `types.yaml` definiert. Standard-Profile (in `chunk_config.py` implementiert): In `types.yaml` definiert. Standard-Profile (in `chunk_config.py` implementiert):
* `sliding_short`: Max 128 Tokens (z.B. für Logs, Chats). * `short`: Max 128 Tokens (z.B. für Logs, Chats).
* `sliding_standard`: Max 512 Tokens (Standard für Massendaten). * `medium`: Max 256 Tokens (z.B. für Konzepte).
* `sliding_smart_edges`: Sliding Window, optimiert für LLM-Analyse (Fließtext). * `long`: Max 512 Tokens (z.B. für Essays, Projekte).
* `structured_smart_edges`: Trennt strikt an Überschriften (für strukturierte Daten). * `by_heading`: Trennt strikt an Überschriften.
### 3.2 Payload-Felder ### 3.2 Payload-Felder
Jeder Chunk erhält zwei Text-Felder: Jeder Chunk erhält zwei Text-Felder:
@ -147,7 +137,7 @@ Jeder Chunk erhält zwei Text-Felder:
## 4. Edge-Erzeugung (Die V2-Logik) ## 4. Edge-Erzeugung (Die V2-Logik)
In v2.6 entstehen Kanten nach strenger Priorität. In v2.2 entstehen Kanten nach strenger Priorität.
### 4.1 Prioritäten & Provenance ### 4.1 Prioritäten & Provenance
Der Importer setzt `provenance`, `rule_id` und `confidence` automatisch: Der Importer setzt `provenance`, `rule_id` und `confidence` automatisch:
@ -157,17 +147,10 @@ Der Importer setzt `provenance`, `rule_id` und `confidence` automatisch:
| **1** | Inline | `[[rel:depends_on X]]` | `inline:rel` | ~0.95 | | **1** | Inline | `[[rel:depends_on X]]` | `inline:rel` | ~0.95 |
| **2** | Callout | `> [!edge] related_to: [[X]]` | `callout:edge` | ~0.90 | | **2** | Callout | `> [!edge] related_to: [[X]]` | `callout:edge` | ~0.90 |
| **3** | Wikilink | `[[X]]` | `explicit:wikilink` | 1.00 | | **3** | Wikilink | `[[X]]` | `explicit:wikilink` | 1.00 |
| **4** | Smart | *(via LLM Filter)* | `smart:llm_filter` | 0.90 | | **4** | Default | *(via types.yaml)* | `edge_defaults:...` | ~0.70 |
| **5** | Default | *(via types.yaml)* | `edge_defaults:...` | ~0.70 | | **5** | Struktur | *(automatisch)* | `structure:...` | 1.00 |
| **6** | Struktur | *(automatisch)* | `structure:...` | 1.00 |
### 4.2 Smart Edge Allocation (WP15) ### 4.2 Typ-Defaults
Dieser Mechanismus löst das Problem, dass Chunks sonst alle Links der Note erben.
* **Prozess:** Der `SemanticAnalyzer` prüft jeden Chunk: "Ist Link X im Kontext von Chunk Y relevant?"
* **Ergebnis:** Kanten werden präzise an den Chunk gebunden, nicht global an die Note.
* **Steuerung:** Das Feature wird in `types.yaml` per Typ aktiviert (`enable_smart_edge_allocation: true`).
### 4.3 Typ-Defaults
Wenn in `types.yaml` für einen Typ `edge_defaults` definiert sind, werden diese **additiv** zu expliziten Links erzeugt. Wenn in `types.yaml` für einen Typ `edge_defaults` definiert sind, werden diese **additiv** zu expliziten Links erzeugt.
* *Beispiel:* Note Typ `project` verlinkt `[[Tool A]]`. * *Beispiel:* Note Typ `project` verlinkt `[[Tool A]]`.
* *Ergebnis:* Kante `references` (explizit) UND Kante `depends_on` (Default). * *Ergebnis:* Kante `references` (explizit) UND Kante `depends_on` (Default).
@ -182,23 +165,25 @@ Der Datenfluss endet nicht beim Finden. Er geht weiter bis zur Antwort.
Der `/chat` Endpunkt nutzt **Hybrid Retrieval** (Semantic + Graph), um auch logisch verbundene, aber textlich unterschiedliche Notizen zu finden (z.B. Decisions zu einem Projekt). Der `/chat` Endpunkt nutzt **Hybrid Retrieval** (Semantic + Graph), um auch logisch verbundene, aber textlich unterschiedliche Notizen zu finden (z.B. Decisions zu einem Projekt).
### 5.2 Intent Router (WP06/07) ### 5.2 Intent Router (WP06/07)
Der Request durchläuft den **Hybrid Router v5**: Der Request durchläuft den **Hybrid Router**:
1. **Question Detection:** Ist es eine Frage (`?`, W-Wörter)? -> RAG Modus. Interviews werden hier blockiert. 1. **Fast Path:** Prüfung auf `trigger_keywords` aus `decision_engine.yaml`.
2. **Keyword Scan:** Enthält es Keywords aus `types.yaml` (Objekt, z.B. "Projekt") oder `decision_engine.yaml` (Action, z.B. "erstellen")? -> INTERVIEW Modus. 2. **Slow Path:** Falls kein Keyword matched und `llm_fallback_enabled=true`, klassifiziert das LLM den Intent.
3. **LLM Fallback:** Wenn unklar, entscheidet das LLM. * `FACT`: Wissen abfragen.
* `DECISION`: Rat suchen.
* `EMPATHY`: Trost suchen.
* `INTERVIEW`: Wissen eingeben (Neu in WP07).
3. **Result:** Auswahl der Strategie und der `inject_types` oder `schemas`.
### 5.3 Context Enrichment ### 5.3 Context Enrichment
Der Router (`chat.py`) reichert die gefundenen Chunks mit Metadaten an: Der Router (`chat.py`) reichert die gefundenen Chunks mit Metadaten an:
* **Typ-Injection:** `[DECISION]`, `[PROJECT]`. * **Typ-Injection:** `[DECISION]`, `[PROJECT]`.
* **Reasoning-Infos:** `(Score: 0.75)`. * **Reasoning-Infos:** `(Score: 0.75)`.
### 5.4 Generation (LLM) mit Traffic Control ### 5.4 Generation (LLM)
* **Engine:** Ollama (lokal). * **Engine:** Ollama (lokal).
* **Modell:** `phi3:mini` (Standard). * **Modell:** `phi3:mini` (Standard).
* **Prompting:** Template wird basierend auf Intent gewählt (`decision_template`, `interview_template` etc.). * **Prompting:** Template wird basierend auf Intent gewählt (`decision_template`, `interview_template` etc.).
* **Traffic Control:** Der `LLMService` unterscheidet: * **One-Shot (WP07):** Im Interview-Modus generiert das LLM direkt einen Markdown-Block ohne Rückfragen.
* **Chat-Requests** (`priority="realtime"`) -> Sofortige Ausführung.
* **Import-Requests** (`priority="background"`) -> Gedrosselt durch Semaphore (Standard: 2).
### 5.5 Active Intelligence Pipeline (Neu in v2.4) ### 5.5 Active Intelligence Pipeline (Neu in v2.4)
Ein paralleler Datenfluss im Frontend ("Draft Editor") zur Unterstützung des Autors. Ein paralleler Datenfluss im Frontend ("Draft Editor") zur Unterstützung des Autors.
@ -267,7 +252,7 @@ Wie entwickeln wir die Pipeline weiter?
--- ---
## 16. Workpackage Status (v2.6.0) ## 16. Workpackage Status (v2.4.0)
Aktueller Implementierungsstand der Module. Aktueller Implementierungsstand der Module.
@ -286,6 +271,3 @@ Aktueller Implementierungsstand der Module.
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). | | **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** | | **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** | | **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** |
| **WP15** | Smart Edge Allocation | 🟢 Live | **LLM-Filter & Traffic Control aktiv.** |
| **WP16** | Auto-Discovery | 🟡 Geplant | UX & Retrieval Tuning. |
| **WP17** | Conversational Memory | 🟡 Geplant | Dialog-Gedächtnis. |

View File

@ -1,7 +1,7 @@
# Mindnet v2.4 User Guide # Mindnet v2.4 User Guide
**Datei:** `docs/mindnet_user_guide_v2.6.md` **Datei:** `docs/mindnet_user_guide_v2.4.md`
**Stand:** 2025-12-12 **Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. Smart Edges, Hybrid Router v5 & Healing UI) **Status:** **FINAL** (Inkl. RAG, Web-Interface & Interview-Assistent & Intelligence)
**Quellen:** `knowledge_design.md`, `wp04_retriever_scoring.md`, `Programmplan_V2.2.md`, `Handbuch.md`. **Quellen:** `knowledge_design.md`, `wp04_retriever_scoring.md`, `Programmplan_V2.2.md`, `Handbuch.md`.
> **Willkommen bei Mindnet.** > **Willkommen bei Mindnet.**
@ -18,7 +18,6 @@ Wenn du nach "Projekt Alpha" suchst, findet Mindnet nicht nur das Dokument. Es f
* **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".
* **Neu in v2.6 (Smart Edges):** Mindnet zeigt dir präzise Verknüpfungen an, die für den spezifischen Textabschnitt relevant sind, statt pauschal alle Links der Notiz anzuzeigen.
### 1.2 Der Zwilling (Die Personas) ### 1.2 Der Zwilling (Die Personas)
Mindnet passt seinen Charakter dynamisch an deine Frage an: Mindnet passt seinen Charakter dynamisch an deine Frage an:
@ -53,22 +52,28 @@ Seit Version 2.3.1 bedienst du Mindnet über eine grafische Oberfläche im Brows
## 3. Den Chat steuern (Intents) ## 3. Den Chat steuern (Intents)
Du steuerst die Persönlichkeit von Mindnet durch deine Wortwahl. Das System nutzt einen **Hybrid Router v5**, der intelligent zwischen Frage und Befehl unterscheidet. Du steuerst die Persönlichkeit von Mindnet durch deine Wortwahl. Das System nutzt einen **Hybrid Router**, der sowohl auf Schlüsselwörter als auch auf die Bedeutung achtet.
### 3.1 Frage-Modus (Wissen abrufen) ### 3.1 Modus: Entscheidung ("Der Berater")
Sobald du ein Fragezeichen `?` benutzt oder Wörter wie "Wer", "Wie", "Was", "Soll ich" verwendest, sucht Mindnet nach Antworten (**RAG**). Wenn du vor einer Wahl stehst, hilft Mindnet dir, konform zu deinen Prinzipien zu bleiben.
* **Entscheidung ("Soll ich?"):** Mindnet lädt deine **Werte** (`type: value`) und **Ziele** (`type: goal`) in den Kontext und prüft die Fakten dagegen. * **Auslöser (Keywords):** "Soll ich...", "Was ist deine Meinung?", "Strategie für...", "Vor- und Nachteile".
* *Beispiel:* "Soll ich Tool X nutzen?" -> "Nein, Tool X speichert Daten in den USA. Das verstößt gegen dein Prinzip 'Privacy First' und dein Ziel 'Digitale Autarkie'." * **Was passiert:** Mindnet lädt deine **Werte** (`type: value`) und **Ziele** (`type: goal`) in den Kontext und prüft die Fakten dagegen.
* **Empathie ("Ich fühle..."):** Mindnet lädt deine **Erfahrungen** (`type: experience`) und **Glaubenssätze** (`type: belief`). Es antwortet verständnisvoll und zitiert deine eigenen Lektionen. * **Beispiel-Dialog:**
* *Beispiel:* "Ich bin frustriert." -> "Das erinnert mich an Projekt Y, da ging es uns ähnlich..." * *Du:* "Soll ich Tool X nutzen?"
* *Mindnet:* "Nein. Tool X speichert Daten in den USA. Das verstößt gegen dein Prinzip 'Privacy First' und dein Ziel 'Digitale Autarkie'."
### 3.2 Befehls-Modus (Wissen erfassen / Interview) ### 3.2 Modus: Empathie ("Der Spiegel")
Wenn du keine Frage stellst, sondern eine Absicht äußerst, wechselt Mindnet in den **Interview-Modus**. Wenn du frustriert bist oder reflektieren willst, wechselt Mindnet in den "Ich"-Modus.
* **Auslöser (Keywords & Semantik):** "Ich fühle mich...", "Traurig", "Gestresst", "Alles ist sinnlos", "Ich bin überfordert".
* **Was passiert:** Mindnet lädt deine **Erfahrungen** (`type: experience`) und **Glaubenssätze** (`type: belief`). Es antwortet verständnisvoll und zitiert deine eigenen Lektionen.
### 3.3 Modus: Interview ("Der Analyst")
Wenn du Wissen festhalten willst, statt zu suchen.
* **Auslöser:** "Neues Projekt", "Notiz erstellen", "Ich will etwas festhalten", "Neue Entscheidung dokumentieren". * **Auslöser:** "Neues Projekt", "Notiz erstellen", "Ich will etwas festhalten", "Neue Entscheidung dokumentieren".
* **Was passiert:** Mindnet sucht nicht im Archiv, sondern öffnet den **Draft-Editor**. * **Was passiert:** Siehe Kapitel 6.3.
* **Beispiel:** "Neue Erfahrung: Streit am Recyclinghof." -> Das System erstellt sofort eine strukturierte Notiz mit den Feldern "Situation", "Reaktion" und "Learning".
--- ---
@ -122,10 +127,9 @@ Mindnet kann dir helfen, Markdown-Notizen zu schreiben.
2. **Generierung:** Mindnet erkennt den Wunsch (`INTERVIEW`), analysiert den Satz und erstellt **sofort** einen Entwurf. 2. **Generierung:** Mindnet erkennt den Wunsch (`INTERVIEW`), analysiert den Satz und erstellt **sofort** einen Entwurf.
3. **Editor:** Die UI wechselt von der Chat-Blase zu einem **Draft-Editor**. 3. **Editor:** Die UI wechselt von der Chat-Blase zu einem **Draft-Editor**.
* Du siehst das generierte Frontmatter (`type: project`, `status: draft`). * Du siehst das generierte Frontmatter (`type: project`, `status: draft`).
* **Healing UI (v2.5):** Falls das LLM Syntax-Fehler gemacht hat (z.B. fehlendes `---`), hat der Editor das bereits automatisch repariert.
* Du siehst den Body-Text mit Platzhaltern (`[TODO]`), wo Infos fehlten (z.B. Stakeholder). * Du siehst den Body-Text mit Platzhaltern (`[TODO]`), wo Infos fehlten (z.B. Stakeholder).
4. **Finalisierung:** Ergänze die fehlenden Infos direkt im Editor und klicke auf **Download** oder **Kopieren**. 4. **Finalisierung:** Ergänze die fehlenden Infos direkt im Editor und klicke auf **Download** oder **Kopieren**.
5. **Speichern:** Klicke auf "💾 Speichern". Die Notiz landet sofort im Vault und im Index. 5. **Speichern:** Speichere die Datei in deinen Obsidian Vault. Beim nächsten Import ist sie im System.
### 6.4 Der Intelligence-Workflow (Neu in v2.4) ### 6.4 Der Intelligence-Workflow (Neu in v2.4)
Wenn du Texte im **manuellen Editor** schreibst, unterstützt dich Mindnet aktiv bei der Vernetzung: Wenn du Texte im **manuellen Editor** schreibst, unterstützt dich Mindnet aktiv bei der Vernetzung:

View File

@ -11,13 +11,6 @@ import logging
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
import logging
# Setzt das Level global auf INFO, damit Sie den Fortschritt sehen
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
# Wenn Sie TIEFE Einblicke wollen, setzen Sie den SemanticAnalyzer spezifisch auf DEBUG:
logging.getLogger("app.services.semantic_analyzer").setLevel(logging.DEBUG)
# Importiere den neuen Async Service # Importiere den neuen Async Service
# Stellen wir sicher, dass der Pfad stimmt (Pythonpath) # Stellen wir sicher, dass der Pfad stimmt (Pythonpath)
import sys import sys

View File

@ -1,105 +0,0 @@
import unittest
import asyncio
import os
import shutil
from pathlib import Path
from app.routers.ingest import save_note, SaveRequest
from app.core.ingestion import IngestionService
# Wir simulieren den API-Aufruf direkt
class TestDialogFullFlow(unittest.IsolatedAsyncioTestCase):
def setUp(self):
# Test Vault definieren
self.test_vault = os.path.abspath("./vault_test_dialog")
os.environ["MINDNET_VAULT_ROOT"] = self.test_vault
# Sicherstellen, dass das Verzeichnis existiert
if not os.path.exists(self.test_vault):
os.makedirs(self.test_vault)
def tearDown(self):
# Cleanup nach dem Test (optional, zum Debuggen auskommentieren)
if os.path.exists(self.test_vault):
shutil.rmtree(self.test_vault)
async def test_a_save_journal_entry(self):
"""
Testet den 'Fast Path' (Journal) -> sliding_short.
Erwartung: Sehr schnell, keine LLM-Warnungen im Log.
"""
print("\n--- TEST A: Journal Save (Fast Path) ---")
content = """---
type: journal
status: active
title: Test Journal Entry
---
# Daily Log
Heute haben wir das Chunking-System getestet.
Es lief alles sehr schnell durch.
"""
req = SaveRequest(
markdown_content=content,
filename="2025-12-12-test-journal.md",
folder="00_Inbox"
)
# Rufe die API-Funktion auf (simuliert Frontend "Save")
response = await save_note(req)
# Checks
self.assertEqual(response.status, "success")
self.assertTrue(response.stats['chunks'] > 0, "Keine Chunks erstellt!")
# File Check
file_path = Path(response.file_path)
self.assertTrue(file_path.exists(), "Datei wurde nicht geschrieben!")
print(f"✅ Journal gespeichert: {response.note_id}")
async def test_b_save_project_entry(self):
"""
Testet den 'Smart Path' (Project) -> sliding_smart_edges.
Erwartung: Smart Edge Allocation läuft (LLM Aufruf), dauert etwas länger.
"""
print("\n--- TEST B: Project Save (Smart Path) ---")
# Ein Text mit expliziten Kanten, um den Smart Chunker zu triggern
content = """---
type: project
status: active
title: Test Projekt Smart Chunking
---
# Mission
Wir wollen [[leitbild-werte#Integrität]] sicherstellen.
Das System muss stabil sein.
# Status
Wir nutzen [[rel:uses|Ollama]] für die Intelligenz.
"""
req = SaveRequest(
markdown_content=content,
filename="Test_Projekt_Smart.md",
folder="Projects"
)
# API Call
response = await save_note(req)
# Checks
self.assertEqual(response.status, "success")
self.assertTrue(response.stats['chunks'] > 0)
self.assertTrue(response.stats['edges'] > 0, "Kanten sollten gefunden werden (mind. structure edges)")
print(f"✅ Projekt gespeichert: {response.note_id}")
# Optional: Prüfen ob Qdrant Daten hat (via IngestionService Helper)
service = IngestionService()
chunks_missing, edges_missing = service._artifacts_missing(response.note_id)
self.assertFalse(chunks_missing, "Chunks fehlen in Qdrant!")
self.assertFalse(edges_missing, "Edges fehlen in Qdrant!")
print("✅ Qdrant Persistenz verifiziert.")
if __name__ == '__main__':
unittest.main()

View File

@ -1,110 +0,0 @@
# tests/test_final_wp15_validation.py
import asyncio
import unittest
from typing import List, Dict, Any
import re
from pathlib import Path
import sys
# --- PFAD-KORREKTUR ---
ROOT_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT_DIR))
from app.core import chunker
from app.services.semantic_analyzer import SemanticAnalyzer
def get_config_for_test(strategy: str, enable_smart_edge: bool) -> Dict[str, Any]:
cfg = chunker.get_chunk_config("concept")
cfg['strategy'] = strategy
cfg['enable_smart_edge_allocation'] = enable_smart_edge
# WICHTIG: Setze sehr kleine Werte, um Split bei kurzem Text zu erzwingen
cfg['target'] = 50
cfg['max'] = 100
return cfg
TEST_NOTE_ID_SMART = "20251212-test-smart"
TEST_NOTE_ID_LEGACY = "20251212-test-legacy"
TEST_MARKDOWN_SMART = """
---
id: 20251212-test-smart
title: Integrationstest - Smart Edges
type: concept
status: active
---
# Teil 1: Wichtige Definition
Die Mission ist: präsent sein.
Dies entspricht unseren Werten [[leitbild-werte#Integrität]].
## Teil 2: Konflikt
Der Konflikt zwischen [[leitbild-rollen#Vater]] und [[leitbild-rollen#Beruf]].
Lösung: [[rel:depends_on leitbild-review#Weekly Review]].
"""
# Verlängerter Text, um Split > 1 zu erzwingen (bei Target 50)
TEST_MARKDOWN_SLIDING = """
---
id: 20251212-test-legacy
title: Fließtext Protokoll
type: journal
status: active
---
Dies ist der erste Absatz. Er muss lang genug sein, damit der Chunker ihn schneidet.
Wir schreiben hier über Rituale wie [[leitbild-rituale-system]] und viele andere Dinge.
Das Wetter ist schön und die Programmierung läuft gut. Dies sind Füllsätze für Länge.
Dies ist der zweite Absatz. Er ist durch eine Leerzeile getrennt und sollte einen neuen Kontext bilden.
Auch hier schreiben wir viel Text, damit die Token-Anzahl die Grenze von 50 Tokens überschreitet.
Das System muss hier splitten.
"""
class TestFinalWP15Integration(unittest.TestCase):
_analyzer_instance = None
@classmethod
def setUpClass(cls):
cls._analyzer_instance = SemanticAnalyzer()
chunker._semantic_analyzer_instance = cls._analyzer_instance
@classmethod
def tearDownClass(cls):
# FIX: Kein explizites Loop-Closing hier, um RuntimeError zu vermeiden
pass
def test_a_smart_edge_allocation(self):
"""A: Prüft Smart Edge Allocation (LLM-Filter)."""
config = get_config_for_test('by_heading', enable_smart_edge=True)
chunks = asyncio.run(chunker.assemble_chunks(
note_id=TEST_NOTE_ID_SMART,
md_text=TEST_MARKDOWN_SMART,
note_type='concept',
config=config
))
self.assertTrue(len(chunks) >= 2, f"A1 Fehler: Erwartete >= 2 Chunks, bekam {len(chunks)}")
print(f" -> Chunks generiert (Smart): {len(chunks)}")
def test_b_backward_compatibility(self):
"""B: Prüft Sliding Window (Legacy)."""
config = get_config_for_test('sliding_window', enable_smart_edge=False)
chunks = asyncio.run(chunker.assemble_chunks(
note_id=TEST_NOTE_ID_LEGACY,
md_text=TEST_MARKDOWN_SLIDING,
note_type='journal',
config=config
))
# Sliding Window muss bei diesem langen Text > 1 Chunk liefern
self.assertTrue(len(chunks) >= 2, f"B1 Fehler: Sliding Window lieferte nur {len(chunks)} Chunk(s). Split defekt.")
# Check: Keine LLM Kanten (da deaktiviert)
injected = re.search(r'\[\[rel:', chunks[0].text)
self.assertIsNone(injected, "B2 Fehler: LLM-Kanten trotz Deaktivierung gefunden!")
print(f" -> Chunks generiert (Legacy): {len(chunks)}")
if __name__ == '__main__':
unittest.main()

View File

@ -1,147 +0,0 @@
# tests/test_smart_chunking_integration.py (Letzte Korrektur zur Umgehung des AsyncIO-Fehlers)
import asyncio
import unittest
import os
import sys
from pathlib import Path
from typing import List, Dict
# --- PFAD-KORREKTUR ---
ROOT_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT_DIR))
# ----------------------
# Import der Kernkomponenten
from app.core import chunker
from app.core import derive_edges
from app.services.semantic_analyzer import SemanticAnalyzer
# 1. Definieren der Test-Note (Simuliert eine journal.md Datei)
TEST_NOTE_ID = "20251211-journal-sem-test"
TEST_NOTE_TYPE = "journal"
TEST_MARKDOWN = """
---
id: 20251211-journal-sem-test
title: Tägliches Log - Semantischer Test
type: journal
status: active
created: 2025-12-11
tags: [test, daily-log]
---
# Log-Eintrag 2025-12-11
Heute war ein guter Tag. Zuerst habe ich mit der R1 Meditation begonnen, um meinen Nordstern Fokus zu klären. Das Ritual [[leitbild-rituale-system]] hat mir geholfen, ruhig in den Tag zu starten. Ich habe gespürt, wie wichtig meine [[leitbild-werte#Integrität]] für meine Entscheidungen ist. Das ist das Fundament.
Am Nachmittag gab es einen Konflikt bei der Karate-Trainer-Ausbildung. Ein Schüler war uneinsichtig. Ich habe die Situation nach [[leitbild-prinzipien#P4 Gerechtigkeit & Fairness]] behandelt und beide Seiten gehört (Steelman). Das war anstrengend, aber ich habe meine [[leitbild-rollen#Karate-Trainer]] Mission erfüllt. Die Konsequenz war klar und ruhig.
Abends habe ich den wöchentlichen Load-Check mit meinem Partner gemacht. Das Paar-Ritual [[leitbild-rituale-system#R5]] hilft, das Ziel [[leitbild-ziele-portfolio#Nordstern Partner]] aktiv zu verfolgen. Es ist der operative Rhythmus für uns beide.
"""
# --- ENTFERNEN DER KOMPLEXEN TEARDOWN-HILFEN ---
# Wir entfernen die fehleranfällige asynchrone Schließungslogik.
class TestSemanticChunking(unittest.TestCase):
_analyzer_instance = None
@classmethod
def setUpClass(cls):
"""Initialisiert den SemanticAnalyzer einmalig."""
cls._analyzer_instance = SemanticAnalyzer()
# Stellt sicher, dass der Chunker diese Singleton-Instanz verwendet
chunker._semantic_analyzer_instance = cls._analyzer_instance
@classmethod
def tearDownClass(cls):
"""
PRAGMATISCHE LÖSUNG: Überspringe das explizite Aclose() im Teardown,
um den Event Loop Konflikt zu vermeiden. Die GC wird die Verbindung schließen.
"""
pass
def setUp(self):
self.config = chunker.get_chunk_config(TEST_NOTE_TYPE)
def test_a_strategy_selection(self):
"""Prüft, ob die Strategie 'semantic_llm' für den Typ 'journal' gewählt wird."""
self.assertEqual(self.config['strategy'], 'semantic_llm',
"Fehler: 'journal' sollte die Strategie 'semantic_llm' nutzen.")
def test_b_llm_chunking_and_injection(self):
"""
Prüft den gesamten End-to-End-Flow: 1. LLM-Chunking, 2. Kanten-Injektion, 3. Kanten-Erkennung.
"""
# --- 1. Chunking (Asynchron) ---
chunks = asyncio.run(chunker.assemble_chunks(
note_id=TEST_NOTE_ID,
md_text=TEST_MARKDOWN,
note_type=TEST_NOTE_TYPE
))
print(f"\n--- LLM Chunker Output: {len(chunks)} Chunks ---")
# Assertion B1: Zerlegung (Das LLM muss mehr als 1 Chunk liefern)
self.assertTrue(len(chunks) > 1,
"Assertion B1 Fehler: LLM hat nicht zerlegt (Fallback aktiv). Prüfe LLM-Stabilität.")
# --- 2. Injektion prüfen ---
chunk_1_text = chunks[0].text
self.assertIn("[[rel:", chunk_1_text,
"Assertion B2 Fehler: Der Chunk-Text muss die injizierte [[rel: Kante enthalten.")
# --- 3. Kanten-Derivation (Synchron) ---
edges = derive_edges.build_edges_for_note(
note_id=TEST_NOTE_ID,
chunks=[c.__dict__ for c in chunks]
)
print(f"--- Edge Derivation Output: {len(edges)} Kanten ---")
# Assertion B3: Mindestens 3 LLM-Kanten (inline:rel)
llm_generated_edges = [
e for e in edges
if e.get('rule_id') == 'inline:rel' and e.get('source_id').startswith(TEST_NOTE_ID + '#sem')
]
self.assertTrue(len(llm_generated_edges) >= 3,
"Assertion B3 Fehler: Es wurden weniger als 3 semantische Kanten gefunden.")
# Assertion B4: Check für die Matrix-Logik / Werte-Kante
has_matrix_kante = any(
e['target_id'].startswith('leitbild-werte') and e['kind'] in ['based_on', 'derived_from']
for e in llm_generated_edges
)
self.assertTrue(has_matrix_kante,
"Assertion B4 Fehler: Die Matrix-Logik / Werte-Kante wurde nicht erkannt.")
print("\n✅ Integrationstest für Semantic Chunking erfolgreich.")
def test_c_draft_status_prevention(self):
"""Prüft, ob 'draft' Status semantic_llm auf by_heading überschreibt."""
DRAFT_MARKDOWN = TEST_MARKDOWN.replace("status: active", "status: draft")
# 1. Chunking mit Draft Status
chunks = asyncio.run(chunker.assemble_chunks(
note_id=TEST_NOTE_ID,
md_text=DRAFT_MARKDOWN,
note_type=TEST_NOTE_TYPE
))
# 2. Prüfen der Chunker-IDs
self.assertFalse(chunks[0].id.startswith(TEST_NOTE_ID + '#sem'),
"Assertion C1 Fehler: LLM-Chunking wurde für den Status 'draft' nicht verhindert.")
self.assertTrue(chunks[0].id.startswith(TEST_NOTE_ID + '#c'),
"Assertion C2 Fehler: Fallback-Strategie 'by_heading' wurde nicht korrekt ausgeführt.")
print(f"\n✅ Prevention Test: Draft-Status hat LLM-Chunking verhindert (Fallback ID: {chunks[0].id}).")
if __name__ == '__main__':
print("Starte den Semantic Chunking Integrationstest.")
unittest.main()

View File

@ -1,76 +0,0 @@
import unittest
import asyncio
from unittest.mock import MagicMock, patch
from app.core import chunker
class TestWP15Orchestration(unittest.TestCase):
def setUp(self):
# Basis Config
self.config = {
"strategy": "sliding_window",
"enable_smart_edge_allocation": True,
"target": 100, "max": 200
}
@patch("app.core.chunker.get_semantic_analyzer")
@patch("app.core.chunker.build_edges_for_note")
def test_smart_allocation_flow(self, mock_build_edges, mock_get_analyzer):
"""
Prüft, ob Kanten gefunden, gefiltert und injiziert werden.
"""
# 1. Mock Edge Discovery (Simuliert derive_edges.py)
# Wir simulieren, dass im Text 2 Kanten gefunden wurden.
mock_build_edges.return_value = [
{"kind": "uses", "target_id": "tool_a"},
{"kind": "references", "target_id": "doc_b"}
]
# 2. Mock LLM Analyzer (Simuliert semantic_analyzer.py)
mock_analyzer_instance = MagicMock()
mock_get_analyzer.return_value = mock_analyzer_instance
# Simuliere LLM Antwort: Chunk 1 bekommt "tool_a", Chunk 2 bekommt nichts.
async def mock_assign(text, candidates, type):
if "Tool A" in text:
return ["uses:tool_a"]
return []
mock_analyzer_instance.assign_edges_to_chunk.side_effect = mock_assign
# 3. Run Chunker
md_text = """
# Intro
Hier nutzen wir Tool A für Tests.
# Outro
Hier ist nur Text ohne Tool.
"""
# Wir führen assemble_chunks aus (im Event Loop des Tests)
chunks = asyncio.run(chunker.assemble_chunks(
"test_note", md_text, "concept", config=self.config
))
# 4. Assertions
# Check: Wurde derive_edges aufgerufen?
mock_build_edges.assert_called_once()
# Check: Wurde LLM Analyzer aufgerufen?
self.assertTrue(mock_analyzer_instance.assign_edges_to_chunk.called)
# Check: Injection in Chunk 1 (Tool A Text)
chunk_with_tool = next((c for c in chunks if "Tool A" in c.text), None)
self.assertIsNotNone(chunk_with_tool)
self.assertIn("[[rel:uses|tool_a]]", chunk_with_tool.text, "Kante wurde nicht injiziert!")
# Check: Fallback (Die Kante 'references:doc_b' wurde vom LLM nirgends zugeordnet)
# Sie sollte also in ALLEN Chunks als Fallback landen.
for c in chunks:
self.assertIn("[[rel:references|doc_b]]", c.text, "Fallback-Kante fehlt!")
print("✅ WP-15 Logic Test passed.")
if __name__ == '__main__':
unittest.main()