WP15 #9

Merged
Lars merged 54 commits from WP15 into main 2025-12-13 06:39:48 +01:00
26 changed files with 2022 additions and 967 deletions

View File

@ -1,6 +1,6 @@
# mindnet v2.4 — Programmplan
**Version:** 2.4.0 (Inkl. WP-11 Backend Intelligence)
**Stand:** 2025-12-11
**Version:** 2.6.0 (Inkl. WP-15 Smart Edge Allocation)
**Stand:** 2025-12-12
**Status:** Aktiv
---
@ -33,6 +33,9 @@
- [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-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)
- [8. Laufzeit- \& Komplexitätsindikatoren (aktualisiert)](#8-laufzeit---komplexitätsindikatoren-aktualisiert)
- [9. Programmfortschritt (Ampel, aktualisiert)](#9-programmfortschritt-ampel-aktualisiert)
@ -514,6 +517,63 @@ 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)
WP01 → WP02 → WP03 → WP04a
@ -522,6 +582,8 @@ Aufräumen, dokumentieren, stabilisieren insbesondere für Onboarding Dritte
WP07 → WP10a
WP03 → WP09
WP01/WP03 → WP10 → WP11 → WP12
WP10 → WP17
WP11 → WP15 → WP16
WP03/WP04 → WP13
Alles → WP14
@ -544,6 +606,9 @@ Aufräumen, dokumentieren, stabilisieren insbesondere für Onboarding Dritte
| WP12 | Niedrig/Mittel | Mittel |
| WP13 | Mittel | Mittel |
| WP14 | Mittel | Niedrig/Mittel |
| WP15 | Mittel | Hoch |
| WP16 | Mittel | Hoch |
| WP17 | Mittel | Mittel |
---
@ -569,6 +634,9 @@ Aufräumen, dokumentieren, stabilisieren insbesondere für Onboarding Dritte
| WP12 | 🟡 |
| WP13 | 🟡 |
| WP14 | 🟡 |
| WP15 | 🟢 |
| WP16 | 🟡 |
| WP17 | 🟡 |
---

View File

@ -1,13 +0,0 @@
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,226 +1,330 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Dict, Optional, Tuple
from typing import List, Dict, Optional, Tuple, Any, Set
import re
import math
import yaml
from pathlib import Path
from markdown_it import MarkdownIt
from markdown_it.token import Token
from .chunk_config import get_sizes
import asyncio
import logging
# --- Hilfen ---
_SENT_SPLIT = re.compile(r'(?<=[.!?])\s+(?=[A-ZÄÖÜ0-9„(])')
_WS = re.compile(r'\s+')
# Services
from app.services.semantic_analyzer import get_semantic_analyzer
# 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:
# leichte Approximation: 1 Token ≈ 4 Zeichen; robust + schnell
t = len(text.strip())
return max(1, math.ceil(t / 4))
return max(1, math.ceil(len(text.strip()) / 4))
def split_sentences(text: str) -> list[str]:
text = _WS.sub(' ', text.strip())
if not text:
return []
if not text: return []
parts = _SENT_SPLIT.split(text)
return [p.strip() for p in parts if p.strip()]
@dataclass
class RawBlock:
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"
kind: str; text: str; level: Optional[int]; section_path: str; section_title: Optional[str]
@dataclass
class Chunk:
id: str
note_id: str
index: int
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
id: str; note_id: str; index: int; text: str; window: str; token_count: int
section_title: Optional[str]; section_path: str
neighbors_prev: Optional[str]; neighbors_next: Optional[str]
suggested_edges: Optional[List[str]] = None
# --- Markdown zu RawBlocks: H2/H3 als Sections, andere Blöcke gruppiert ---
def parse_blocks(md_text: str) -> List[RawBlock]:
md = MarkdownIt("commonmark").enable("table")
tokens: List[Token] = md.parse(md_text)
# ==========================================
# 3. PARSING & STRATEGIES (SYNCHRON)
# ==========================================
blocks: List[RawBlock] = []
h2, h3 = None, None
def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]:
"""Zerlegt Text in logische Blöcke (Absätze, Header)."""
blocks = []
h1_title = "Dokument"
section_path = "/"
cur_text = []
cur_kind = None
current_h2 = None
fm, text_without_fm = extract_frontmatter_from_text(md_text)
h1_match = re.search(r'^#\s+(.*)', text_without_fm, re.MULTILINE)
if h1_match:
h1_title = h1_match.group(1).strip()
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))
i = 0
while i < len(tokens):
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]
if t.type in ("fence", "code_block"):
# Codeblock hat eigenen content im selben Token
content = t.content or ""
push(kind, content, None)
else:
# 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
return blocks
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
blocks = parse_blocks(md_text)
chunks: List[Chunk] = []
buf: List[Tuple[str, str, str]] = [] # (text, section_title, section_path)
char_pos = 0
def flush_buffer(force=False):
nonlocal buf, chunks, char_pos
if not buf:
return
text = "\n\n".join([b[0] for b in buf]).strip()
if not text:
buf = []
return
# Wenn zu groß, satzbasiert weich umbrechen
toks = estimate_tokens(text)
if toks > max_tokens:
sentences = split_sentences(text)
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:
cur.append(s)
cur_tokens += st
if cur:
_emit("\n".join(cur))
lines = text_without_fm.split('\n')
buffer = []
for line in lines:
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:
_emit(text)
buffer.append(line)
if buffer:
content = "\n".join(buffer).strip()
if content:
blocks.append(RawBlock("paragraph", content, None, section_path, current_h2))
return blocks, h1_title
def _strategy_sliding_window(blocks: List[RawBlock], config: Dict[str, Any], note_id: str, doc_title: str = "", context_prefix: str = "") -> List[Chunk]:
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):
idx = len(chunks)
chunks.append(Chunk(
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():
nonlocal buf
if not buf: return
text_body = "\n\n".join([b.text for b in buf])
win_body = f"{context_prefix}\n{text_body}".strip() if context_prefix else text_body
if estimate_tokens(text_body) <= max_tokens:
_create_chunk(text_body, win_body, buf[-1].section_title, buf[-1].section_path)
else:
sentences = split_sentences(text_body)
current_chunk_sents = []
current_len = 0
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:
break
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 = []
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:
if b.kind == "heading" and b.level in (2, 3):
# Sectionwechsel ⇒ Buffer flushen
if b.kind == "heading": continue
current_buf_text = "\n\n".join([x.text for x in buf])
if estimate_tokens(current_buf_text) + estimate_tokens(b.text) >= target:
flush_buffer()
cur_sec_title = b.text.strip()
# 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:
buf.append(b)
if estimate_tokens(b.text) >= target:
flush_buffer()
flush_buffer(force=True)
flush_buffer()
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):
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
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

View File

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

View File

@ -5,6 +5,7 @@ import os
import json
import re
import yaml
import unicodedata
from datetime import datetime
from pathlib import Path
from dotenv import load_dotenv
@ -23,7 +24,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
# --- PAGE SETUP ---
st.set_page_config(page_title="mindnet v2.3.10", page_icon="🧠", layout="wide")
st.set_page_config(page_title="mindnet v2.5", page_icon="🧠", layout="wide")
# --- CSS STYLING ---
st.markdown("""
@ -53,15 +54,6 @@ st.markdown("""
background-color: white;
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>
""", unsafe_allow_html=True)
@ -71,8 +63,18 @@ if "user_id" not in st.session_state: st.session_state.user_id = str(uuid.uuid4(
# --- 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):
"""Sanitizer: Stellt sicher, dass nur erlaubte Felder im Frontmatter bleiben."""
ALLOWED_KEYS = {"title", "type", "status", "tags", "id", "created", "updated", "aliases", "lang"}
clean_meta = {}
extra_content = []
@ -99,7 +101,11 @@ def normalize_meta_and_body(meta, body):
extra_content.append(f"## {header}\n{val}\n")
if all_tags:
clean_meta["tags"] = list(set(all_tags))
clean_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:
new_section = "\n".join(extra_content)
@ -110,37 +116,67 @@ def normalize_meta_and_body(meta, body):
return clean_meta, final_body
def parse_markdown_draft(full_text):
"""Robustes Parsing + Sanitization."""
clean_text = full_text
"""
HEALING PARSER: Repariert kaputten LLM Output (z.B. fehlendes schließendes '---').
"""
clean_text = full_text.strip()
pattern_block = r"```(?:markdown|md)?\s*(.*?)\s*```"
match_block = re.search(pattern_block, full_text, re.DOTALL | re.IGNORECASE)
# 1. Code-Block Wrapper entfernen
pattern_block = r"```(?:markdown|md|yaml)?\s*(.*?)\s*```"
match_block = re.search(pattern_block, clean_text, re.DOTALL | re.IGNORECASE)
if match_block:
clean_text = match_block.group(1).strip()
parts = re.split(r"^---+\s*$", clean_text, maxsplit=2, flags=re.MULTILINE)
meta = {}
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:
yaml_str = parts[1]
body_candidate = parts[2]
body = 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:
parsed = yaml.safe_load(yaml_str)
parsed = yaml.safe_load(yaml_str_clean)
if isinstance(parsed, dict):
meta = parsed
body = body_candidate.strip()
except Exception:
pass
except Exception as e:
print(f"YAML Parsing Warning: {e}")
# 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)
def build_markdown_doc(meta, body):
"""Baut das finale Dokument zusammen."""
if "id" not in meta or meta["id"] == "generated_on_save":
safe_title = re.sub(r'[^a-zA-Z0-9]', '-', meta.get('title', 'note')).lower()[:30]
meta["id"] = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}-{uuid.uuid4().hex[:4]}"
raw_title = meta.get('title', 'note')
clean_slug = slugify(raw_title)[:50] or "note"
meta["id"] = f"{datetime.now().strftime('%Y%m%d')}-{clean_slug}"
meta["updated"] = datetime.now().strftime("%Y-%m-%d")
@ -189,7 +225,6 @@ def send_chat_message(message: str, top_k: int, explain: bool):
return {"error": str(e)}
def analyze_draft_text(text: str, n_type: str):
"""Ruft den neuen Intelligence-Service (WP-11) auf."""
try:
response = requests.post(
INGEST_ANALYZE_ENDPOINT,
@ -202,12 +237,11 @@ def analyze_draft_text(text: str, n_type: str):
return {"error": str(e)}
def save_draft_to_vault(markdown_content: str, filename: str = None):
"""Ruft den neuen Persistence-Service (WP-11) auf."""
try:
response = requests.post(
INGEST_SAVE_ENDPOINT,
json={"markdown_content": markdown_content, "filename": filename},
timeout=60
timeout=API_TIMEOUT
)
response.raise_for_status()
return response.json()
@ -225,7 +259,7 @@ def submit_feedback(query_id, node_id, score, comment=None):
def render_sidebar():
with st.sidebar:
st.title("🧠 mindnet")
st.caption("v2.3.10 | Mode Switch Fix")
st.caption("v2.5 | Healing Parser")
mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0)
st.divider()
st.subheader("⚙️ Settings")
@ -240,7 +274,6 @@ def render_sidebar():
return mode, top_k, explain
def render_draft_editor(msg):
# Ensure ID Stability
if "query_id" not in msg or not msg["query_id"]:
msg["query_id"] = str(uuid.uuid4())
@ -253,7 +286,7 @@ def render_draft_editor(msg):
widget_body_key = f"{key_base}_widget_body"
data_body_key = f"{key_base}_data_body"
# --- 1. INIT STATE (Nur beim allerersten Laden der Message) ---
# --- 1. INIT STATE ---
if f"{key_base}_init" not in st.session_state:
meta, body = parse_markdown_draft(msg["content"])
if "type" not in meta: meta["type"] = "default"
@ -261,26 +294,34 @@ def render_draft_editor(msg):
tags = meta.get("tags", [])
meta["tags_str"] = ", ".join(tags) if isinstance(tags, list) else str(tags)
# Persistent Data (Source of Truth)
# Persistent Data
st.session_state[data_meta_key] = meta
st.session_state[data_sugg_key] = []
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
# --- 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.
# --- 2. RESURRECTION ---
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]
# --- 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():
# Sync Widget -> Data (Source of Truth)
st.session_state[data_body_key] = st.session_state[widget_body_key]
def _insert_text(text_to_insert):
# Insert in Widget Key und Sync Data
current = st.session_state.get(widget_body_key, "")
new_text = f"{current}\n\n{text_to_insert}"
st.session_state[widget_body_key] = new_text
@ -296,33 +337,23 @@ def render_draft_editor(msg):
st.markdown(f'<div class="draft-box">', unsafe_allow_html=True)
st.markdown("### 📝 Entwurf bearbeiten")
# Metadata Form
meta_ref = st.session_state[data_meta_key]
c1, c2 = st.columns([2, 1])
with c1:
# 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)
st.text_input("Titel", key=f"{key_base}_wdg_title", on_change=_sync_meta)
with c2:
known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle"]
curr = meta_ref["type"]
if curr not in known_types: known_types.append(curr)
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)
known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle", "risk", "belief"]
curr_type = st.session_state.get(f"{key_base}_wdg_type", meta_ref["type"])
if curr_type not in known_types: known_types.append(curr_type)
st.selectbox("Typ", known_types, key=f"{key_base}_wdg_type", 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)
st.text_input("Tags", key=f"{key_base}_wdg_tags", on_change=_sync_meta)
# Tabs
tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"])
# --- TAB 1: EDITOR ---
with tab_edit:
# Hier kein 'value' Argument mehr, da wir den Key oben (Resurrection) initialisiert haben.
st.text_area(
"Body",
key=widget_body_key,
@ -338,11 +369,11 @@ def render_draft_editor(msg):
if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"):
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, ""))
current_doc_type = st.session_state.get(f"{key_base}_wdg_type", "concept")
with st.spinner("Analysiere..."):
analysis = analyze_draft_text(text_to_analyze, meta_ref["type"])
analysis = analyze_draft_text(text_to_analyze, current_doc_type)
if "error" in analysis:
st.error(f"Fehler: {analysis['error']}")
@ -354,7 +385,6 @@ def render_draft_editor(msg):
else:
st.success(f"{len(suggestions)} Vorschläge gefunden.")
# Render List
suggestions = st.session_state[data_sugg_key]
if suggestions:
current_text_state = st.session_state.get(widget_body_key, "")
@ -380,16 +410,24 @@ def render_draft_editor(msg):
st.button(" Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,))
# --- TAB 3: SAVE ---
final_tags = [t.strip() for t in meta_ref["tags_str"].split(",") if t.strip()]
final_tags_str = st.session_state.get(f"{key_base}_wdg_tags", "")
final_tags = [t.strip() for t in final_tags_str.split(",") if t.strip()]
final_meta = {
"id": "generated_on_save",
"type": meta_ref["type"],
"title": meta_ref["title"],
"type": st.session_state.get(f"{key_base}_wdg_type", "default"),
"title": st.session_state.get(f"{key_base}_wdg_title", "").strip(),
"status": "draft",
"tags": final_tags
}
# Final Doc aus Data
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)
with tab_view:
@ -403,7 +441,13 @@ def render_draft_editor(msg):
with b1:
if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"):
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"
result = save_draft_to_vault(final_doc, filename=fname)
@ -423,7 +467,6 @@ def render_chat_interface(top_k, explain):
for idx, msg in enumerate(st.session_state.messages):
with st.chat_message(msg["role"]):
if msg["role"] == "assistant":
# Header
intent = msg.get("intent", "UNKNOWN")
src = msg.get("intent_source", "?")
icon = {"EMPATHY":"❤️", "DECISION":"⚖️", "CODING":"💻", "FACT":"📚", "INTERVIEW":"📝"}.get(intent, "🧠")
@ -432,13 +475,11 @@ def render_chat_interface(top_k, explain):
with st.expander("🐞 Debug Raw Payload", expanded=False):
st.json(msg)
# Logic
if intent == "INTERVIEW":
render_draft_editor(msg)
else:
st.markdown(msg["content"])
# Sources
if "sources" in msg and msg["sources"]:
for hit in msg["sources"]:
with st.expander(f"📄 {hit.get('note_id', '?')} ({hit.get('total_score', 0):.2f})"):

View File

@ -1,21 +1,15 @@
"""
app/routers/chat.py RAG Endpunkt (WP-06 Hybrid Router + WP-07 Interview Mode)
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)
app/routers/chat.py RAG Endpunkt
Version: 2.5.0 (Fix: Question Detection protects against False-Positive Interviews)
"""
from fastapi import APIRouter, HTTPException, Depends
from typing import List, Dict, Any
from typing import List, Dict, Any, Optional
import time
import uuid
import logging
import yaml
import os
from pathlib import Path
from app.config import get_settings
@ -30,6 +24,7 @@ logger = logging.getLogger(__name__)
# --- Helper: Config Loader ---
_DECISION_CONFIG_CACHE = None
_TYPES_CONFIG_CACHE = None
def _load_decision_config() -> Dict[str, Any]:
settings = get_settings()
@ -51,12 +46,27 @@ def _load_decision_config() -> Dict[str, Any]:
logger.error(f"Failed to load decision config: {e}")
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]:
global _DECISION_CONFIG_CACHE
if _DECISION_CONFIG_CACHE is None:
_DECISION_CONFIG_CACHE = _load_decision_config()
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]:
config = get_full_config()
strategies = config.get("strategies", {})
@ -67,40 +77,40 @@ def get_decision_strategy(intent: str) -> Dict[str, Any]:
def _detect_target_type(message: str, configured_schemas: Dict[str, Any]) -> str:
"""
Versucht zu erraten, welchen Notiz-Typ der User erstellen will.
Nutzt Keywords und Mappings.
Nutzt Keywords aus types.yaml UND Mappings.
"""
message_lower = message.lower()
# 1. Direkter Match mit Schema-Keys (z.B. "projekt", "entscheidung")
# Ignoriere 'default' hier
# 1. Check types.yaml detection_keywords (Priority!)
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 message_lower:
return type_name
# 2. Direkter Match mit Schema-Keys
for type_key in configured_schemas.keys():
if type_key == "default":
continue
if type_key == "default": continue
if type_key in message_lower:
return type_key
# 2. Synonym-Mapping (Deutsch -> Schema Key)
# Dies verbessert die UX, falls User deutsche Begriffe nutzen
# 3. Synonym-Mapping (Legacy Fallback)
synonyms = {
"projekt": "project",
"vorhaben": "project",
"entscheidung": "decision",
"beschluss": "decision",
"projekt": "project", "vorhaben": "project",
"entscheidung": "decision", "beschluss": "decision",
"ziel": "goal",
"erfahrung": "experience",
"lektion": "experience",
"erfahrung": "experience", "lektion": "experience",
"wert": "value",
"prinzip": "principle",
"grundsatz": "principle",
"notiz": "default",
"idee": "default"
"notiz": "default", "idee": "default"
}
for term, schema_key in synonyms.items():
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"
@ -126,7 +136,6 @@ def _build_enriched_context(hits: List[QueryHit]) -> str:
)
title = hit.note_id or "Unbekannt"
# [FIX] Robustes Auslesen des Typs (Payload > Source > Unknown)
payload = hit.payload or {}
note_type = payload.get("type") or source.get("type", "unknown")
note_type = str(note_type).upper()
@ -140,54 +149,77 @@ def _build_enriched_context(hits: List[QueryHit]) -> str:
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]:
"""
Hybrid Router v3:
Gibt Tuple zurück: (Intent, Source)
Hybrid Router v5:
1. Decision Keywords (Strategie) -> Prio 1
2. Type Keywords (Interview Trigger) -> Prio 2, ABER NUR WENN KEINE FRAGE!
3. LLM (Fallback) -> Prio 3
"""
config = get_full_config()
strategies = config.get("strategies", {})
settings = config.get("settings", {})
query_lower = query.lower()
best_intent = None
max_match_length = 0
# 1. FAST PATH: Keywords
# 1. FAST PATH A: Strategie Keywords (z.B. "Soll ich...")
for intent_name, strategy in strategies.items():
if intent_name == "FACT": continue
keywords = strategy.get("trigger_keywords", [])
for k in keywords:
if k.lower() in query_lower:
if len(k) > max_match_length:
max_match_length = len(k)
best_intent = intent_name
return intent_name, "Keyword (Strategy)"
if best_intent:
return best_intent, "Keyword (Fast Path)"
# 2. FAST PATH B: Type Keywords (z.B. "Projekt", "Werte") -> INTERVIEW
# FIX: Wir prüfen, ob es eine Frage ist. Fragen zu Typen sollen RAG (FACT/DECISION) sein,
# keine Interviews. Wir überlassen das dann dem LLM Router (Slow Path).
if not _is_question(query_lower):
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})"
# 2. SLOW PATH: LLM Router
# 3. SLOW PATH: LLM Router
if settings.get("llm_fallback_enabled", False):
router_prompt_template = settings.get("llm_router_prompt", "")
# Nutze Prompts aus prompts.yaml (via LLM Service)
router_prompt_template = llm.prompts.get("router_prompt", "")
if router_prompt_template:
prompt = router_prompt_template.replace("{query}", query)
logger.info("Keywords failed. Asking LLM for Intent...")
logger.info("Keywords failed (or Question detected). Asking LLM for Intent...")
raw_response = await llm.generate_raw_response(prompt)
# Parsing logic
llm_output_upper = raw_response.upper()
found_intents = []
for strat_key in strategies.keys():
if strat_key in llm_output_upper:
found_intents.append(strat_key)
if len(found_intents) == 1:
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)"
try:
# Nutze priority="realtime" für den Router, damit er nicht wartet
raw_response = await llm.generate_raw_response(prompt, priority="realtime")
llm_output_upper = raw_response.upper()
# 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():
if strat_key in llm_output_upper:
return strat_key, "LLM Router"
except Exception as e:
logger.error(f"Router LLM failed: {e}")
return "FACT", "Default (No Match)"
@ -202,7 +234,7 @@ async def chat_endpoint(
logger.info(f"Chat request [{query_id}]: {request.message[:50]}...")
try:
# 1. Intent Detection (mit Source)
# 1. Intent Detection
intent, intent_source = await _classify_intent(request.message, llm)
logger.info(f"[{query_id}] Final Intent: {intent} via {intent_source}")
@ -210,57 +242,41 @@ async def chat_endpoint(
strategy = get_decision_strategy(intent)
prompt_key = strategy.get("prompt_template", "rag_template")
# --- SPLIT LOGIC: INTERVIEW vs. RAG ---
sources_hits = []
final_prompt = ""
if intent == "INTERVIEW":
# --- WP-07: INTERVIEW MODE ---
# Kein Retrieval. Wir nutzen den Dialog-Kontext.
# --- INTERVIEW MODE ---
target_type = _detect_target_type(request.message, strategy.get("schemas", {}))
# 1. Schema Loading (Late Binding)
schemas = strategy.get("schemas", {})
target_type = _detect_target_type(request.message, schemas)
active_schema = schemas.get(target_type, schemas.get("default"))
types_cfg = get_types_config()
type_def = types_cfg.get("types", {}).get(target_type, {})
fields_list = type_def.get("schema", [])
logger.info(f"[{query_id}] Starting Interview for Type: {target_type}")
# Robustes Schema-Parsing (Dict vs List)
if isinstance(active_schema, dict):
fields_list = active_schema.get("fields", [])
hint_str = active_schema.get("hint", "")
else:
fields_list = active_schema # Fallback falls nur Liste definiert
hint_str = ""
if not fields_list:
configured_schemas = strategy.get("schemas", {})
fallback_schema = configured_schemas.get(target_type, configured_schemas.get("default"))
if isinstance(fallback_schema, dict):
fields_list = fallback_schema.get("fields", [])
else:
fields_list = fallback_schema or []
logger.info(f"[{query_id}] Interview Type: {target_type}. Fields: {len(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, "")
final_prompt = template.replace("{context_str}", context_str) \
final_prompt = template.replace("{context_str}", "Dialogverlauf...") \
.replace("{query}", request.message) \
.replace("{target_type}", target_type) \
.replace("{schema_fields}", fields_str) \
.replace("{schema_hint}", hint_str)
# Keine Hits im Interview
.replace("{schema_hint}", "")
sources_hits = []
else:
# --- WP-06: STANDARD RAG MODE ---
# --- RAG MODE ---
inject_types = strategy.get("inject_types", [])
prepend_instr = strategy.get("prepend_instruction", "")
# 2. Primary Retrieval
query_req = QueryRequest(
query=request.message,
mode="hybrid",
@ -270,9 +286,7 @@ async def chat_endpoint(
retrieve_result = await retriever.search(query_req)
hits = retrieve_result.results
# 3. Strategic Retrieval (WP-06 Kernfeature)
if inject_types:
logger.info(f"[{query_id}] Executing Strategic Retrieval for types: {inject_types}...")
strategy_req = QueryRequest(
query=request.message,
mode="hybrid",
@ -281,19 +295,16 @@ async def chat_endpoint(
explain=False
)
strategy_result = await retriever.search(strategy_req)
existing_ids = {h.node_id for h in hits}
for strat_hit in strategy_result.results:
if strat_hit.node_id not in existing_ids:
hits.append(strat_hit)
# 4. Context Building
if not hits:
context_str = "Keine relevanten Notizen gefunden."
else:
context_str = _build_enriched_context(hits)
# 5. Generation Setup
template = llm.prompts.get(prompt_key, "{context_str}\n\n{query}")
if prepend_instr:
@ -302,35 +313,29 @@ async def chat_endpoint(
final_prompt = template.replace("{context_str}", context_str).replace("{query}", request.message)
sources_hits = hits
# --- COMMON GENERATION ---
# --- GENERATION ---
system_prompt = llm.prompts.get("system_prompt", "")
logger.info(f"[{query_id}] Sending to LLM (Intent: {intent}, Template: {prompt_key})...")
# System-Prompt separat übergeben
answer_text = await llm.generate_raw_response(prompt=final_prompt, system=system_prompt)
# Chat nutzt IMMER realtime priority
answer_text = await llm.generate_raw_response(
prompt=final_prompt,
system=system_prompt,
priority="realtime"
)
duration_ms = int((time.time() - start_time) * 1000)
# 6. Logging (Fire & Forget)
# Logging
try:
log_search(
query_id=query_id,
query_text=request.message,
results=sources_hits,
mode="interview" if intent == "INTERVIEW" else "chat_rag",
metadata={
"intent": intent,
"intent_source": intent_source,
"generated_answer": answer_text,
"model": llm.settings.LLM_MODEL
}
metadata={"intent": intent, "source": intent_source}
)
except Exception as e:
logger.error(f"Logging failed: {e}")
except: pass
# 7. Response
return ChatResponse(
query_id=query_id,
answer=answer_text,

View File

@ -1,80 +1,142 @@
"""
app/services/llm_service.py LLM Client (Ollama)
Version: 0.2.1 (Fix: System Prompt Handling for Phi-3)
app/services/llm_service.py LLM Client
Version: 2.8.0 (Configurable Concurrency Limit)
"""
import httpx
import yaml
import logging
import os
import asyncio
from pathlib import Path
from app.config import get_settings
from typing import Optional, Dict, Any, Literal
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:
# GLOBALER SEMAPHOR (Lazy Initialization)
# Wir initialisieren ihn erst, wenn wir die Settings kennen.
_background_semaphore = None
def __init__(self):
self.settings = get_settings()
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(
base_url=self.settings.OLLAMA_URL,
timeout=self.settings.LLM_TIMEOUT
timeout=self.timeout
)
def _load_prompts(self) -> dict:
path = Path(self.settings.PROMPTS_PATH)
if not path.exists():
return {}
if not path.exists(): return {}
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:
logger.error(f"Failed to load prompts: {e}")
return {}
async def generate_raw_response(self, prompt: str, system: str = None) -> str:
async def generate_raw_response(
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.
Unterstützt nun explizite System-Prompts für sauberes Templating.
Führt einen LLM Call aus.
priority="realtime": Chat (Sofort, keine Bremse).
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,
"prompt": prompt,
"stream": False,
"options": {
# Temperature etwas höher für Empathie, niedriger für Code?
# Wir lassen es auf Standard, oder steuern es später via Config.
"temperature": 0.7,
"num_ctx": 2048
"temperature": 0.1 if force_json else 0.7,
"num_ctx": 8192
}
}
# WICHTIG: System-Prompt separat übergeben, damit Ollama formatiert
if force_json:
payload["format"] = "json"
if system:
payload["system"] = system
try:
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."
data = response.json()
return data.get("response", "").strip()
except Exception as e:
logger.error(f"LLM Raw Gen Error: {e}")
return "Interner LLM Fehler."
attempt = 0
while True:
try:
response = await self.client.post("/api/generate", json=payload)
if response.status_code == 200:
data = response.json()
return data.get("response", "").strip()
else:
response.raise_for_status()
except Exception as e:
attempt += 1
if attempt > max_retries:
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:
"""Legacy Support"""
"""
Chat-Wrapper: Immer Realtime.
"""
system_prompt = self.prompts.get("system_prompt", "")
rag_template = self.prompts.get("rag_template", "{context_str}\n\n{query}")
final_prompt = rag_template.format(context_str=context_str, query=query)
# Leite an die neue Methode weiter
return await self.generate_raw_response(final_prompt, system=system_prompt)
return await self.generate_raw_response(
final_prompt,
system=system_prompt,
max_retries=0,
force_json=False,
priority="realtime"
)
async def close(self):
await self.client.aclose()
if self.client:
await self.client.aclose()

View File

@ -0,0 +1,138 @@
"""
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,32 +1,31 @@
# config/decision_engine.yaml
# Steuerung der Decision Engine (WP-06 + WP-07)
# Hybrid-Modus: Keywords (Fast) + LLM Router (Smart Fallback)
version: 1.3
# Steuerung der Decision Engine (Intent Recognition)
# Version: 2.4.0 (Clean Architecture: Generic Intents only)
version: 1.4
settings:
llm_fallback_enabled: true
# Few-Shot Prompting für bessere SLM-Performance
# Erweitert um INTERVIEW Beispiele
# Few-Shot Prompting für den LLM-Router (Slow Path)
llm_router_prompt: |
Du bist ein Klassifikator. Analysiere die Nachricht und wähle die passende Strategie.
Antworte NUR mit dem Namen der Strategie.
STRATEGIEN:
- INTERVIEW: User will Wissen strukturieren, Notizen anlegen, Projekte starten ("Neu", "Festhalten").
- INTERVIEW: User will Wissen erfassen, Notizen anlegen oder Dinge festhalten.
- DECISION: Rat, Strategie, Vor/Nachteile, "Soll ich".
- EMPATHY: Gefühle, Frust, Freude, Probleme, "Alles ist sinnlos", "Ich bin traurig".
- CODING: Code, Syntax, Programmierung, Python.
- EMPATHY: Gefühle, Frust, Freude, Probleme.
- CODING: Code, Syntax, Programmierung.
- FACT: Wissen, Fakten, Definitionen.
BEISPIELE:
User: "Wie funktioniert Qdrant?" -> FACT
User: "Soll ich Qdrant nutzen?" -> DECISION
User: "Ich möchte ein neues Projekt anlegen" -> INTERVIEW
User: "Lass uns eine Entscheidung festhalten" -> INTERVIEW
User: "Ich möchte etwas notieren" -> INTERVIEW
User: "Lass uns das festhalten" -> INTERVIEW
User: "Schreibe ein Python Script" -> CODING
User: "Alles ist grau und sinnlos" -> EMPATHY
User: "Mir geht es heute gut" -> EMPATHY
NACHRICHT: "{query}"
@ -51,11 +50,9 @@ strategies:
- "empfehlung"
- "strategie"
- "entscheidung"
- "wert"
- "prinzip"
- "vor- und nachteile"
- "abwägung"
inject_types: ["value", "principle", "goal"]
- "vergleich"
inject_types: ["value", "principle", "goal", "risk"]
prompt_template: "decision_template"
prepend_instruction: |
!!! ENTSCHEIDUNGS-MODUS !!!
@ -72,6 +69,7 @@ strategies:
- "angst"
- "nervt"
- "überfordert"
- "müde"
inject_types: ["experience", "belief", "profile"]
prompt_template: "empathy_template"
prepend_instruction: null
@ -88,56 +86,37 @@ strategies:
- "syntax"
- "json"
- "yaml"
- "bash"
inject_types: ["snippet", "reference", "source"]
prompt_template: "technical_template"
prepend_instruction: null
# 5. Interview / Datenerfassung (WP-07)
# 5. Interview / Datenerfassung
# HINWEIS: Spezifische Typen (Projekt, Ziel etc.) werden automatisch
# über die types.yaml erkannt. Hier stehen nur generische Trigger.
INTERVIEW:
description: "Der User möchte strukturiertes Wissen erfassen (Projekt, Notiz, Idee)."
description: "Der User möchte Wissen erfassen."
trigger_keywords:
- "neue notiz"
- "neues projekt"
- "neue entscheidung"
- "neues ziel"
- "etwas notieren"
- "festhalten"
- "entwurf erstellen"
- "interview"
- "erstellen"
- "dokumentieren"
- "anlegen"
- "interview"
- "erfassen"
- "idee speichern"
inject_types: [] # Keine RAG-Suche, reiner Kontext-Dialog
- "draft"
inject_types: []
prompt_template: "interview_template"
prepend_instruction: null
# LATE BINDING SCHEMAS:
# Definition der Pflichtfelder pro Typ (korrespondiert mit types.yaml)
# Wenn ein Typ hier fehlt, wird 'default' genutzt.
# Schemas: Hier nur der Fallback.
# Spezifische Schemas (Project, Experience) kommen jetzt aus types.yaml!
schemas:
default:
fields: ["Titel", "Thema/Inhalt", "Tags"]
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."
fields:
- "Titel"
- "Thema/Inhalt"
- "Tags"
hint: "Halte es einfach und übersichtlich."

View File

@ -97,44 +97,66 @@ technical_template: |
# ---------------------------------------------------------
# 5. INTERVIEW: Der "One-Shot Extractor" (Performance Mode)
# ---------------------------------------------------------
interview_template: |
TASK:
Erstelle einen Markdown-Entwurf für eine Notiz vom Typ '{target_type}'.
Du bist ein professioneller Ghostwriter. Verwandle den "USER INPUT" in eine strukturierte Notiz vom Typ '{target_type}'.
SCHEMA (Inhaltliche Pflichtfelder für den Body):
STRUKTUR (Nutze EXAKT diese Überschriften):
{schema_fields}
USER INPUT:
"{query}"
ANWEISUNG:
1. Extrahiere Informationen aus dem Input.
2. Generiere validen Markdown.
ANWEISUNG ZUM INHALT:
1. Analysiere den Input genau.
2. Schreibe die Inhalte unter die passenden Überschriften aus der STRUKTUR-Liste oben.
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 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
OUTPUT FORMAT (YAML + MARKDOWN):
---
type: {target_type}
status: draft
title: ...
tags: [...]
title: (Erstelle einen treffenden, kurzen Titel für den Inhalt)
tags: [Tag1, Tag2]
---
# Titel der Notiz
## Erstes Schema Feld
Der Inhalt hier...
# (Wiederhole den Titel 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,86 +1,201 @@
version: 1.1 # Update auf v1.1 für Mindnet v2.4
version: 2.4.0 # Optimized for Async Intelligence & Hybrid Router
# ==============================================================================
# 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:
retriever_weight: 1.0
chunk_profile: default
chunking_profile: sliding_standard
edge_defaults: []
# ==============================================================================
# 3. TYPE DEFINITIONS
# ==============================================================================
types:
# --- WISSENSBAUSTEINE ---
concept:
chunk_profile: medium
retriever_weight: 0.60
edge_defaults: ["references", "related_to"]
source:
chunk_profile: short
retriever_weight: 0.50
edge_defaults: [] # Quellen sind passiv
glossary:
chunk_profile: short
retriever_weight: 0.40
edge_defaults: ["related_to"]
# --- 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"]
# --- KERNTYPEN (Hoch priorisiert & Smart) ---
experience:
chunk_profile: medium
chunking_profile: sliding_smart_edges
retriever_weight: 0.90
edge_defaults: ["derived_from", "references"] # Erfahrungen haben einen Ursprung
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 ---
# --- STRATEGIE & ENTSCHEIDUNG ---
goal:
chunk_profile: medium
chunking_profile: sliding_smart_edges
retriever_weight: 0.95
edge_defaults: ["depends_on", "related_to"]
schema: ["Zielzustand", "Zeitrahmen & KPIs", "Motivation"]
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
risk:
chunking_profile: sliding_short
retriever_weight: 0.85
edge_defaults: ["related_to", "blocks"] # Risiken blockieren ggf. Projekte
edge_defaults: ["related_to", "blocks"]
detection_keywords: ["risiko", "gefahr", "bedrohung", "problem", "angst"]
schema: ["Beschreibung des Risikos", "Mögliche Auswirkungen", "Gegenmaßnahmen"]
milestone:
chunk_profile: short
retriever_weight: 0.70
edge_defaults: ["related_to", "part_of"]
# --- BASIS & WISSEN ---
# --- OPERATIV ---
project:
chunk_profile: long
retriever_weight: 0.97 # Projekte sind der Kontext für alles
edge_defaults: ["references", "depends_on"]
concept:
chunking_profile: sliding_smart_edges
retriever_weight: 0.60
edge_defaults: ["references", "related_to"]
schema:
- "Definition"
- "Kontext & Hintergrund"
- "Verwandte Konzepte"
task:
chunk_profile: short
chunking_profile: sliding_short
retriever_weight: 0.80
edge_defaults: ["depends_on", "part_of"]
schema: ["Aufgabe", "Kontext", "Definition of Done"]
journal:
chunk_profile: medium
chunking_profile: sliding_standard
retriever_weight: 0.80
edge_defaults: ["references", "related_to"]
edge_defaults: ["references", "related_to"]
schema: ["Log-Eintrag", "Gedanken & Erkenntnisse"]
source:
chunking_profile: sliding_standard
retriever_weight: 0.50
edge_defaults: []
schema:
- "Metadaten (Autor, URL, Datum)"
- "Kernaussage / Zusammenfassung"
- "Zitate & Notizen"
glossary:
chunking_profile: sliding_short
retriever_weight: 0.40
edge_defaults: ["related_to"]
schema: ["Begriff", "Definition"]

View File

@ -1,7 +1,7 @@
# mindnet v2.4 Knowledge Design Manual
**Datei:** `docs/mindnet_knowledge_design_manual_v2.4.md`
**Stand:** 2025-12-11
**Status:** **FINAL** (Integrierter Stand WP01WP11)
**Datei:** `docs/mindnet_knowledge_design_manual_v2.6.md`
**Stand:** 2025-12-12
**Status:** **FINAL** (Integrierter Stand WP01WP15)
**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
Mindnet ist mehr als eine Dokumentablage. Es ist ein vernetztes System, das deine Persönlichkeit, Entscheidungen und Erfahrungen abbildet.
Seit Version 2.4 verfügt Mindnet über:
Seit Version 2.6 verfügt Mindnet über:
* **Hybrid Router:** Das System erkennt, ob du Fakten, Entscheidungen oder Empathie brauchst.
* **Context Intelligence:** Das System lädt je nach Situation unterschiedliche Notiz-Typen (z.B. Werte bei Entscheidungen).
* **Smart Edge Allocation:** Das System prüft deine Links intelligent und bindet sie nur an die Textabschnitte (Chunks), wo sie wirklich relevant sind.
* **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).
@ -69,7 +69,7 @@ Diese Felder sind technisch nicht zwingend, aber für bestimmte Typen sinnvoll:
**Die ID (Identifikator):**
* Muss global eindeutig und **stabil** sein.
* Darf sich nicht ändern, wenn die Datei umbenannt oder verschoben wird.
* **Empfehlung:** `YYYYMMDD-slug-hash` (z. B. `20231027-vektor-db-a1b2`). Dies garantiert Eindeutigkeit und Chronologie.
* **Empfehlung:** `YYYYMMDD-slug` (z. B. `20231027-vektor-db`). Dies garantiert Eindeutigkeit und Chronologie.
**Dateinamen & Pfade:**
* 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
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.
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**.
### 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` |
| **`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` |
| **`risk`** | **NEU:** Ein identifiziertes Risiko oder eine Gefahr. | **DECISION** | Auswirkung, Wahrscheinlichkeit | Quelle für `blocks` |
| **`belief`** | **NEU:** Glaubenssatz / Überzeugung. | **EMPATHY** | Ursprung, Mantra | - |
| **`risk`** | Ein identifiziertes Risiko oder eine Gefahr. | **DECISION** | Auswirkung, Wahrscheinlichkeit | Quelle für `blocks` |
| **`belief`** | Glaubenssatz / Überzeugung. | **EMPATHY** | Ursprung, Mantra | - |
| **`person`** | Eine reale Person (Netzwerk, Autor). | **FACT** | Rolle, Kontext | - |
| **`journal`** | Zeitbezogener Log-Eintrag, Daily Note. | **FACT** | Datum, Tags | - |
| **`source`** | Externe Quelle (Buch, PDF, Artikel). | **FACT** | Autor, URL | - |
@ -107,13 +107,11 @@ Der `type` steuert im Hintergrund drei technische Mechanismen:
1. **`retriever_weight` (Wichtigkeit):**
* 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):**
* `journal` (short): Wird fein zerlegt.
* `project` (long): Längere Kontext-Fenster.
3. **`edge_defaults` (Automatische Vernetzung):**
* Mindnet ergänzt automatisch Kanten.
* Beispiel: Ein Link in einem `project` wird automatisch als `depends_on` (Abhängigkeit) interpretiert.
* `experience` (sliding_smart_edges): Wird intelligent analysiert.
3. **`enable_smart_edge_allocation` (WP15):**
* 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.
---
@ -291,4 +289,4 @@ Wir vermeiden es, Logik in den Markdown-Dateien hart zu kodieren.
### 7.2 Was bedeutet das für dich?
* Du kannst dich auf den Inhalt konzentrieren.
* 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.
* 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.

View File

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

View File

@ -1,8 +1,8 @@
# Mindnet v2.4 Admin Guide
**Datei:** `docs/mindnet_admin_guide_v2.4.md`
**Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. Async Architecture & Nomic Model)
**Quellen:** `Handbuch.md`, `mindnet_developer_guide_v2.4.md`.
**Datei:** `docs/mindnet_admin_guide_v2.6.md`
**Stand:** 2025-12-12
**Status:** **FINAL** (Inkl. Traffic Control & Smart Edge Config)
**Quellen:** `Handbuch.md`, `mindnet_developer_guide_v2.6.md`.
> 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)
Mindnet benötigt einen lokalen LLM-Server für Chat UND Embeddings.
**WICHTIG (Update v2.3.10):** Es muss zwingend `nomic-embed-text` installiert sein, sonst startet der Import nicht.
**WICHTIG (Update v2.4):** Es muss zwingend `nomic-embed-text` installiert sein, sonst startet der Import nicht.
# 1. Installieren (Linux Script)
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"}'
### 2.5 Konfiguration (ENV)
Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM` und `MINDNET_EMBEDDING_MODEL`.
Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM` und das neue `MINDNET_LLM_BACKGROUND_LIMIT`.
# Server Config
UVICORN_HOST=0.0.0.0
@ -87,11 +87,15 @@ Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM`
# AI Modelle (Ollama)
MINDNET_OLLAMA_URL="http://127.0.0.1:11434"
MINDNET_LLM_MODEL="phi3:mini"
MINDNET_EMBEDDING_MODEL="nomic-embed-text" # NEU
MINDNET_EMBEDDING_MODEL="nomic-embed-text"
# Timeouts (Erhöht für Async/Nomic)
# Timeouts (Erhöht für Async/Nomic/Smart Edges)
MINDNET_LLM_TIMEOUT=300.0
MINDNET_API_TIMEOUT=60.0
MINDNET_API_TIMEOUT=300.0 # Wichtig: Frontend muss warten können
# Traffic Control (Neu in v2.6)
# Begrenzt parallele LLM-Calls im Import, um Chat stabil zu halten.
MINDNET_LLM_BACKGROUND_LIMIT=2
# Configs
MINDNET_PROMPTS_PATH="./config/prompts.yaml"
@ -155,12 +159,15 @@ Mindnet benötigt zwei Services pro Umgebung: API (Uvicorn) und UI (Streamlit).
## 3. Betrieb im Alltag
### 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** und eine Semaphore, um Ollama nicht zu überlasten.
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**.
**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
**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
Prüfe regelmäßig, ob alle Komponenten laufen.
@ -176,7 +183,7 @@ Prüfe regelmäßig, ob alle Komponenten laufen.
---
## 4. Troubleshooting (Update v2.4)
## 4. Troubleshooting (Update v2.6)
### "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.
@ -184,15 +191,17 @@ Prüfe regelmäßig, ob alle Komponenten laufen.
1. `python -m scripts.reset_qdrant --mode wipe --prefix mindnet --yes` (Löscht DB).
2. `python -m scripts.import_markdown ...` (Baut neu auf).
### "500 Internal Server Error" beim Speichern
* **Ursache:** Oft Timeout bei Ollama, wenn `nomic-embed-text` noch nicht im RAM geladen ist ("Cold Start").
### "500 Internal Server Error" beim Speichern/Chatten
* **Ursache:** Oft Timeout bei Ollama, wenn `nomic-embed-text` noch nicht im RAM geladen ist ("Cold Start") oder der Import die GPU blockiert.
* **Lösung:**
1. Sicherstellen, dass Modell existiert: `ollama list`.
2. API neustarten (re-initialisiert Async Clients).
1. `MINDNET_LLM_TIMEOUT` in `.env` auf `300.0` setzen.
2. `MINDNET_LLM_BACKGROUND_LIMIT` auf `1` reduzieren (falls Hardware schwach).
### "NameError: name 'os' is not defined"
* **Ursache:** Fehlender Import in Skripten nach Updates.
* **Lösung:** `git pull` (Fix wurde in v2.3.10 deployed).
### Import ist sehr langsam
* **Ursache:** Smart Edges sind aktiv (`types.yaml`) und analysieren jeden Chunk.
* **Lösung:**
* Akzeptieren (für bessere Qualität).
* Oder für Massen-Initial-Import in `types.yaml` temporär `enable_smart_edge_allocation: false` setzen.
---
@ -215,7 +224,7 @@ Wenn die Datenbank korrupt ist oder Modelle gewechselt werden:
# 1. DB komplett leeren (Wipe)
python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes
# 2. Alles neu importieren
# 2. Alles neu importieren (Force re-calculates Hashes)
python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
---

View File

@ -1,7 +1,7 @@
# Mindnet v2.4 Appendices & Referenzen
**Datei:** `docs/mindnet_appendices_v2.4.md`
**Stand:** 2025-12-11
**Status:** **FINAL** (Integrierter Stand WP01WP11)
**Datei:** `docs/mindnet_appendices_v2.6.md`
**Stand:** 2025-12-12
**Status:** **FINAL** (Integrierter Stand WP01WP15)
**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.
@ -10,22 +10,27 @@
## Anhang A: Typ-Registry Referenz (Default-Werte)
Diese Tabelle zeigt die Standard-Konfiguration der `types.yaml` (Stand v2.4).
Diese Tabelle zeigt die Standard-Konfiguration der `types.yaml` (Stand v2.6 / Config v1.6).
| Typ (`type`) | Chunk Profile | Retriever Weight | Edge Defaults (Auto-Kanten) | Beschreibung |
| Typ (`type`) | Chunk Profile | Retriever Weight | Smart Edges? | Beschreibung |
| :--- | :--- | :--- | :--- | :--- |
| **concept** | `medium` | 0.60 | `references`, `related_to` | Abstrakte Begriffe, Theorien. |
| **project** | `long` | 0.97 | `references`, `depends_on` | Aktive Vorhaben. Hohe Priorität. |
| **decision** | `long` | 1.00 | `caused_by`, `references` | Entscheidungen (ADRs). Höchste Prio. |
| **experience** | `medium` | 0.90 | `derived_from`, `inspired_by` | Persönliche Learnings. |
| **journal** | `short` | 0.80 | `related_to` | Zeitgebundene Logs. Fein granular. |
| **person** | `short` | 0.50 | `related_to` | Personen-Profile. |
| **source** | `long` | 0.50 | *(keine)* | Externe Quellen (Bücher, PDFs). |
| **event** | `short` | 0.60 | `related_to` | Meetings, Konferenzen. |
| **value** | `medium` | 1.00 | `related_to` | Persönliche Werte/Prinzipien. |
| **goal** | `medium` | 0.95 | `depends_on` | Strategische Ziele. |
| **belief** | `medium` | 0.90 | `related_to` | Glaubenssätze. |
| **default** | `medium` | 1.00 | `references` | Fallback, wenn Typ unbekannt. |
| **concept** | `sliding_smart_edges` | 0.60 | **Ja** | Abstrakte Begriffe, Theorien. |
| **project** | `sliding_smart_edges` | 0.97 | **Ja** | Aktive Vorhaben. Hohe Priorität. |
| **decision** | `structured_smart_edges` | 1.00 | **Ja** | Entscheidungen (ADRs). Höchste Prio. |
| **experience** | `sliding_smart_edges` | 0.90 | **Ja** | Persönliche Learnings. Intensiv analysiert. |
| **journal** | `sliding_standard` | 0.80 | Nein | Zeitgebundene Logs. Performance-optimiert. |
| **person** | `sliding_standard` | 0.50 | Nein | Personen-Profile. |
| **source** | `sliding_standard` | 0.50 | Nein | Externe Quellen (Bücher, PDFs). |
| **event** | `sliding_standard` | 0.60 | Nein | Meetings, Konferenzen. |
| **value** | `structured_smart_edges` | 1.00 | **Ja** | Persönliche Werte/Prinzipien. |
| **principle** | `structured_smart_edges` | 1.00 | **Ja** | Handlungsleitlinien. |
| **profile** | `structured_smart_edges` | 0.80 | **Ja** | Eigene Identitäts-Beschreibungen. |
| **goal** | `sliding_standard` | 0.95 | Nein | Strategische Ziele. |
| **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. |
---
@ -43,6 +48,7 @@ Referenz aller implementierten Kantenarten (`kind`).
| `similar_to` | Inline | Ja | Inhaltliche Ähnlichkeit. "Ist wie X". |
| `caused_by` | Inline | Nein | Kausalität. "X ist der Grund für 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". |
| `based_on` | Matrix | Nein | Fundament. "Erfahrung basiert auf Wert Y". |
| `uses` | Matrix | Nein | Nutzung. "Projekt nutzt Konzept Z". |
@ -60,7 +66,7 @@ Diese sind die Felder, die effektiv in Qdrant gespeichert werden.
"title": "string (text)", // Titel aus Frontmatter
"type": "string (keyword)", // Typ (z.B. 'project')
"retriever_weight": "float", // Numerische Wichtigkeit (0.0-1.0)
"chunk_profile": "string", // Genutztes Profil (z.B. 'long')
"chunk_profile": "string", // Genutztes Profil (z.B. 'sliding_smart_edges')
"edge_defaults": ["string"], // Liste der aktiven Default-Kanten
"tags": ["string"], // Liste von Tags
"created": "string (iso-date)", // Erstellungsdatum
@ -78,6 +84,7 @@ Diese sind die Felder, die effektiv in Qdrant gespeichert werden.
"window": "string (text)", // Text + Overlap (für Embedding)
"ord": "integer", // Laufende Nummer (1..N)
"retriever_weight": "float", // Kopie aus Note (für Query-Speed)
"chunk_profile": "string", // Vererbt von Note
"neighbors_prev": ["string"], // ID des Vorgängers
"neighbors_next": ["string"] // ID des Nachfolgers
}
@ -92,7 +99,8 @@ Diese sind die Felder, die effektiv in Qdrant gespeichert werden.
"scope": "string (keyword)", // Immer 'chunk'
"rule_id": "string (keyword)", // Traceability: 'inline:rel', 'explicit:wikilink'
"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)
}
---
@ -115,7 +123,8 @@ Diese Variablen steuern das Verhalten der Skripte und Container.
| `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_LLM_TIMEOUT` | `300.0` | Timeout für Ollama (Erhöht für CPU-Inference). |
| `MINDNET_API_TIMEOUT` | `60.0` | **NEU:** Frontend Timeout (Streamlit). |
| `MINDNET_API_TIMEOUT` | `300.0` | **NEU:** Frontend Timeout (Erhöht für Smart Edges). |
| `MINDNET_LLM_BACKGROUND_LIMIT`| `2` | **NEU:** Max. parallele Import-Tasks (Traffic Control). |
| `MINDNET_VAULT_ROOT` | `./vault` | **NEU:** Pfad für Write-Back Operationen. |
| `MINDNET_HASH_COMPARE` | `Body` | Vergleichsmodus für Import (`Body`, `Frontmatter`, `Full`). |
| `MINDNET_HASH_SOURCE` | `parsed` | Quelle für Hash (`parsed`, `raw`, `file`). |
@ -129,16 +138,19 @@ Diese Variablen steuern das Verhalten der Skripte und Container.
* **Decision Engine:** Komponente, die den Intent prüft und Strategien wählt (WP06).
* **Draft Editor:** Web-Komponente zur Bearbeitung generierter Notizen (WP10a).
* **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.
* **Matrix Logic:** Regelwerk, das Kanten-Typen basierend auf Quell- und Ziel-Typ bestimmt.
* **Nomic:** Das neue, hochpräzise Embedding-Modell (768 Dim).
* **One-Shot Extractor:** LLM-Strategie zur sofortigen Generierung von Drafts ohne Rückfragen (WP07).
* **RAG (Retrieval Augmented Generation):** Kombination aus Suche und Text-Generierung.
* **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.4.0)
## Anhang F: Workpackage Status (v2.6.0)
Aktueller Implementierungsstand der Module.
@ -151,9 +163,12 @@ Aktueller Implementierungsstand der Module.
| **WP04b**| Explanation Layer | 🟢 Live | API liefert Reasons & Breakdown. |
| **WP04c**| Feedback Loop | 🟢 Live | Logging (JSONL) & Traceability aktiv. |
| **WP05** | Persönlichkeit / Chat | 🟢 Live | RAG-Integration mit Context Enrichment. |
| **WP06** | Decision Engine | 🟢 Live | Intent-Router & Strategic Retrieval. |
| **WP06** | Decision Engine | 🟢 Live | Hybrid Router v5, Strategic Retrieval. |
| **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** |
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
| **WP11** | Backend Intelligence | 🟢 Live | **Async Core, Nomic, Matrix.** |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI & Healing Parser.** |
| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, 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
**Datei:** `docs/DEV_WORKFLOW.md`
**Stand:** 2025-12-11 (Aktualisiert: Inkl. Async Intelligence & Nomic)
**Stand:** 2025-12-12 (Aktualisiert: v2.6 mit Traffic Control)
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:**
* Klicke wieder unten links auf `main`.
* Wähle `+ Create new branch...`.
* Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp11-async-fix`).
* Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp15-traffic-control`).
* Drücke **Enter**.
3. **Sicherheits-Check:**
* Steht unten links jetzt dein Feature-Branch? **Nur dann darfst du Code ändern!**
4. **Coden:**
* Nimm deine Änderungen vor (z.B. neue Schemas in `decision_engine.yaml` oder Async-Logik in `ingestion.py`).
* Nimm deine Änderungen vor (z.B. neue Schemas in `types.yaml` oder Async-Logik in `ingestion.py`).
5. **Sichern & Hochladen:**
* **Source Control** Icon (Gabel-Symbol) -> Nachricht eingeben -> **Commit**.
@ -64,15 +64,27 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft.
```bash
git fetch
# Tipp: 'git branch -r' zeigt alle verfügbaren Branches an
git checkout feature/wp11-async-fix
git checkout feature/wp15-traffic-control
git pull
```
4. **Umgebung vorbereiten (WICHTIG für v2.4):**
4. **Umgebung vorbereiten (WICHTIG für v2.6):**
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
source .venv/bin/activate
pip install -r requirements.txt # HTTPX usw.
# Sicherstellen, dass das neue Embedding-Modell da ist:
pip install -r requirements.txt
ollama pull nomic-embed-text
```
@ -102,18 +114,15 @@ 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
```
6. **Validieren (Smoke Tests):**
6. **Validieren (Smoke Tests v2.6):**
* **Browser:** Öffne `http://<IP>:8502` um die UI zu testen (Intent Badge prüfen!).
* **CLI:** Führe Testskripte in einem **zweiten Terminal** aus:
**Test A: Intelligence / Aliases (Neu in WP11)**
```bash
python debug_analysis.py
# Erwartung: "✅ ALIAS GEFUNDEN"
```
* **Test A: Last-Test (Traffic Control):**
1. Starte einen Import im Terminal: `python3 -m scripts.import_markdown ...`
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 B: API Check**
```bash
curl -X POST "http://localhost:8002/ingest/analyze" -d '{"text": "mindnet", "type": "journal"}'
```
@ -147,6 +156,9 @@ Jetzt bringen wir die Änderung in das Live-System (Port 8001 / 8501).
pip install -r requirements.txt
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):
# python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes
# python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
@ -168,7 +180,7 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch.
cd ~/mindnet_dev
git checkout main
git pull
git branch -d feature/wp11-async-fix
git branch -d feature/wp15-traffic-control
```
3. **VS Code:**
* Auf `main` wechseln.
@ -190,15 +202,15 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch.
## 4. Troubleshooting
**"Vector dimension error: expected 768, got 384"**
* **Ursache:** Du hast `nomic-embed-text` (768) aktiviert, aber die DB ist noch alt (384).
* **Lösung:** `scripts.reset_qdrant` ausführen und neu importieren.
**"Read timed out" im Frontend**
* **Ursache:** Backend braucht für Smart Edges länger als 60s.
* **Lösung:** `MINDNET_API_TIMEOUT=300.0` in `.env` setzen und Services neustarten.
**"Read timed out (300s)" / 500 Error beim Interview**
* **Ursache:** Das LLM (Ollama) braucht für den One-Shot Draft länger als das Timeout erlaubt.
**Import ist extrem langsam**
* **Ursache:** Smart Edges analysieren jeden Chunk mit LLM.
* **Lösung:**
1. Erhöhe in `.env` den Wert: `MINDNET_LLM_TIMEOUT=300.0`.
2. Starte die Server neu.
* Akzeptieren (Qualität vor Speed).
* Oder temporär in `config/types.yaml`: `enable_smart_edge_allocation: false`.
**"UnicodeDecodeError in .env"**
* **Ursache:** Umlaute oder Sonderzeichen in der `.env` Datei.

View File

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

View File

@ -1,9 +1,9 @@
# Mindnet v2.4 Fachliche Architektur
**Datei:** `docs/mindnet_functional_architecture_v2.4.md`
**Stand:** 2025-12-11
**Status:** **FINAL** (Integrierter Stand WP01WP11: Async Intelligence)
**Datei:** `docs/mindnet_functional_architecture_v2.6.md`
**Stand:** 2025-12-12
**Status:** **FINAL** (Integrierter Stand WP01WP15: Smart Edges & Traffic Control)
> 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).
> 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).
---
<details>
@ -19,7 +19,8 @@
- [2.1 Struktur-Kanten (Das Skelett)](#21-struktur-kanten-das-skelett)
- [2.2 Inhalts-Kanten (explizit)](#22-inhalts-kanten-explizit)
- [2.3 Typ-basierte Default-Kanten (Regelbasiert)](#23-typ-basierte-default-kanten-regelbasiert)
- [2.4 Matrix-Logik (Kontextsensitive Kanten) Neu in v2.4](#24-matrix-logik-kontextsensitive-kanten--neu-in-v24)
- [2.4 Smart Edge Allocation (LLM-gefiltert) Neu in v2.6](#24-smart-edge-allocation-llm-gefiltert--neu-in-v26)
- [2.5 Matrix-Logik (Kontextsensitive Kanten)](#25-matrix-logik-kontextsensitive-kanten)
- [3) Edge-Payload Felder \& Semantik](#3-edge-payload--felder--semantik)
- [4) Typ-Registry (`config/types.yaml`)](#4-typ-registry-configtypesyaml)
- [4.1 Zweck](#41-zweck)
@ -27,13 +28,15 @@
- [5) Der Retriever (Funktionaler Layer)](#5-der-retriever-funktionaler-layer)
- [5.1 Scoring-Modell](#51-scoring-modell)
- [5.2 Erklärbarkeit (Explainability) WP04b](#52-erklärbarkeit-explainability--wp04b)
- [6) Context Intelligence \& Intent Router (WP06WP11)](#6-context-intelligence--intent-router-wp06wp11)
- [5.3 Graph-Expansion](#53-graph-expansion)
- [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.2 Der Intent-Router (Keyword \& Semantik)](#62-der-intent-router-keyword--semantik)
- [6.3 Strategic Retrieval (Injektion von Werten)](#63-strategic-retrieval-injektion-von-werten)
- [6.4 Reasoning (Das Gewissen)](#64-reasoning-das-gewissen)
- [6.5 Der Interview-Modus (One-Shot Extraction)](#65-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.2 Der Hybrid Router v5 (Action vs. Question)](#62-der-hybrid-router-v5-action-vs-question)
- [6.3 Traffic Control (Realtime vs. Background)](#63-traffic-control-realtime-vs-background)
- [6.4 Strategic Retrieval (Injektion von Werten)](#64-strategic-retrieval-injektion-von-werten)
- [6.5 Reasoning (Das Gewissen)](#65-reasoning-das-gewissen)
- [6.6 Der Interview-Modus (One-Shot Extraction)](#66-der-interview-modus-one-shot-extraction)
- [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.1 Antizipation durch Erfahrung](#71-antizipation-durch-erfahrung)
- [7.2 Empathie \& "Ich"-Modus](#72-empathie--ich-modus)
@ -49,7 +52,7 @@
- [13) Lösch-/Update-Garantien (Idempotenz)](#13-lösch-update-garantien-idempotenz)
- [14) Beispiel Von Markdown zu Kanten](#14-beispiel--von-markdown-zu-kanten)
- [15) Referenzen (Projektdateien \& Leitlinien)](#15-referenzen-projektdateien--leitlinien)
- [16) Workpackage Status (v2.4.0)](#16-workpackage-status-v240)
- [16) Workpackage Status (v2.6.0)](#16-workpackage-status-v260)
</details>
---
@ -63,7 +66,7 @@ Die drei zentralen Artefakt-Sammlungen lauten:
- `mindnet_chunks` semantische Teilstücke einer Note (Fenster/„Chunks“)
- `mindnet_edges` gerichtete Beziehungen zwischen Knoten (Chunks/Notes)
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*.
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*.
---
@ -80,9 +83,9 @@ Die Import-Pipeline (seit v2.3.10 asynchron) erzeugt diese Artefakte **determini
- Jeder Chunk gehört **genau einer** Note.
- 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).
- **Neu in v2.2:** Alle Kanten entstehen ausschließlich zwischen Chunks (Scope="chunk"), nie zwischen Notes direkt. Notes dienen nur noch als Metadatencontainer.
- **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).
> **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.
> **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.
---
@ -97,7 +100,7 @@ Edges kodieren Beziehungen. Sie sind **gerichtet** und werden in `mindnet_edges`
Diese Kanten entstehen immer, unabhängig von Inhalten.
### 2.2 Inhalts-Kanten (explizit)
Hier unterscheidet v2.2 präzise zwischen verschiedenen Quellen der Evidenz:
Hier unterscheidet v2.6 präzise zwischen verschiedenen Quellen der Evidenz:
1. **Explizite Inline-Relationen (Höchste Priorität):**
Im Fließtext notierte, semantisch qualifizierte Relationen.
@ -131,7 +134,19 @@ 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.
Diese Kanten tragen *provenance=rule* und eine **rule_id** der Form `edge_defaults:{note_type}:{relation}` sowie eine geringere Confidence (~0.7).
### 2.4 Matrix-Logik (Kontextsensitive Kanten) Neu in v2.4
### 2.4 Smart Edge Allocation (LLM-gefiltert) Neu in v2.6
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").
**Beispiel für `Source Type: experience`:**
@ -154,7 +169,7 @@ Jede Kante hat mindestens:
Erweiterte/abgeleitete Felder (WP03 Superset):
- `provenance` `"explicit"` (Wikilink/Inline/Callout) oder `"rule"` (Typ-Defaults)
- `provenance` `"explicit"` (Wikilink/Inline/Callout), `"rule"` (Typ-Defaults) oder neu `"smart"` (vom LLM validiert).
- `rule_id` maschinenlesbare Regelquelle (z.B. `inline:rel`, `edge_defaults:project:depends_on`)
- `confidence` 0.01.0; Heuristik zur Gewichtung im Scoring.
@ -168,20 +183,24 @@ Erweiterte/abgeleitete Felder (WP03 Superset):
- Steuert **Chunking-Profile** (`short|medium|long`) **pro Typ**
- Liefert **retriever_weight** (Note-/Chunk-Gewichtung im Ranking) **pro Typ**
- 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.
### 4.2 Beispiel (auslieferungsnah)
version: 1.0
version: 2.6.0
types:
concept:
chunk_profile: medium
edge_defaults: ["references", "related_to"]
retriever_weight: 0.60
project:
chunk_profile: long
edge_defaults: ["references", "depends_on"]
retriever_weight: 0.97
experience:
chunk_profile: sliding_smart_edges
enable_smart_edge_allocation: true # WP15: LLM prüft Kanten
detection_keywords: ["passiert", "erlebt", "situation"] # WP06: Router-Trigger
schema: # WP07: Interview-Struktur
- "Situation (Was ist passiert?)"
- "Meine Reaktion (Was habe ich getan?)"
**Auflösung im Importer**
- `effective_chunk_profile(note_type, registry)``"short|medium|long"|None`
@ -220,9 +239,12 @@ 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.
### 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 (WP06WP11)
## 6) Context Intelligence & Intent Router (WP06WP15)
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.
@ -230,40 +252,51 @@ 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.
* **Heute (WP06):** Das System erkennt, *was* der User will (Rat, Fakten oder Datenerfassung) und wechselt den Modus.
### 6.2 Der Intent-Router (Keyword & Semantik)
Der Router prüft vor jeder Antwort die Absicht über konfigurierbare Strategien (`config/decision_engine.yaml`):
### 6.2 Der Hybrid Router v5 (Action vs. Question)
Der Router wurde in v2.6 (WP15) weiterentwickelt, um Fehlalarme zu vermeiden.
1. **FACT:** Reine Wissensfrage ("Was ist Qdrant?"). → Standard RAG.
2. **DECISION:** Frage nach Rat oder Strategie ("Soll ich Qdrant nutzen?"). → Aktiviert die Decision Engine.
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.
1. **Frage-Erkennung:**
* Das System prüft zuerst: Enthält der Satz ein `?` oder typische W-Wörter (Wer, Wie, Was)?
* Wenn **JA** -> Gehe in den **RAG Modus** (Intent `FACT` oder `DECISION`). Interviews werden hier blockiert.
2. **Befehls-Erkennung (Fast Path):**
* 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.
### 6.3 Strategic Retrieval (Injektion von Werten)
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:
* **Values (`type: value`):** Moralische Werte (z.B. "Privacy First").
* **Principles (`type: principle`):** Handlungsanweisungen.
* **Goals (`type: goal`):** Strategische Ziele.
### 6.4 Reasoning (Das Gewissen)
### 6.5 Reasoning (Das Gewissen)
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").
### 6.5 Der Interview-Modus (One-Shot Extraction)
### 6.6 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**.
* **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.
* **Draft-Status:** Fehlende Pflichtfelder werden mit `[TODO]` markiert.
* **Healing Parser (v2.5):** Falls das LLM die YAML-Syntax beschädigt (z.B. fehlendes Ende), repariert das Frontend den Entwurf automatisch.
* **UI-Integration:** Das Frontend rendert statt einer Chat-Antwort einen **interaktiven Editor** (WP10), in dem der Entwurf finalisiert werden kann.
### 6.6 Active Intelligence (Link Suggestions) Neu in v2.4
### 6.7 Active Intelligence (Link Suggestions)
Im **Draft Editor** (Frontend) unterstützt das System den Autor aktiv.
* **Analyse:** Ein "Sliding Window" scannt den Text im Hintergrund (auch lange Entwürfe).
* **Erkennung:** Es findet Begriffe ("Mindnet") und semantische Konzepte ("Autofahrt in Italien").
* **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.
* **Logik:** Dabei kommt die in 2.4 beschriebene **Matrix-Logik** zum Einsatz, um den korrekten Kanten-Typ vorzuschlagen.
* **Logik:** Dabei kommt die in 2.5 beschriebene **Matrix-Logik** zum Einsatz, um den korrekten Kanten-Typ vorzuschlagen.
---
@ -301,8 +334,9 @@ Mindnet lernt nicht durch klassisches Training (Fine-Tuning), sondern durch **Ko
chunk_profile: short # Risiken sind oft kurze Statements
retriever_weight: 0.90 # Hohe Priorität im Ranking
edge_defaults: ["blocks"] # Automatische Kante zu verlinkten Projekten
detection_keywords: ["gefahr", "risiko"] # Für den Hybrid Router
```
*Effekt:* Der Retriever spült diese Notizen bei relevanten Anfragen nach oben.
*Effekt:* Der Router erkennt das Wort "Risiko" und bietet ein Interview an. Der Retriever spült diese Notizen bei relevanten Anfragen nach oben.
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.
@ -359,12 +393,12 @@ Die Interaktion erfolgt primär über das **Web-Interface (WP10)**.
## 10) Confidence & Provenance wozu?
Der Retriever kann Edges gewichten:
- **provenance**: *explicit* > *rule*
- **provenance**: *explicit* > *smart* (Neu) > *rule*
- **confidence**: numerische Feinsteuerung
- **retriever_weight (Note/Chunk)**: skaliert die Relevanz des gesamten Knotens
Eine typische Gewichtung (konfigurierbar in `retriever.yaml`) ist:
`explicit: 1.0`, `rule: 0.8`. Damit bevorzugt der Graph **kuratiertes Wissen** (explizit notierte Links) vor „erweiterten“ Default-Ableitungen.
`explicit: 1.0`, `smart: 0.9`, `rule: 0.8`. Damit bevorzugt der Graph **kuratiertes Wissen** (explizit notierte Links) vor „erweiterten“ Default-Ableitungen.
---
@ -413,11 +447,11 @@ Frontmatter-Eigenschaften (Properties) bleiben **minimiert**:
> [!edge] related_to: [[Vector DB Basics]]
**Ergebnis (fachlich)**
1. `depends_on(Chunk→Qdrant)` mit `rule_id=inline:rel`, `confidence≈0.95`.
2. `references(Chunk→Embeddings 101)` mit `rule_id=explicit:wikilink`, `confidence=1.0`.
3. `related_to(Chunk→Vector DB Basics)` via Callout; `rule_id=callout:edge`, `confidence≈0.90`.
4. **Typ-Defaults:** Falls die Note vom Typ `project` ist, entstehen zusätzlich `depends_on`-Kanten zu den Zielen aus (2) und (3).
**Ergebnis (fachlich - Smart Edges)**
Das LLM analysiert jeden Chunk.
1. Chunk 1 ("Wir nutzen..."): Enthält `depends_on(Chunk→Qdrant)`. Das LLM bestätigt: Relevant. -> Kante wird erstellt.
2. Chunk 2 ("Siehe auch..."): Enthält `references(Chunk→Embeddings)`. Das LLM bestätigt.
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.
---
@ -425,6 +459,7 @@ Frontmatter-Eigenschaften (Properties) bleiben **minimiert**:
- Import-Pipeline & Registry-Auflösung: `scripts/import_markdown.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`.
- Retriever-Scoring & Explanation: `app/core/retriever.py`.
- Persönlichkeit & Chat: `config/prompts.yaml` & `app/routers/chat.py`.
@ -435,7 +470,7 @@ Frontmatter-Eigenschaften (Properties) bleiben **minimiert**:
---
## 16) Workpackage Status (v2.4.0)
## 16) Workpackage Status (v2.6.0)
Aktueller Implementierungsstand der Module.
@ -448,9 +483,12 @@ Aktueller Implementierungsstand der Module.
| **WP04b**| Explanation Layer | 🟢 Live | API liefert Reasons & Breakdown. |
| **WP04c**| Feedback Loop | 🟢 Live | Logging (JSONL) & Traceability aktiv. |
| **WP05** | Persönlichkeit / Chat | 🟢 Live | RAG-Integration mit Context Enrichment. |
| **WP06** | Decision Engine | 🟢 Live | Intent-Router & Strategic Retrieval. |
| **WP06** | Decision Engine | 🟢 Live | Hybrid Router, Strategic Retrieval. |
| **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** |
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI & Healing Parser.** |
| **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.4 Technische Architektur
**Datei:** `docs/mindnet_technical_architecture_v2.4.md`
**Stand:** 2025-12-11
**Status:** **FINAL** (Integrierter Stand WP01WP11: Async Intelligence)
**Quellen:** `Programmplan_V2.2.md`, `Handbuch.md`, `chunking_strategy.md`, `wp04_retriever_scoring.md`.
# Mindnet v2.6 Technische Architektur
**Datei:** `docs/mindnet_technical_architecture_v2.6.md`
**Stand:** 2025-12-12
**Status:** **FINAL** (Integrierter Stand WP01WP15: Smart Edges & Traffic Control)
**Quellen:** `Programmplan_V2.6.md`, `Handbuch.md`, `chunking_strategy.md`, `wp04_retriever_scoring.md`.
> **Ziel dieses Dokuments:**
> 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)**.
> 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)**.
---
<details>
<summary>📖 <b>Inhaltsverzeichnis (Klicken zum Öffnen)</b></summary>
- [Mindnet v2.4 Technische Architektur](#mindnet-v24--technische-architektur)
- [Mindnet v2.6 Technische Architektur](#mindnet-v26--technische-architektur)
- [](#)
- [1. Systemüberblick](#1-systemüberblick)
- [1.1 Architektur-Zielbild](#11-architektur-zielbild)
- [1.2 Verzeichnisstruktur \& Komponenten (Post-WP10)](#12-verzeichnisstruktur--komponenten-post-wp10)
- [1.2 Verzeichnisstruktur \& Komponenten (Post-WP15)](#12-verzeichnisstruktur--komponenten-post-wp15)
- [2. Datenmodell \& Collections (Qdrant)](#2-datenmodell--collections-qdrant)
- [2.1 Notes Collection (`<prefix>_notes`)](#21-notes-collection-prefix_notes)
- [2.2 Chunks Collection (`<prefix>_chunks`)](#22-chunks-collection-prefix_chunks)
@ -27,22 +27,23 @@
- [3.4 Prompts (`config/prompts.yaml`)](#34-prompts-configpromptsyaml)
- [3.5 Environment (`.env`)](#35-environment-env)
- [4. Import-Pipeline (Markdown → Qdrant)](#4-import-pipeline-markdown--qdrant)
- [4.1 Verarbeitungsschritte (Async)](#41-verarbeitungsschritte-async)
- [4.1 Verarbeitungsschritte (Async + Smart)](#41-verarbeitungsschritte-async--smart)
- [5. Retriever-Architektur \& Scoring](#5-retriever-architektur--scoring)
- [5.1 Betriebsmodi](#51-betriebsmodi)
- [5.2 Scoring-Formel (WP04a)](#52-scoring-formel-wp04a)
- [5.3 Explanation Layer (WP04b)](#53-explanation-layer-wp04b)
- [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.1 Architektur-Pattern: Intent Router](#61-architektur-pattern-intent-router)
- [6.2 Schritt 1: Intent Detection (Hybrid)](#62-schritt-1-intent-detection-hybrid)
- [6.3 Schritt 2: Strategy Resolution (Late Binding)](#63-schritt-2-strategy-resolution-late-binding)
- [6.4 Schritt 3: Retrieval vs. Extraction](#64-schritt-3-retrieval-vs-extraction)
- [6.5 Schritt 4: Generation \& Response](#65-schritt-4-generation--response)
- [6.1 Architektur-Pattern: Intent Router v5](#61-architektur-pattern-intent-router-v5)
- [6.2 Traffic Control (Priorisierung)](#62-traffic-control-priorisierung)
- [6.3 Schritt 1: Intent Detection (Question vs. Action)](#63-schritt-1-intent-detection-question-vs-action)
- [6.4 Schritt 2: Strategy Resolution](#64-schritt-2-strategy-resolution)
- [6.5 Schritt 3: Retrieval vs. Extraction](#65-schritt-3-retrieval-vs-extraction)
- [6.6 Schritt 4: Generation \& Response](#66-schritt-4-generation--response)
- [7. Frontend Architektur (WP10)](#7-frontend-architektur-wp10)
- [7.1 Kommunikation](#71-kommunikation)
- [7.2 Features \& UI-Logik](#72-features--ui-logik)
- [7.3 Draft-Editor \& Sanitizer (Neu in WP10a)](#73-draft-editor--sanitizer-neu-in-wp10a)
- [7.3 Draft-Editor \& Healing Parser (Neu in WP10a/v2.5)](#73-draft-editor--healing-parser-neu-in-wp10av25)
- [7.4 State Management (Resurrection Pattern)](#74-state-management-resurrection-pattern)
- [7.5 Deployment Ports](#75-deployment-ports)
- [8. Feedback \& Logging Architektur (WP04c)](#8-feedback--logging-architektur-wp04c)
@ -62,57 +63,63 @@
Mindnet ist ein **lokales RAG-System (Retrieval Augmented Generation)** mit Web-Interface.
1. **Source:** Markdown-Notizen in einem Vault (Obsidian-kompatibel).
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:**
* **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).
4. **Backend:** Eine FastAPI-Anwendung stellt Endpunkte für **Semantische** und **Hybride Suche** sowie **Feedback** bereit.
* **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** und **Intelligence-Features**.
* **Update v2.6:** Der Core arbeitet nun vollständig **asynchron (AsyncIO)** mit **Traffic Control** (Semaphore zur Lastverteilung).
5. **Frontend:** Streamlit-App (`ui.py`) für Interaktion und Visualisierung inkl. **Draft Editor**, **Active Intelligence** und **Healing Parser**.
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`).
### 1.2 Verzeichnisstruktur & Komponenten (Post-WP10)
### 1.2 Verzeichnisstruktur & Komponenten (Post-WP15)
/mindnet/
├── app/
│ ├── main.py # FastAPI Einstiegspunkt
│ ├── core/
│ │ ├── ingestion.py # NEU: Async Ingestion Service (WP11)
│ │ ├── ingestion.py # NEU: Async Ingestion mit Change Detection
│ │ ├── qdrant.py # Client-Factory & Connection
│ │ ├── qdrant_points.py # Low-Level Point Operations (Upsert/Delete)
│ │ ├── note_payload.py # Bau der Note-Objekte
│ │ ├── chunk_payload.py # Bau der Chunk-Objekte
│ │ ├── chunker.py # Text-Zerlegung (Profiling)
│ │ ├── chunker.py # Smart Chunker Orchestrator (WP15)
│ │ ├── edges.py # Edge-Datenstrukturen
│ │ ├── derive_edges.py # Logik der Kantenableitung (WP03)
│ │ ├── graph_adapter.py # Subgraph & Reverse-Lookup (WP04b)
│ │ └── retriever.py # Scoring, Expansion & Explanation (WP04a/b)
│ ├── models/ # Pydantic DTOs
│ │ └── dto.py # Zentrale DTO-Definition
│ ├── routers/
│ │ ├── query.py # Such-Endpunkt
│ │ ├── ingest.py # NEU: API für Save & Analyze (WP11)
│ │ ├── chat.py # Hybrid Router & Interview Logic (WP06/07)
│ │ ├── chat.py # Hybrid Router v5 & Interview Logic (WP06/07)
│ │ ├── feedback.py # Feedback-Endpunkt (WP04c)
│ │ └── ...
│ ├── services/
│ │ ├── llm_service.py # Ollama Chat Client
│ │ ├── embeddings_client.py# NEU: Async Embedding Client (HTTPX)
│ │ └── feedback_service.py # JSONL Logging (WP04c)
│ │ ├── llm_service.py # Ollama Chat Client mit Traffic Control (v2.8.0)
│ │ ├── semantic_analyzer.py# NEU: LLM-Filter für Edges (WP15)
│ │ ├── embeddings_client.py# NEU: Async Embeddings (HTTPX)
│ │ ├── feedback_service.py # Logging (JSONL Writer)
│ │ └── discovery.py # NEU: Intelligence Logic (WP11)
│ ├── frontend/ # NEU (WP10)
│ └── ui.py # Streamlit Application inkl. Sanitizer
├── config/
│ ├── types.yaml # Typ-Definitionen (Import-Zeit)
│ ├── retriever.yaml # Scoring-Gewichte (Laufzeit)
│ ├── decision_engine.yaml # Strategien & Schemas (WP06/WP07)
│ └── prompts.yaml # LLM System-Prompts & Templates (WP06)
│ │ └── ui.py # Streamlit Application inkl. Healing Parser
│ └── main.py # Entrypoint der API
├── config/ # YAML-Konfigurationen (Single Source of Truth)
│ ├── types.yaml # Import-Regeln & Smart-Edge Config
│ ├── prompts.yaml # LLM Prompts & Interview Templates (WP06/07)
│ ├── decision_engine.yaml # Router-Strategien (Actions only)
│ └── retriever.yaml # Scoring-Regeln & Kantengewichte
├── data/
│ └── logs/ # Lokale JSONL-Logs (WP04c)
├── scripts/
│ └── logs/ # Lokale Logs (search_history.jsonl, feedback.jsonl)
├── scripts/ # CLI-Tools (Import, Diagnose, Reset)
│ ├── import_markdown.py # Haupt-Importer CLI (Async)
│ ├── payload_dryrun.py # Diagnose: JSON-Generierung ohne DB
│ └── edges_full_check.py # Diagnose: Graph-Integrität
└── tests/ # Pytest Suite
├── tests/ # Pytest Suite & Smoke Scripts
└── vault/ # Dein lokaler Markdown-Content (Git-ignored)
---
@ -140,7 +147,7 @@ Repräsentiert die Metadaten einer Datei.
### 2.2 Chunks Collection (`<prefix>_chunks`)
Die atomaren Sucheinheiten.
* **Zweck:** Vektorsuche (Embeddings), Granulares Ergebnis.
* **Update v2.3.10:** Vektor-Dimension ist jetzt **768** (für `nomic-embed-text`).
* **Update v2.4:** Vektor-Dimension ist jetzt **768** (für `nomic-embed-text`).
* **Schema (Payload):**
| Feld | Datentyp | Beschreibung |
@ -171,6 +178,7 @@ Gerichtete Kanten. Massiv erweitert in WP03 für Provenienz-Tracking.
| `scope` | Keyword | Geltungsbereich. | Immer `"chunk"` (v2.2). |
| `rule_id` | Keyword | Herkunftsregel. | `explicit:wikilink`, `inline:rel` |
| `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` |
---
@ -181,13 +189,20 @@ Die Logik ist ausgelagert in YAML-Dateien.
### 3.1 Typ-Registry (`config/types.yaml`)
Steuert den Import-Prozess.
* **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):**
types:
concept:
chunk_profile: medium
chunk_profile: sliding_standard
edge_defaults: ["references", "related_to"]
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`)
Steuert das Scoring zur Laufzeit (WP04a).
@ -199,14 +214,14 @@ Steuert das Scoring zur Laufzeit (WP04a).
centrality_weight: 0.5
### 3.3 Decision Engine (`config/decision_engine.yaml`)
**Neu in WP06/07:** Steuert den Intent-Router und die Interview-Schemas.
**Neu in WP06/07:** Steuert den Intent-Router.
* Definiert Strategien (`DECISION`, `INTERVIEW`, etc.).
* Definiert `schemas` für den Interview-Modus (Pflichtfelder pro Typ).
* **Update v2.6:** Enthält nur noch **Handlungs-Keywords** (Verben wie "neu", "erstellen"). Objektnamen ("Projekt") werden nun über `types.yaml` aufgelöst.
* Definiert LLM-Router-Settings (`llm_fallback_enabled`).
### 3.4 Prompts (`config/prompts.yaml`)
Steuert die LLM-Persönlichkeit und Templates.
* Enthält Templates für alle Strategien inkl. `interview_template` mit One-Shot Logik.
* Enthält Templates für alle Strategien inkl. `interview_template` und neu `router_prompt`.
### 3.5 Environment (`.env`)
Erweiterung für LLM-Steuerung und Embedding-Modell:
@ -215,32 +230,33 @@ Erweiterung für LLM-Steuerung und Embedding-Modell:
MINDNET_EMBEDDING_MODEL=nomic-embed-text # NEU in v2.3.10
MINDNET_OLLAMA_URL=http://127.0.0.1:11434
MINDNET_LLM_TIMEOUT=300.0 # Neu: Erhöht für CPU-Inference Cold-Starts
MINDNET_API_TIMEOUT=60.0 # Neu: Timeout für Frontend-API Calls
MINDNET_API_TIMEOUT=300.0 # Neu: Timeout für Frontend-API (Smart Edges brauchen Zeit)
MINDNET_DECISION_CONFIG="config/decision_engine.yaml"
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)
Das Skript `scripts/import_markdown.py` orchestriert den Prozess.
**Neu in v2.3.10:** Der Import nutzt `asyncio` und eine **Semaphore**, um Ollama nicht zu überlasten.
**Neu in v2.6:** Der Import nutzt `asyncio` und **Traffic Control**, um Ollama nicht zu überlasten.
### 4.1 Verarbeitungsschritte (Async)
### 4.1 Verarbeitungsschritte (Async + Smart)
1. **Discovery & Parsing:**
* Einlesen der `.md` Dateien. Hash-Vergleich (Body/Frontmatter) zur Erkennung von Änderungen.
2. **Typauflösung:**
* Bestimmung des `type` via `types.yaml`.
3. **Chunking:**
* Zerlegung via `chunker.py` basierend auf `chunk_profile`.
4. **Embedding (Async):**
* Der `EmbeddingsClient` (`app/services/embeddings_client.py`) sendet Text-Chunks asynchron an Ollama.
* Modell: `nomic-embed-text` (768d).
* Semaphore: Max. 5 gleichzeitige Files, um OOM (Out-of-Memory) zu verhindern.
1. **Discovery & Parsing:** Hash-Vergleich zur Erkennung von Änderungen.
2. **Typauflösung:** Bestimmung des `type` via `types.yaml`.
3. **Config Check:** Laden des `chunk_profile` und `enable_smart_edge_allocation`.
4. **Chunking & Smart Edges (WP15):**
* Zerlegung des Textes via `chunker.py`.
* Wenn Smart Edges aktiv: Der `SemanticAnalyzer` sendet Chunks an das LLM.
* **Traffic Control:** Der Request nutzt `priority="background"`. Die Semaphore (Limit: 2) drosselt die Parallelität.
5. **Kantenableitung (Edge Derivation):**
* `derive_edges.py` erzeugt Inline-, Callout- und Default-Edges.
6. **Upsert:**
6. **Embedding (Async):**
* Der `EmbeddingsClient` (`app/services/embeddings_client.py`) sendet Text-Chunks asynchron an Ollama.
* Modell: `nomic-embed-text` (768d).
7. **Upsert:**
* Schreiben in Qdrant. Nutzung von `--purge-before-upsert`.
* **Strict Mode:** Der Prozess bricht ab, wenn Embeddings leer sind oder Dimension `0` haben.
@ -269,49 +285,55 @@ $$
* **Gewichte ($W$):** Stammen aus `retriever.yaml`.
### 5.3 Explanation Layer (WP04b)
Der Retriever kann Ergebnisse erklären (`explain=True`).
* **Logik:**
* Berechnung des `ScoreBreakdown` (Anteile von Semantik, Graph, Typ).
* Analyse des lokalen Subgraphen mittels `graph_adapter.py`.
* **Incoming Edges (Authority):** Wer zeigt auf diesen Treffer? (z.B. "Referenziert von...")
* **Outgoing Edges (Hub):** Worauf zeigt dieser Treffer? (z.B. "Verweist auf...")
* **Output:** `QueryHit` enthält ein `explanation` Objekt mit menschenlesbaren `reasons` und `related_edges`.
Der Retriever ist keine Blackbox mehr. Er liefert auf Wunsch (`explain=True`) eine strukturierte Begründung (`Explanation`-Objekt).
**Die "Warum"-Logik:**
1. **Semantik:** Prüfung der Cosine-Similarity ("Sehr hohe textuelle Übereinstimmung").
2. **Typ:** Prüfung des `retriever_weight` ("Bevorzugt, da Entscheidung").
3. **Graph (Kontext):**
* **Hub (Outgoing):** Worauf verweist dieser Treffer? ("Verweist auf Qdrant").
* **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
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. Der `ChatRouter` (`app/routers/chat.py`) fungiert als zentraler Dispatcher.
Der Flow für eine Chat-Anfrage (`/chat`) wurde in WP06 auf eine **Configuration-Driven Architecture** umgestellt und in WP15 (v2.6) verfeinert.
### 6.1 Architektur-Pattern: Intent Router
### 6.1 Architektur-Pattern: Intent Router v5
Die Behandlung einer Anfrage ist nicht mehr hartkodiert, sondern wird dynamisch zur Laufzeit entschieden.
* **Input:** User Message.
* **Config:** `config/decision_engine.yaml` (Strategien & Keywords).
* **Config:** `decision_engine.yaml` (Strategien) + `types.yaml` (Objekte).
* **Komponenten:**
* **Fast Path:** Keyword Matching (CPU-schonend).
* **Slow Path:** LLM-basierter Semantic Router (für subtile Intents).
* **Traffic Control:** `LLMService` priorisiert Chat-Anfragen.
* **Question Detection:** Unterscheidung Frage vs. Befehl.
### 6.2 Schritt 1: Intent Detection (Hybrid)
### 6.2 Traffic Control (Priorisierung)
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.
1. **Keyword Scan (Fast Path):**
* Iteration über alle Strategien in `decision_engine.yaml`.
* Prüfung auf `trigger_keywords`.
* **Best Match:** Bei mehreren Treffern gewinnt das längste/spezifischste Keyword (Robustheit gegen Shadowing).
2. **LLM Fallback (Slow Path):**
* Nur aktiv, wenn `llm_fallback_enabled: true`.
* 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`.
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.
2. **Keyword Scan (Fast Path):**
* Prüfung auf `trigger_keywords` (Handlung) in `decision_engine.yaml`.
* Prüfung auf `detection_keywords` (Objekt) in `types.yaml`.
* Treffer -> **INTERVIEW Modus** (Erfassen).
3. **LLM Fallback (Slow Path):**
* Greift, wenn keine Keywords passen. Sendet Query an LLM Router.
### 6.3 Schritt 2: Strategy Resolution (Late Binding)
### 6.4 Schritt 2: Strategy Resolution
Basierend auf dem Intent lädt der Router die Parameter:
* **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`).
### 6.4 Schritt 3: Retrieval vs. Extraction
### 6.5 Schritt 3: Retrieval vs. Extraction
Der Router verzweigt hier:
**A) RAG Modus (FACT, DECISION, EMPATHY):**
@ -324,9 +346,9 @@ Der Router verzweigt hier:
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.
### 6.5 Schritt 4: Generation & Response
### 6.6 Schritt 4: Generation & Response
* **Templating:** Das LLM erhält den Prompt basierend auf dem gewählten Template.
* **Execution:** Der `LLMService` führt den Call aus. Ein konfigurierbarer Timeout (`MINDNET_LLM_TIMEOUT`) fängt Cold-Start-Verzögerungen auf CPU-Hardware ab.
* **Execution:** Der `LLMService` führt den Call mit `priority="realtime"` aus.
* **Response:** Rückgabe enthält Antworttext (im Interview-Modus: Markdown Codeblock), Quellenliste und den erkannten `intent`.
---
@ -337,8 +359,12 @@ Das Frontend ist eine **Streamlit-Anwendung** (`app/frontend/ui.py`), die als se
### 7.1 Kommunikation
* **Backend-URL:** Konfiguriert via `MINDNET_API_URL` (Default: `http://localhost:8002`).
* **Endpoints:** Nutzt `/chat` für Interaktion, `/feedback` für Bewertungen und `/ingest/analyze` für Intelligence.
* **Resilienz:** Das Frontend implementiert eigene Timeouts (`MINDNET_API_TIMEOUT`, Default 300s).
* **Endpoints:**
* `/chat` (Interaktion)
* `/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
* **State Management:** Session-State speichert Chat-Verlauf und `query_id`.
@ -347,16 +373,16 @@ Das Frontend ist eine **Streamlit-Anwendung** (`app/frontend/ui.py`), die als se
* **Source Expanders:** Zeigt verwendete Chunks inkl. Score und "Why"-Explanation.
* **Sidebar:** Zeigt Suchhistorie (Log-basiert) und Konfiguration.
### 7.3 Draft-Editor & Sanitizer (Neu in WP10a)
### 7.3 Draft-Editor & Healing Parser (Neu in WP10a/v2.5)
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.
* **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`):**
* Prüft den YAML-Frontmatter auf unerlaubte Felder (Halluzinationen des LLMs).
* 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.
4. **Action:** Buttons zum Download oder Kopieren des fertigen Markdowns.
4. **Save Action:** Speichert über `/ingest/save` atomar in Vault und DB. Erzeugt intelligente Dateinamen via `slugify`.
### 7.4 State Management (Resurrection Pattern)
Um Datenverlust bei Tab-Wechseln (Chat <-> Editor) zu verhindern, nutzt `ui.py` ein Persistenz-Muster:
@ -420,5 +446,5 @@ Validierung erfolgt über `tests/ensure_indexes_and_show.py`.
2. **Unresolved Targets:**
* Kanten zu Notizen, die noch nicht existieren, werden mit `target_id="Titel"` angelegt.
* Heilung durch `scripts/resolve_unresolved_references.py` möglich.
3. **Vektor-Konfiguration für Edges:**
* `mindnet_edges` hat aktuell keine Vektoren (`vectors = null`). Eine semantische Suche *auf Kanten* ist noch nicht möglich.
3. **Hardware-Last bei Smart Import:**
* Der "Smart Edge" Import ist rechenintensiv. Trotz Traffic Control kann die Antwortzeit im Chat leicht steigen, wenn die GPU am VRAM-Limit arbeitet.

View File

@ -1,7 +1,7 @@
# mindnet v2.4 Pipeline Playbook
**Datei:** `docs/mindnet_pipeline_playbook_v2.4.md`
**Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. Async Ingestion & Active Intelligence)
**Datei:** `docs/mindnet_pipeline_playbook_v2.6.md`
**Stand:** 2025-12-12
**Status:** **FINAL** (Inkl. Smart Edges & Traffic Control)
**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)
- [2. Die Import-Pipeline (Runbook)](#2-die-import-pipeline-runbook)
- [2.1 Der 12-Schritte-Prozess (Async)](#21-der-12-schritte-prozess-async)
- [2.1 Der 13-Schritte-Prozess (Async + Smart)](#21-der-13-schritte-prozess-async--smart)
- [2.2 Standard-Betrieb (Inkrementell)](#22-standard-betrieb-inkrementell)
- [2.3 Deployment \& Restart (Systemd)](#23-deployment--restart-systemd)
- [2.4 Full Rebuild (Clean Slate)](#24-full-rebuild-clean-slate)
@ -21,12 +21,13 @@
- [3.2 Payload-Felder](#32-payload-felder)
- [4. Edge-Erzeugung (Die V2-Logik)](#4-edge-erzeugung-die-v2-logik)
- [4.1 Prioritäten \& Provenance](#41-prioritäten--provenance)
- [4.2 Typ-Defaults](#42-typ-defaults)
- [4.2 Smart Edge Allocation (WP15)](#42-smart-edge-allocation-wp15)
- [4.3 Typ-Defaults](#43-typ-defaults)
- [5. Retriever, Chat \& Generation (RAG Pipeline)](#5-retriever-chat--generation-rag-pipeline)
- [5.1 Retrieval (Hybrid)](#51-retrieval-hybrid)
- [5.2 Intent Router (WP06/07)](#52-intent-router-wp0607)
- [5.3 Context Enrichment](#53-context-enrichment)
- [5.4 Generation (LLM)](#54-generation-llm)
- [5.4 Generation (LLM) mit Traffic Control](#54-generation-llm-mit-traffic-control)
- [5.5 Active Intelligence Pipeline (Neu in v2.4)](#55-active-intelligence-pipeline-neu-in-v24)
- [6. Feedback \& Lernen (WP04c)](#6-feedback--lernen-wp04c)
- [7. Quality Gates \& Tests](#7-quality-gates--tests)
@ -34,7 +35,7 @@
- [7.2 Smoke-Test (E2E)](#72-smoke-test-e2e)
- [8. Ausblick \& Roadmap (Technische Skizzen)](#8-ausblick--roadmap-technische-skizzen)
- [8.1 WP-08: Self-Tuning (Skizze)](#81-wp-08-self-tuning-skizze)
- [16. Workpackage Status (v2.4.0)](#16-workpackage-status-v240)
- [16. Workpackage Status (v2.6.0)](#16-workpackage-status-v260)
</details>
---
@ -45,7 +46,7 @@ Dieses Playbook ist das zentrale operative Handbuch für die **mindnet-Pipeline*
**Zielgruppe:** Dev/Ops, Tech-Leads.
**Scope:**
* **Ist-Stand (WP01WP11):** Async Import, Chunking, Edge-Erzeugung, Hybrider Retriever, RAG-Chat (Hybrid Router), Feedback Loop, Frontend, Draft Editor, Active Intelligence.
* **Ist-Stand (WP01WP15):** Async Import, Smart 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).
---
@ -54,23 +55,32 @@ 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.
### 2.1 Der 12-Schritte-Prozess (Async)
Seit v2.3.10 läuft der Import **asynchron**, um Netzwerk-Blockaden bei der Embedding-Generierung zu vermeiden.
### 2.1 Der 13-Schritte-Prozess (Async + Smart)
Seit v2.6 läuft der Import vollständig asynchron, nutzt intelligente Kantenvalidierung (Smart Edges) und drosselt sich selbst ("Traffic Control").
1. **Markdown lesen:** Rekursives Scannen des Vaults.
2. **Frontmatter extrahieren:** Validierung von Pflichtfeldern (`id`, `type`, `title`).
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`.
5. **Chunking anwenden:** Zerlegung des Textes basierend auf dem `chunk_profile` des Typs.
6. **Inline-Kanten finden:** Parsing von `[[rel:...]]` im Fließtext.
7. **Callout-Kanten finden:** Parsing von `> [!edge]` Blöcken.
8. **Default-Edges erzeugen:** Anwendung der `edge_defaults` aus der Typ-Registry.
9. **Strukturkanten erzeugen:** `belongs_to` (Chunk->Note), `next`/`prev` (Sequenz).
10. **Embedding & Upsert (Async):**
* Das System nutzt eine **Semaphore** (Limit: 5 Files concurrent), um Ollama nicht zu überlasten.
5. **Chunking anwenden:** Zerlegung des Textes basierend auf dem `chunk_profile` des Typs (z.B. `sliding_smart_edges`).
6. **Smart Edge Allocation (Neu in WP15):**
* Wenn in `types.yaml` aktiviert (`enable_smart_edge_allocation`):
* Der `SemanticAnalyzer` sendet jeden Chunk an das LLM.
* **Resilienz & Traffic Control:**
* **Priority:** Der Request nutzt `priority="background"`.
* **Semaphore:** Eine globale Semaphore (Limit: 2, konfigurierbar) verhindert System-Überlastung.
* **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).
11. **Strict Mode:** Der Prozess bricht sofort ab, wenn ein Embedding leer ist oder die Dimension `0` hat.
12. **Diagnose:** Automatischer Check der Integrität nach dem Lauf.
12. **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.
### 2.2 Standard-Betrieb (Inkrementell)
Für regelmäßige Updates (z.B. Cronjob). Erkennt Änderungen via Hash.
@ -102,7 +112,7 @@ Nach einem Import oder Code-Update müssen die API-Prozesse neu gestartet werden
sudo systemctl status mindnet-prod
### 2.4 Full Rebuild (Clean Slate)
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`).
Notwendig bei Änderungen an `types.yaml` (z.B. Smart Edges an/aus) oder beim Wechsel des Embedding-Modells.
**WICHTIG:** Vorher das Modell pullen, sonst schlägt der Import fehl!
@ -123,10 +133,10 @@ Das Chunking ist profilbasiert und typgesteuert.
### 3.1 Chunk-Profile
In `types.yaml` definiert. Standard-Profile (in `chunk_config.py` implementiert):
* `short`: Max 128 Tokens (z.B. für Logs, Chats).
* `medium`: Max 256 Tokens (z.B. für Konzepte).
* `long`: Max 512 Tokens (z.B. für Essays, Projekte).
* `by_heading`: Trennt strikt an Überschriften.
* `sliding_short`: Max 128 Tokens (z.B. für Logs, Chats).
* `sliding_standard`: Max 512 Tokens (Standard für Massendaten).
* `sliding_smart_edges`: Sliding Window, optimiert für LLM-Analyse (Fließtext).
* `structured_smart_edges`: Trennt strikt an Überschriften (für strukturierte Daten).
### 3.2 Payload-Felder
Jeder Chunk erhält zwei Text-Felder:
@ -137,7 +147,7 @@ Jeder Chunk erhält zwei Text-Felder:
## 4. Edge-Erzeugung (Die V2-Logik)
In v2.2 entstehen Kanten nach strenger Priorität.
In v2.6 entstehen Kanten nach strenger Priorität.
### 4.1 Prioritäten & Provenance
Der Importer setzt `provenance`, `rule_id` und `confidence` automatisch:
@ -147,10 +157,17 @@ Der Importer setzt `provenance`, `rule_id` und `confidence` automatisch:
| **1** | Inline | `[[rel:depends_on X]]` | `inline:rel` | ~0.95 |
| **2** | Callout | `> [!edge] related_to: [[X]]` | `callout:edge` | ~0.90 |
| **3** | Wikilink | `[[X]]` | `explicit:wikilink` | 1.00 |
| **4** | Default | *(via types.yaml)* | `edge_defaults:...` | ~0.70 |
| **5** | Struktur | *(automatisch)* | `structure:...` | 1.00 |
| **4** | Smart | *(via LLM Filter)* | `smart:llm_filter` | 0.90 |
| **5** | Default | *(via types.yaml)* | `edge_defaults:...` | ~0.70 |
| **6** | Struktur | *(automatisch)* | `structure:...` | 1.00 |
### 4.2 Typ-Defaults
### 4.2 Smart Edge Allocation (WP15)
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.
* *Beispiel:* Note Typ `project` verlinkt `[[Tool A]]`.
* *Ergebnis:* Kante `references` (explizit) UND Kante `depends_on` (Default).
@ -165,25 +182,23 @@ 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).
### 5.2 Intent Router (WP06/07)
Der Request durchläuft den **Hybrid Router**:
1. **Fast Path:** Prüfung auf `trigger_keywords` aus `decision_engine.yaml`.
2. **Slow Path:** Falls kein Keyword matched und `llm_fallback_enabled=true`, klassifiziert das LLM den Intent.
* `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`.
Der Request durchläuft den **Hybrid Router v5**:
1. **Question Detection:** Ist es eine Frage (`?`, W-Wörter)? -> RAG Modus. Interviews werden hier blockiert.
2. **Keyword Scan:** Enthält es Keywords aus `types.yaml` (Objekt, z.B. "Projekt") oder `decision_engine.yaml` (Action, z.B. "erstellen")? -> INTERVIEW Modus.
3. **LLM Fallback:** Wenn unklar, entscheidet das LLM.
### 5.3 Context Enrichment
Der Router (`chat.py`) reichert die gefundenen Chunks mit Metadaten an:
* **Typ-Injection:** `[DECISION]`, `[PROJECT]`.
* **Reasoning-Infos:** `(Score: 0.75)`.
### 5.4 Generation (LLM)
### 5.4 Generation (LLM) mit Traffic Control
* **Engine:** Ollama (lokal).
* **Modell:** `phi3:mini` (Standard).
* **Prompting:** Template wird basierend auf Intent gewählt (`decision_template`, `interview_template` etc.).
* **One-Shot (WP07):** Im Interview-Modus generiert das LLM direkt einen Markdown-Block ohne Rückfragen.
* **Traffic Control:** Der `LLMService` unterscheidet:
* **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)
Ein paralleler Datenfluss im Frontend ("Draft Editor") zur Unterstützung des Autors.
@ -252,7 +267,7 @@ Wie entwickeln wir die Pipeline weiter?
---
## 16. Workpackage Status (v2.4.0)
## 16. Workpackage Status (v2.6.0)
Aktueller Implementierungsstand der Module.
@ -270,4 +285,7 @@ Aktueller Implementierungsstand der Module.
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
| **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
**Datei:** `docs/mindnet_user_guide_v2.4.md`
**Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. RAG, Web-Interface & Interview-Assistent & Intelligence)
**Datei:** `docs/mindnet_user_guide_v2.6.md`
**Stand:** 2025-12-12
**Status:** **FINAL** (Inkl. Smart Edges, Hybrid Router v5 & Healing UI)
**Quellen:** `knowledge_design.md`, `wp04_retriever_scoring.md`, `Programmplan_V2.2.md`, `Handbuch.md`.
> **Willkommen bei Mindnet.**
@ -18,6 +18,7 @@ Wenn du nach "Projekt Alpha" suchst, findet Mindnet nicht nur das Dokument. Es f
* **Abhängigkeiten:** "Technologie X wird benötigt".
* **Entscheidungen:** "Warum nutzen wir X?".
* **Ä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)
Mindnet passt seinen Charakter dynamisch an deine Frage an:
@ -52,28 +53,22 @@ Seit Version 2.3.1 bedienst du Mindnet über eine grafische Oberfläche im Brows
## 3. Den Chat steuern (Intents)
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.
Du steuerst die Persönlichkeit von Mindnet durch deine Wortwahl. Das System nutzt einen **Hybrid Router v5**, der intelligent zwischen Frage und Befehl unterscheidet.
### 3.1 Modus: Entscheidung ("Der Berater")
Wenn du vor einer Wahl stehst, hilft Mindnet dir, konform zu deinen Prinzipien zu bleiben.
### 3.1 Frage-Modus (Wissen abrufen)
Sobald du ein Fragezeichen `?` benutzt oder Wörter wie "Wer", "Wie", "Was", "Soll ich" verwendest, sucht Mindnet nach Antworten (**RAG**).
* **Auslöser (Keywords):** "Soll ich...", "Was ist deine Meinung?", "Strategie für...", "Vor- und Nachteile".
* **Was passiert:** Mindnet lädt deine **Werte** (`type: value`) und **Ziele** (`type: goal`) in den Kontext und prüft die Fakten dagegen.
* **Beispiel-Dialog:**
* *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'."
* **Entscheidung ("Soll ich?"):** Mindnet lädt deine **Werte** (`type: value`) und **Ziele** (`type: goal`) in den Kontext und prüft die Fakten dagegen.
* *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'."
* **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:* "Ich bin frustriert." -> "Das erinnert mich an Projekt Y, da ging es uns ähnlich..."
### 3.2 Modus: Empathie ("Der Spiegel")
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.
### 3.2 Befehls-Modus (Wissen erfassen / Interview)
Wenn du keine Frage stellst, sondern eine Absicht äußerst, wechselt Mindnet in den **Interview-Modus**.
* **Auslöser:** "Neues Projekt", "Notiz erstellen", "Ich will etwas festhalten", "Neue Entscheidung dokumentieren".
* **Was passiert:** Siehe Kapitel 6.3.
* **Was passiert:** Mindnet sucht nicht im Archiv, sondern öffnet den **Draft-Editor**.
* **Beispiel:** "Neue Erfahrung: Streit am Recyclinghof." -> Das System erstellt sofort eine strukturierte Notiz mit den Feldern "Situation", "Reaktion" und "Learning".
---
@ -127,9 +122,10 @@ Mindnet kann dir helfen, Markdown-Notizen zu schreiben.
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**.
* 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).
4. **Finalisierung:** Ergänze die fehlenden Infos direkt im Editor und klicke auf **Download** oder **Kopieren**.
5. **Speichern:** Speichere die Datei in deinen Obsidian Vault. Beim nächsten Import ist sie im System.
5. **Speichern:** Klicke auf "💾 Speichern". Die Notiz landet sofort im Vault und im Index.
### 6.4 Der Intelligence-Workflow (Neu in v2.4)
Wenn du Texte im **manuellen Editor** schreibst, unterstützt dich Mindnet aktiv bei der Vernetzung:

View File

@ -11,6 +11,13 @@ import logging
from pathlib import Path
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
# Stellen wir sicher, dass der Pfad stimmt (Pythonpath)
import sys

View File

@ -0,0 +1,105 @@
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

@ -0,0 +1,110 @@
# 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

@ -0,0 +1,147 @@
# 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()

76
tests/test_wp15_final.py Normal file
View File

@ -0,0 +1,76 @@
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()