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 # mindnet v2.4 — Programmplan
**Version:** 2.4.0 (Inkl. WP-11 Backend Intelligence) **Version:** 2.6.0 (Inkl. WP-15 Smart Edge Allocation)
**Stand:** 2025-12-11 **Stand:** 2025-12-12
**Status:** Aktiv **Status:** Aktiv
--- ---
@ -33,6 +33,9 @@
- [WP-12 Knowledge Rewriter (Soft Mode, geplant)](#wp-12--knowledge-rewriter-soft-mode-geplant) - [WP-12 Knowledge Rewriter (Soft Mode, geplant)](#wp-12--knowledge-rewriter-soft-mode-geplant)
- [WP-13 MCP-Integration \& Agenten-Layer (geplant)](#wp-13--mcp-integration--agenten-layer-geplant) - [WP-13 MCP-Integration \& Agenten-Layer (geplant)](#wp-13--mcp-integration--agenten-layer-geplant)
- [WP-14 Review / Refactoring / Dokumentation (geplant)](#wp-14--review--refactoring--dokumentation-geplant) - [WP-14 Review / Refactoring / Dokumentation (geplant)](#wp-14--review--refactoring--dokumentation-geplant)
- [WP-15 Smart Edge Allocation \& Chunking Strategies (abgeschlossen)](#wp-15--smart-edge-allocation--chunking-strategies-abgeschlossen)
- [WP-16 Auto-Discovery \& Enrichment (geplant)](#wp-16--auto-discovery--enrichment-geplant)
- [WP-17 Conversational Memory (Dialog-Gedächtnis) (geplant)](#wp-17--conversational-memory-dialog-gedächtnis-geplant)
- [7. Abhängigkeiten (vereinfacht, aktualisiert)](#7-abhängigkeiten-vereinfacht-aktualisiert) - [7. Abhängigkeiten (vereinfacht, aktualisiert)](#7-abhängigkeiten-vereinfacht-aktualisiert)
- [8. Laufzeit- \& Komplexitätsindikatoren (aktualisiert)](#8-laufzeit---komplexitätsindikatoren-aktualisiert) - [8. Laufzeit- \& Komplexitätsindikatoren (aktualisiert)](#8-laufzeit---komplexitätsindikatoren-aktualisiert)
- [9. Programmfortschritt (Ampel, aktualisiert)](#9-programmfortschritt-ampel-aktualisiert) - [9. Programmfortschritt (Ampel, aktualisiert)](#9-programmfortschritt-ampel-aktualisiert)
@ -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) ## 7. Abhängigkeiten (vereinfacht, aktualisiert)
WP01 → WP02 → WP03 → WP04a WP01 → WP02 → WP03 → WP04a
@ -522,6 +582,8 @@ Aufräumen, dokumentieren, stabilisieren insbesondere für Onboarding Dritte
WP07 → WP10a WP07 → WP10a
WP03 → WP09 WP03 → WP09
WP01/WP03 → WP10 → WP11 → WP12 WP01/WP03 → WP10 → WP11 → WP12
WP10 → WP17
WP11 → WP15 → WP16
WP03/WP04 → WP13 WP03/WP04 → WP13
Alles → WP14 Alles → WP14
@ -544,6 +606,9 @@ Aufräumen, dokumentieren, stabilisieren insbesondere für Onboarding Dritte
| WP12 | Niedrig/Mittel | Mittel | | WP12 | Niedrig/Mittel | Mittel |
| WP13 | Mittel | Mittel | | WP13 | Mittel | Mittel |
| WP14 | Mittel | Niedrig/Mittel | | WP14 | Mittel | Niedrig/Mittel |
| WP15 | Mittel | Hoch |
| WP16 | Mittel | Hoch |
| WP17 | Mittel | Mittel |
--- ---
@ -569,6 +634,9 @@ Aufräumen, dokumentieren, stabilisieren insbesondere für Onboarding Dritte
| WP12 | 🟡 | | WP12 | 🟡 |
| WP13 | 🟡 | | WP13 | 🟡 |
| WP14 | 🟡 | | 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 __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Dict, Optional, Tuple from typing import List, Dict, Optional, Tuple, Any, Set
import re import re
import math import math
import yaml
from pathlib import Path
from markdown_it import MarkdownIt from markdown_it import MarkdownIt
from markdown_it.token import Token from markdown_it.token import Token
from .chunk_config import get_sizes import asyncio
import logging
# --- Hilfen --- # Services
_SENT_SPLIT = re.compile(r'(?<=[.!?])\s+(?=[A-ZÄÖÜ0-9„(])') from app.services.semantic_analyzer import get_semantic_analyzer
_WS = re.compile(r'\s+')
# Core Imports
try:
from app.core.derive_edges import build_edges_for_note
except ImportError:
# Mock für Tests
def build_edges_for_note(note_id, chunks, note_level_references=None, include_note_scope_refs=False): return []
logger = logging.getLogger(__name__)
# ==========================================
# 1. HELPER & CONFIG
# ==========================================
BASE_DIR = Path(__file__).resolve().parent.parent.parent
CONFIG_PATH = BASE_DIR / "config" / "types.yaml"
DEFAULT_PROFILE = {"strategy": "sliding_window", "target": 400, "max": 600, "overlap": (50, 80)}
_CONFIG_CACHE = None
def _load_yaml_config() -> Dict[str, Any]:
global _CONFIG_CACHE
if _CONFIG_CACHE is not None: return _CONFIG_CACHE
if not CONFIG_PATH.exists(): return {}
try:
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
_CONFIG_CACHE = data
return data
except Exception: return {}
def get_chunk_config(note_type: str) -> Dict[str, Any]:
full_config = _load_yaml_config()
profiles = full_config.get("chunking_profiles", {})
type_def = full_config.get("types", {}).get(note_type.lower(), {})
profile_name = type_def.get("chunking_profile")
if not profile_name:
profile_name = full_config.get("defaults", {}).get("chunking_profile", "sliding_standard")
config = profiles.get(profile_name, DEFAULT_PROFILE).copy()
if "overlap" in config and isinstance(config["overlap"], list):
config["overlap"] = tuple(config["overlap"])
return config
def extract_frontmatter_from_text(md_text: str) -> Tuple[Dict[str, Any], str]:
fm_match = re.match(r'^\s*---\s*\n(.*?)\n---', md_text, re.DOTALL)
if not fm_match: return {}, md_text
try:
frontmatter = yaml.safe_load(fm_match.group(1))
if not isinstance(frontmatter, dict): frontmatter = {}
except yaml.YAMLError:
frontmatter = {}
text_without_fm = re.sub(r'^\s*---\s*\n(.*?)\n---', '', md_text, flags=re.DOTALL)
return frontmatter, text_without_fm.strip()
# ==========================================
# 2. DATA CLASSES
# ==========================================
_SENT_SPLIT = re.compile(r'(?<=[.!?])\s+(?=[A-ZÄÖÜ0-9„(])'); _WS = re.compile(r'\s+')
def estimate_tokens(text: str) -> int: def estimate_tokens(text: str) -> int:
# leichte Approximation: 1 Token ≈ 4 Zeichen; robust + schnell return max(1, math.ceil(len(text.strip()) / 4))
t = len(text.strip())
return max(1, math.ceil(t / 4))
def split_sentences(text: str) -> list[str]: def split_sentences(text: str) -> list[str]:
text = _WS.sub(' ', text.strip()) text = _WS.sub(' ', text.strip())
if not text: if not text: return []
return []
parts = _SENT_SPLIT.split(text) parts = _SENT_SPLIT.split(text)
return [p.strip() for p in parts if p.strip()] return [p.strip() for p in parts if p.strip()]
@dataclass @dataclass
class RawBlock: class RawBlock:
kind: str # "heading" | "paragraph" | "list" | "code" | "table" | "thematic_break" | "blockquote" kind: str; text: str; level: Optional[int]; section_path: str; section_title: Optional[str]
text: str
level: Optional[int] # heading level (2,3,...) or None
section_path: str # e.g., "/H2 Title/H3 Subtitle"
@dataclass @dataclass
class Chunk: class Chunk:
id: str id: str; note_id: str; index: int; text: str; window: str; token_count: int
note_id: str section_title: Optional[str]; section_path: str
index: int neighbors_prev: Optional[str]; neighbors_next: Optional[str]
text: str suggested_edges: Optional[List[str]] = None
token_count: int
section_title: Optional[str]
section_path: str
neighbors_prev: Optional[str]
neighbors_next: Optional[str]
char_start: int
char_end: int
# --- Markdown zu RawBlocks: H2/H3 als Sections, andere Blöcke gruppiert --- # ==========================================
def parse_blocks(md_text: str) -> List[RawBlock]: # 3. PARSING & STRATEGIES (SYNCHRON)
md = MarkdownIt("commonmark").enable("table") # ==========================================
tokens: List[Token] = md.parse(md_text)
blocks: List[RawBlock] = [] def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]:
h2, h3 = None, None """Zerlegt Text in logische Blöcke (Absätze, Header)."""
blocks = []
h1_title = "Dokument"
section_path = "/" section_path = "/"
cur_text = [] current_h2 = None
cur_kind = 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]): lines = text_without_fm.split('\n')
nonlocal section_path buffer = []
txt = txt.strip()
if not txt: for line in lines:
return stripped = line.strip()
title = None if stripped.startswith('# '):
if kind == "heading" and lvl: continue
title = txt elif stripped.startswith('## '):
blocks.append(RawBlock(kind=kind, text=txt, level=lvl, section_path=section_path)) if buffer:
content = "\n".join(buffer).strip()
i = 0 if content:
while i < len(tokens): blocks.append(RawBlock("paragraph", content, None, section_path, current_h2))
t = tokens[i] buffer = []
if t.type == "heading_open": current_h2 = stripped[3:].strip()
lvl = int(t.tag[1]) section_path = f"/{current_h2}"
# Sammle heading inline blocks.append(RawBlock("heading", stripped, 2, section_path, current_h2))
i += 1 elif not stripped:
title_txt = "" if buffer:
while i < len(tokens) and tokens[i].type != "heading_close": content = "\n".join(buffer).strip()
if tokens[i].type == "inline": if content:
title_txt += tokens[i].content blocks.append(RawBlock("paragraph", content, None, section_path, current_h2))
i += 1 buffer = []
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))
else: 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 = [] buf = []
def _emit(text_block: str):
nonlocal chunks, char_pos
idx = len(chunks)
chunk_id = f"{note_id}#c{idx:02d}"
token_count = estimate_tokens(text_block)
# section aus letztem buffer-entry ableiten
sec_title = buf[-1][1] if buf else None
sec_path = buf[-1][2] if buf else "/"
start = char_pos
end = start + len(text_block)
chunks.append(Chunk(
id=chunk_id,
note_id=note_id,
index=idx,
text=text_block,
token_count=token_count,
section_title=sec_title,
section_path=sec_path,
neighbors_prev=None,
neighbors_next=None,
char_start=start,
char_end=end
))
char_pos = end + 1
# Blocks in Puffer sammeln; bei Überschreiten Zielbereich flushen
cur_sec_title = None
for b in blocks: for b in blocks:
if b.kind == "heading" and b.level in (2, 3): if b.kind == "heading": continue
# Sectionwechsel ⇒ Buffer flushen 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() flush_buffer()
cur_sec_title = b.text.strip() buf.append(b)
# Heading selbst nicht als Chunk, aber als Kontexttitel nutzen if estimate_tokens(b.text) >= target:
continue
txt = b.text.strip()
if not txt:
continue
tentative = "\n\n".join([*(x[0] for x in buf), txt]).strip()
if estimate_tokens(tentative) > max(get_sizes(note_type)["target"]):
# weicher Schnitt vor Hinzufügen
flush_buffer()
buf.append((txt, cur_sec_title, b.section_path))
# bei Erreichen ~Target flushen
if estimate_tokens("\n\n".join([x[0] for x in buf])) >= target:
flush_buffer() flush_buffer()
flush_buffer(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): for i, ch in enumerate(chunks):
ch.neighbors_prev = chunks[i-1].id if i > 0 else None ch.neighbors_prev = chunks[i-1].id if i > 0 else None
ch.neighbors_next = chunks[i+1].id if i < len(chunks)-1 else None ch.neighbors_next = chunks[i+1].id if i < len(chunks)-1 else None
return chunks 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 app/core/ingestion.py
Zentraler Service für die Transformation von Markdown-Dateien in Qdrant-Objekte (Notes, Chunks, Edges). Zentraler Service für die Transformation von Markdown-Dateien in Qdrant-Objekte.
Dient als Shared Logic für: Version: 2.5.2 (Full Feature: Change Detection + Robust IO + Clean Config)
1. CLI-Imports (scripts/import_markdown.py)
2. API-Uploads (WP-11)
Refactored for Async Embedding Support.
""" """
import os import os
import logging import logging
import asyncio
import time
from typing import Dict, List, Optional, Tuple, Any from typing import Dict, List, Optional, Tuple, Any
# Core Module Imports # Core Module Imports
@ -18,22 +17,14 @@ from app.core.parser import (
validate_required_frontmatter, validate_required_frontmatter,
) )
from app.core.note_payload import make_note_payload from app.core.note_payload import make_note_payload
from app.core.chunker import assemble_chunks from app.core.chunker import assemble_chunks, get_chunk_config
from app.core.chunk_payload import make_chunk_payloads from app.core.chunk_payload import make_chunk_payloads
# Fallback für Edges Import (Robustheit) # Fallback für Edges
try: try:
from app.core.derive_edges import build_edges_for_note from app.core.derive_edges import build_edges_for_note
except ImportError: except ImportError:
try: def build_edges_for_note(*args, **kwargs): return []
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 []
from app.core.qdrant import QdrantConfig, get_client, ensure_collections, ensure_payload_indexes from app.core.qdrant import QdrantConfig, get_client, ensure_collections, ensure_payload_indexes
from app.core.qdrant_points import ( from app.core.qdrant_points import (
@ -43,30 +34,22 @@ from app.core.qdrant_points import (
upsert_batch, upsert_batch,
) )
# WICHTIG: Wir nutzen den API-Client für Embeddings (Async Support)
from app.services.embeddings_client import EmbeddingsClient from app.services.embeddings_client import EmbeddingsClient
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# --- Helper für Type-Registry --- # --- Helper ---
def load_type_registry(custom_path: Optional[str] = None) -> dict: def load_type_registry(custom_path: Optional[str] = None) -> dict:
import yaml import yaml
path = custom_path or os.getenv("MINDNET_TYPES_FILE", "config/types.yaml") path = custom_path or os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
if not os.path.exists(path): if not os.path.exists(path): return {}
if os.path.exists("types.yaml"):
path = "types.yaml"
else:
return {}
try: try:
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {}
return yaml.safe_load(f) or {} except Exception: return {}
except Exception:
return {}
def resolve_note_type(requested: Optional[str], reg: dict) -> str: def resolve_note_type(requested: Optional[str], reg: dict) -> str:
types = reg.get("types", {}) types = reg.get("types", {})
if requested and requested in types: if requested and requested in types: return requested
return requested
return "concept" return "concept"
def effective_chunk_profile(note_type: str, reg: dict) -> str: def effective_chunk_profile(note_type: str, reg: dict) -> str:
@ -84,7 +67,6 @@ def effective_retriever_weight(note_type: str, reg: dict) -> float:
class IngestionService: class IngestionService:
def __init__(self, collection_prefix: str = None): def __init__(self, collection_prefix: str = None):
# Prefix Logik vereinheitlichen
env_prefix = os.getenv("COLLECTION_PREFIX", "mindnet") env_prefix = os.getenv("COLLECTION_PREFIX", "mindnet")
self.prefix = collection_prefix or env_prefix self.prefix = collection_prefix or env_prefix
@ -92,19 +74,14 @@ class IngestionService:
self.cfg.prefix = self.prefix self.cfg.prefix = self.prefix
self.client = get_client(self.cfg) self.client = get_client(self.cfg)
self.dim = self.cfg.dim self.dim = self.cfg.dim
# Registry laden
self.registry = load_type_registry() self.registry = load_type_registry()
# Embedding Service initialisieren (Async Client)
self.embedder = EmbeddingsClient() self.embedder = EmbeddingsClient()
# Init DB Checks (Fehler abfangen, falls DB nicht erreichbar)
try: try:
ensure_collections(self.client, self.prefix, self.dim) ensure_collections(self.client, self.prefix, self.dim)
ensure_payload_indexes(self.client, self.prefix) ensure_payload_indexes(self.client, self.prefix)
except Exception as e: except Exception as e:
logger.warning(f"DB initialization warning: {e}") logger.warning(f"DB init warning: {e}")
async def process_file( async def process_file(
self, self,
@ -119,7 +96,8 @@ class IngestionService:
hash_normalize: str = "canonical" hash_normalize: str = "canonical"
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Verarbeitet eine einzelne Datei (ASYNC Version). Verarbeitet eine einzelne Datei (ASYNC).
Inklusive Change Detection (Hash-Check) gegen Qdrant.
""" """
result = { result = {
"path": file_path, "path": file_path,
@ -128,7 +106,7 @@ class IngestionService:
"error": None "error": None
} }
# 1. Parse & Frontmatter # 1. Parse & Frontmatter Validation
try: try:
parsed = read_markdown(file_path) parsed = read_markdown(file_path)
if not parsed: if not parsed:
@ -169,7 +147,7 @@ class IngestionService:
logger.error(f"Payload build failed: {e}") logger.error(f"Payload build failed: {e}")
return {**result, "error": f"Payload build failed: {str(e)}"} return {**result, "error": f"Payload build failed: {str(e)}"}
# 4. Change Detection # 4. Change Detection (Das fehlende Stück!)
old_payload = None old_payload = None
if not force_replace: if not force_replace:
old_payload = self._fetch_note_payload(note_id) old_payload = self._fetch_note_payload(note_id)
@ -193,47 +171,38 @@ class IngestionService:
# 5. Processing (Chunking, Embedding, Edges) # 5. Processing (Chunking, Embedding, Edges)
try: try:
body_text = getattr(parsed, "body", "") or "" body_text = getattr(parsed, "body", "") or ""
chunks = assemble_chunks(fm["id"], body_text, fm["type"])
# --- Config Loading (Clean) ---
chunk_config = get_chunk_config(note_type)
# Hier greift die Logik aus types.yaml (smart=True/False)
chunks = await assemble_chunks(fm["id"], body_text, fm["type"], config=chunk_config)
chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text) chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text)
# --- EMBEDDING FIX (ASYNC) --- # Embedding
vecs = [] vecs = []
if chunk_pls: if chunk_pls:
texts = [c.get("window") or c.get("text") or "" for c in chunk_pls] texts = [c.get("window") or c.get("text") or "" for c in chunk_pls]
try: try:
# Async Aufruf des Embedders (via Batch oder Loop)
if hasattr(self.embedder, 'embed_documents'): if hasattr(self.embedder, 'embed_documents'):
vecs = await self.embedder.embed_documents(texts) vecs = await self.embedder.embed_documents(texts)
else: else:
# Fallback Loop falls Client kein Batch unterstützt
for t in texts: for t in texts:
v = await self.embedder.embed_query(t) v = await self.embedder.embed_query(t)
vecs.append(v) vecs.append(v)
# Validierung der Dimensionen
if vecs and len(vecs) > 0:
dim_got = len(vecs[0])
if dim_got != self.dim:
# Wirf keinen Fehler, aber logge Warnung. Qdrant Upsert wird failen wenn 0.
logger.warning(f"Vector dimension mismatch. Expected {self.dim}, got {dim_got}")
if dim_got == 0:
raise ValueError("Embedding returned empty vectors (Dim 0)")
except Exception as e: except Exception as e:
logger.error(f"Embedding generation failed: {e}") logger.error(f"Embedding failed: {e}")
raise RuntimeError(f"Embedding failed: {e}") raise RuntimeError(f"Embedding failed: {e}")
# Edges # Edges
note_refs = note_pl.get("references") or []
# Versuche flexible Signatur für Edges (V1 vs V2)
try: try:
edges = build_edges_for_note( edges = build_edges_for_note(
note_id, note_id,
chunk_pls, chunk_pls,
note_level_references=note_refs, note_level_references=note_pl.get("references", []),
include_note_scope_refs=note_scope_refs include_note_scope_refs=note_scope_refs
) )
except TypeError: except TypeError:
# Fallback für ältere Signatur
edges = build_edges_for_note(note_id, chunk_pls) edges = build_edges_for_note(note_id, chunk_pls)
except Exception as e: except Exception as e:
@ -251,7 +220,7 @@ class IngestionService:
if chunk_pls and vecs: if chunk_pls and vecs:
c_name, c_pts = points_for_chunks(self.prefix, chunk_pls, vecs) c_name, c_pts = points_for_chunks(self.prefix, chunk_pls, vecs)
upsert_batch(self.client, c_name, c_pts) upsert_batch(self.client, c_name, c_pts)
if edges: if edges:
e_name, e_pts = points_for_edges(self.prefix, edges) e_name, e_pts = points_for_edges(self.prefix, edges)
upsert_batch(self.client, e_name, e_pts) upsert_batch(self.client, e_name, e_pts)
@ -268,7 +237,7 @@ class IngestionService:
logger.error(f"Upsert failed: {e}", exc_info=True) logger.error(f"Upsert failed: {e}", exc_info=True)
return {**result, "error": f"DB Upsert failed: {e}"} return {**result, "error": f"DB Upsert failed: {e}"}
# --- Interne Qdrant Helper --- # --- Qdrant Helper (Restored) ---
def _fetch_note_payload(self, note_id: str) -> Optional[dict]: def _fetch_note_payload(self, note_id: str) -> Optional[dict]:
from qdrant_client.http import models as rest from qdrant_client.http import models as rest
@ -297,8 +266,7 @@ class IngestionService:
for suffix in ["chunks", "edges"]: for suffix in ["chunks", "edges"]:
try: try:
self.client.delete(collection_name=f"{self.prefix}_{suffix}", points_selector=selector) self.client.delete(collection_name=f"{self.prefix}_{suffix}", points_selector=selector)
except Exception: except Exception: pass
pass
async def create_from_text( async def create_from_text(
self, self,
@ -309,35 +277,29 @@ class IngestionService:
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
WP-11 Persistence API Entrypoint. WP-11 Persistence API Entrypoint.
Schreibt Text in Vault und indiziert ihn sofort.
""" """
# 1. Zielordner
target_dir = os.path.join(vault_root, folder) target_dir = os.path.join(vault_root, folder)
try: os.makedirs(target_dir, exist_ok=True)
os.makedirs(target_dir, exist_ok=True)
except Exception as e:
return {"status": "error", "error": f"Could not create folder {target_dir}: {e}"}
# 2. Dateiname file_path = os.path.join(target_dir, filename)
safe_filename = os.path.basename(filename)
if not safe_filename.endswith(".md"):
safe_filename += ".md"
file_path = os.path.join(target_dir, safe_filename)
# 3. Schreiben
try: try:
# Robust Write: Ensure Flush & Sync
with open(file_path, "w", encoding="utf-8") as f: with open(file_path, "w", encoding="utf-8") as f:
f.write(markdown_content) f.write(markdown_content)
f.flush()
os.fsync(f.fileno())
await asyncio.sleep(0.1)
logger.info(f"Written file to {file_path}") logger.info(f"Written file to {file_path}")
except Exception as e: except Exception as e:
return {"status": "error", "error": f"Disk write failed 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( return await self.process_file(
file_path=file_path, file_path=file_path,
vault_root=vault_root, vault_root=vault_root,
apply=True, apply=True,
force_replace=True, force_replace=True,
purge_before=True purge_before=True
) )

View File

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

View File

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

View File

@ -1,80 +1,142 @@
""" """
app/services/llm_service.py LLM Client (Ollama) app/services/llm_service.py LLM Client
Version: 0.2.1 (Fix: System Prompt Handling for Phi-3) Version: 2.8.0 (Configurable Concurrency Limit)
""" """
import httpx import httpx
import yaml import yaml
import logging import logging
import os import os
import asyncio
from pathlib import Path from pathlib import Path
from app.config import get_settings from typing import Optional, Dict, Any, Literal
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Settings:
OLLAMA_URL = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434")
LLM_TIMEOUT = float(os.getenv("MINDNET_LLM_TIMEOUT", 300.0))
LLM_MODEL = os.getenv("MINDNET_LLM_MODEL", "phi3:mini")
PROMPTS_PATH = os.getenv("MINDNET_PROMPTS_PATH", "./config/prompts.yaml")
# NEU: Konfigurierbares Limit für Hintergrund-Last
# Default auf 2 (konservativ), kann in .env erhöht werden.
BACKGROUND_LIMIT = int(os.getenv("MINDNET_LLM_BACKGROUND_LIMIT", "2"))
def get_settings():
return Settings()
class LLMService: class LLMService:
# GLOBALER SEMAPHOR (Lazy Initialization)
# Wir initialisieren ihn erst, wenn wir die Settings kennen.
_background_semaphore = None
def __init__(self): def __init__(self):
self.settings = get_settings() self.settings = get_settings()
self.prompts = self._load_prompts() self.prompts = self._load_prompts()
# Initialisiere Semaphore einmalig auf Klassen-Ebene basierend auf Config
if LLMService._background_semaphore is None:
limit = self.settings.BACKGROUND_LIMIT
logger.info(f"🚦 LLMService: Initializing Background Semaphore with limit: {limit}")
LLMService._background_semaphore = asyncio.Semaphore(limit)
self.timeout = httpx.Timeout(self.settings.LLM_TIMEOUT, connect=10.0)
self.client = httpx.AsyncClient( self.client = httpx.AsyncClient(
base_url=self.settings.OLLAMA_URL, base_url=self.settings.OLLAMA_URL,
timeout=self.settings.LLM_TIMEOUT timeout=self.timeout
) )
def _load_prompts(self) -> dict: def _load_prompts(self) -> dict:
path = Path(self.settings.PROMPTS_PATH) path = Path(self.settings.PROMPTS_PATH)
if not path.exists(): if not path.exists(): return {}
return {}
try: try:
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f)
return yaml.safe_load(f)
except Exception as e: except Exception as e:
logger.error(f"Failed to load prompts: {e}") logger.error(f"Failed to load prompts: {e}")
return {} return {}
async def generate_raw_response(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. Führt einen LLM Call aus.
Unterstützt nun explizite System-Prompts für sauberes Templating. 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, "model": self.settings.LLM_MODEL,
"prompt": prompt, "prompt": prompt,
"stream": False, "stream": False,
"options": { "options": {
# Temperature etwas höher für Empathie, niedriger für Code? "temperature": 0.1 if force_json else 0.7,
# Wir lassen es auf Standard, oder steuern es später via Config. "num_ctx": 8192
"temperature": 0.7,
"num_ctx": 2048
} }
} }
# WICHTIG: System-Prompt separat übergeben, damit Ollama formatiert if force_json:
payload["format"] = "json"
if system: if system:
payload["system"] = system payload["system"] = system
try: attempt = 0
response = await self.client.post("/api/generate", json=payload)
if response.status_code != 200: while True:
logger.error(f"Ollama Error ({response.status_code}): {response.text}") try:
return "Fehler bei der Generierung." response = await self.client.post("/api/generate", json=payload)
data = response.json() if response.status_code == 200:
return data.get("response", "").strip() data = response.json()
return data.get("response", "").strip()
except Exception as e: else:
logger.error(f"LLM Raw Gen Error: {e}") response.raise_for_status()
return "Interner LLM Fehler."
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: async def generate_rag_response(self, query: str, context_str: str) -> str:
"""Legacy Support""" """
Chat-Wrapper: Immer Realtime.
"""
system_prompt = self.prompts.get("system_prompt", "") system_prompt = self.prompts.get("system_prompt", "")
rag_template = self.prompts.get("rag_template", "{context_str}\n\n{query}") rag_template = self.prompts.get("rag_template", "{context_str}\n\n{query}")
final_prompt = rag_template.format(context_str=context_str, query=query) final_prompt = rag_template.format(context_str=context_str, query=query)
# Leite an die neue Methode weiter return await self.generate_raw_response(
return await self.generate_raw_response(final_prompt, system=system_prompt) final_prompt,
system=system_prompt,
max_retries=0,
force_json=False,
priority="realtime"
)
async def close(self): 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 # config/decision_engine.yaml
# Steuerung der Decision Engine (WP-06 + WP-07) # Steuerung der Decision Engine (Intent Recognition)
# Hybrid-Modus: Keywords (Fast) + LLM Router (Smart Fallback) # Version: 2.4.0 (Clean Architecture: Generic Intents only)
version: 1.3
version: 1.4
settings: settings:
llm_fallback_enabled: true llm_fallback_enabled: true
# Few-Shot Prompting für bessere SLM-Performance # Few-Shot Prompting für den LLM-Router (Slow Path)
# Erweitert um INTERVIEW Beispiele
llm_router_prompt: | llm_router_prompt: |
Du bist ein Klassifikator. Analysiere die Nachricht und wähle die passende Strategie. Du bist ein Klassifikator. Analysiere die Nachricht und wähle die passende Strategie.
Antworte NUR mit dem Namen der Strategie. Antworte NUR mit dem Namen der Strategie.
STRATEGIEN: STRATEGIEN:
- INTERVIEW: User will Wissen strukturieren, Notizen anlegen, Projekte starten ("Neu", "Festhalten"). - INTERVIEW: User will Wissen erfassen, Notizen anlegen oder Dinge festhalten.
- DECISION: Rat, Strategie, Vor/Nachteile, "Soll ich". - DECISION: Rat, Strategie, Vor/Nachteile, "Soll ich".
- EMPATHY: Gefühle, Frust, Freude, Probleme, "Alles ist sinnlos", "Ich bin traurig". - EMPATHY: Gefühle, Frust, Freude, Probleme.
- CODING: Code, Syntax, Programmierung, Python. - CODING: Code, Syntax, Programmierung.
- FACT: Wissen, Fakten, Definitionen. - FACT: Wissen, Fakten, Definitionen.
BEISPIELE: BEISPIELE:
User: "Wie funktioniert Qdrant?" -> FACT User: "Wie funktioniert Qdrant?" -> FACT
User: "Soll ich Qdrant nutzen?" -> DECISION User: "Soll ich Qdrant nutzen?" -> DECISION
User: "Ich möchte ein neues Projekt anlegen" -> INTERVIEW User: "Ich möchte etwas notieren" -> INTERVIEW
User: "Lass uns eine Entscheidung festhalten" -> INTERVIEW User: "Lass uns das festhalten" -> INTERVIEW
User: "Schreibe ein Python Script" -> CODING User: "Schreibe ein Python Script" -> CODING
User: "Alles ist grau und sinnlos" -> EMPATHY User: "Alles ist grau und sinnlos" -> EMPATHY
User: "Mir geht es heute gut" -> EMPATHY
NACHRICHT: "{query}" NACHRICHT: "{query}"
@ -51,11 +50,9 @@ strategies:
- "empfehlung" - "empfehlung"
- "strategie" - "strategie"
- "entscheidung" - "entscheidung"
- "wert"
- "prinzip"
- "vor- und nachteile"
- "abwägung" - "abwägung"
inject_types: ["value", "principle", "goal"] - "vergleich"
inject_types: ["value", "principle", "goal", "risk"]
prompt_template: "decision_template" prompt_template: "decision_template"
prepend_instruction: | prepend_instruction: |
!!! ENTSCHEIDUNGS-MODUS !!! !!! ENTSCHEIDUNGS-MODUS !!!
@ -72,6 +69,7 @@ strategies:
- "angst" - "angst"
- "nervt" - "nervt"
- "überfordert" - "überfordert"
- "müde"
inject_types: ["experience", "belief", "profile"] inject_types: ["experience", "belief", "profile"]
prompt_template: "empathy_template" prompt_template: "empathy_template"
prepend_instruction: null prepend_instruction: null
@ -88,56 +86,37 @@ strategies:
- "syntax" - "syntax"
- "json" - "json"
- "yaml" - "yaml"
- "bash"
inject_types: ["snippet", "reference", "source"] inject_types: ["snippet", "reference", "source"]
prompt_template: "technical_template" prompt_template: "technical_template"
prepend_instruction: null prepend_instruction: null
# 5. Interview / Datenerfassung (WP-07) # 5. Interview / Datenerfassung
# HINWEIS: Spezifische Typen (Projekt, Ziel etc.) werden automatisch
# über die types.yaml erkannt. Hier stehen nur generische Trigger.
INTERVIEW: INTERVIEW:
description: "Der User möchte strukturiertes Wissen erfassen (Projekt, Notiz, Idee)." description: "Der User möchte Wissen erfassen."
trigger_keywords: trigger_keywords:
- "neue notiz" - "neue notiz"
- "neues projekt" - "etwas notieren"
- "neue entscheidung"
- "neues ziel"
- "festhalten" - "festhalten"
- "entwurf erstellen" - "erstellen"
- "interview"
- "dokumentieren" - "dokumentieren"
- "anlegen"
- "interview"
- "erfassen" - "erfassen"
- "idee speichern" - "idee speichern"
inject_types: [] # Keine RAG-Suche, reiner Kontext-Dialog - "draft"
inject_types: []
prompt_template: "interview_template" prompt_template: "interview_template"
prepend_instruction: null prepend_instruction: null
# LATE BINDING SCHEMAS: # Schemas: Hier nur der Fallback.
# Definition der Pflichtfelder pro Typ (korrespondiert mit types.yaml) # Spezifische Schemas (Project, Experience) kommen jetzt aus types.yaml!
# Wenn ein Typ hier fehlt, wird 'default' genutzt.
schemas: schemas:
default: default:
fields: ["Titel", "Thema/Inhalt", "Tags"] fields:
hint: "Halte es einfach und übersichtlich." - "Titel"
- "Thema/Inhalt"
project: - "Tags"
fields: ["Titel", "Zielsetzung (Goal)", "Status (draft/active)", "Wichtige Stakeholder", "Nächste Schritte"] hint: "Halte es einfach und übersichtlich."
hint: "Achte darauf, Abhängigkeiten zu anderen Projekten mit [[rel:depends_on]] zu erfragen."
decision:
fields: ["Titel", "Kontext (Warum entscheiden wir?)", "Getroffene Entscheidung", "Betrachtete Alternativen", "Status (proposed/final)"]
hint: "Wichtig: Frage explizit nach den Gründen gegen die Alternativen."
goal:
fields: ["Titel", "Zeitrahmen (Deadline)", "Messkriterien (KPIs)", "Verbundene Werte"]
hint: "Ziele sollten SMART formuliert sein."
experience:
fields: ["Titel", "Situation (Kontext)", "Erkenntnis (Learning)", "Emotionale Keywords (für Empathie-Suche)"]
hint: "Fokussiere dich auf die persönliche Lektion."
value:
fields: ["Titel (Name des Werts)", "Definition (Was bedeutet das für uns?)", "Anti-Beispiel (Was ist es nicht?)"]
hint: "Werte dienen als Entscheidungsgrundlage."
principle:
fields: ["Titel", "Handlungsanweisung", "Begründung"]
hint: "Prinzipien sind härter als Werte."

View File

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

View File

@ -1,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: defaults:
retriever_weight: 1.0 retriever_weight: 1.0
chunk_profile: default chunking_profile: sliding_standard
edge_defaults: [] edge_defaults: []
# ==============================================================================
# 3. TYPE DEFINITIONS
# ==============================================================================
types: types:
# --- WISSENSBAUSTEINE ---
concept:
chunk_profile: medium
retriever_weight: 0.60
edge_defaults: ["references", "related_to"]
source: # --- KERNTYPEN (Hoch priorisiert & Smart) ---
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"]
experience: experience:
chunk_profile: medium chunking_profile: sliding_smart_edges
retriever_weight: 0.90 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: goal:
chunk_profile: medium chunking_profile: sliding_smart_edges
retriever_weight: 0.95 retriever_weight: 0.95
edge_defaults: ["depends_on", "related_to"] edge_defaults: ["depends_on", "related_to"]
schema: ["Zielzustand", "Zeitrahmen & KPIs", "Motivation"]
decision: # ADRs (Architecture Decision Records) risk:
chunk_profile: long # Entscheidungen brauchen oft viel Kontext (Begründung) chunking_profile: sliding_short
retriever_weight: 1.00 # MAX: Getroffene Entscheidungen sind Gesetz
edge_defaults: ["caused_by", "references"] # Entscheidungen haben Gründe
risk: # NEU: Risikomanagement
chunk_profile: short
retriever_weight: 0.85 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: # --- BASIS & WISSEN ---
chunk_profile: short
retriever_weight: 0.70
edge_defaults: ["related_to", "part_of"]
# --- OPERATIV --- concept:
project: chunking_profile: sliding_smart_edges
chunk_profile: long retriever_weight: 0.60
retriever_weight: 0.97 # Projekte sind der Kontext für alles edge_defaults: ["references", "related_to"]
edge_defaults: ["references", "depends_on"] schema:
- "Definition"
- "Kontext & Hintergrund"
- "Verwandte Konzepte"
task: task:
chunk_profile: short chunking_profile: sliding_short
retriever_weight: 0.80 retriever_weight: 0.80
edge_defaults: ["depends_on", "part_of"] edge_defaults: ["depends_on", "part_of"]
schema: ["Aufgabe", "Kontext", "Definition of Done"]
journal: journal:
chunk_profile: medium chunking_profile: sliding_standard
retriever_weight: 0.80 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 # mindnet v2.4 Knowledge Design Manual
**Datei:** `docs/mindnet_knowledge_design_manual_v2.4.md` **Datei:** `docs/mindnet_knowledge_design_manual_v2.6.md`
**Stand:** 2025-12-11 **Stand:** 2025-12-12
**Status:** **FINAL** (Integrierter Stand WP01WP11) **Status:** **FINAL** (Integrierter Stand WP01WP15)
**Quellen:** `knowledge_design.md`, `TYPE_REGISTRY_MANUAL.md`, `chunking_strategy.md`, `mindnet_functional_architecture.md`. **Quellen:** `knowledge_design.md`, `TYPE_REGISTRY_MANUAL.md`, `chunking_strategy.md`, `mindnet_functional_architecture.md`.
--- ---
@ -24,9 +24,9 @@ Dieses Handbuch ist die **primäre Arbeitsanweisung** für dich als Mindmaster (
### 1.1 Zielsetzung ### 1.1 Zielsetzung
Mindnet ist mehr als eine Dokumentablage. Es ist ein vernetztes System, das deine Persönlichkeit, Entscheidungen und Erfahrungen abbildet. Mindnet ist mehr als eine Dokumentablage. Es ist ein vernetztes System, das deine Persönlichkeit, Entscheidungen und Erfahrungen abbildet.
Seit Version 2.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. * **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. * **Web UI (WP10):** Du kannst direkt sehen, welche Quellen genutzt wurden.
* **Active Intelligence (WP11):** Das System hilft dir beim Schreiben und Vernetzen (Link-Vorschläge). * **Active Intelligence (WP11):** Das System hilft dir beim Schreiben und Vernetzen (Link-Vorschläge).
@ -69,7 +69,7 @@ Diese Felder sind technisch nicht zwingend, aber für bestimmte Typen sinnvoll:
**Die ID (Identifikator):** **Die ID (Identifikator):**
* Muss global eindeutig und **stabil** sein. * Muss global eindeutig und **stabil** sein.
* Darf sich nicht ändern, wenn die Datei umbenannt oder verschoben wird. * Darf sich nicht ändern, wenn die Datei umbenannt oder verschoben wird.
* **Empfehlung:** `YYYYMMDD-slug-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:** **Dateinamen & Pfade:**
* Pfade dienen der menschlichen Ordnung (Ordnerstruktur), sind für Mindnet aber sekundär. * Pfade dienen der menschlichen Ordnung (Ordnerstruktur), sind für Mindnet aber sekundär.
@ -80,7 +80,7 @@ Diese Felder sind technisch nicht zwingend, aber für bestimmte Typen sinnvoll:
## 3. Note-Typen & Typ-Registry ## 3. Note-Typen & Typ-Registry
Der `type` ist der wichtigste Hebel im Knowledge Design. Er steuert nicht nur das Gewicht bei der Suche, sondern seit WP-06 auch, **wann** eine Notiz aktiv in den Chat geholt wird ("Strategic Retrieval") und **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 ### 3.1 Übersicht der Kern-Typen
@ -95,8 +95,8 @@ Mindnet unterscheidet verschiedene Wissensarten. Wähle den Typ, der die **Rolle
| **`value`** | Ein persönlicher Wert oder ein Prinzip. | **DECISION** | Definition, Anti-Beispiel | Ziel für `based_on` | | **`value`** | Ein persönlicher Wert oder ein Prinzip. | **DECISION** | Definition, Anti-Beispiel | Ziel für `based_on` |
| **`principle`** | Handlungsleitlinie. | **DECISION** | Regel, Ausnahme | Quelle für `derived_from` | | **`principle`** | Handlungsleitlinie. | **DECISION** | Regel, Ausnahme | Quelle für `derived_from` |
| **`goal`** | Ein strategisches Ziel (kurz- oder langfristig). | **DECISION** | Zeitrahmen, KPIs, Werte | Ziel für `related_to` | | **`goal`** | Ein strategisches Ziel (kurz- oder langfristig). | **DECISION** | Zeitrahmen, KPIs, Werte | Ziel für `related_to` |
| **`risk`** | **NEU:** Ein identifiziertes Risiko oder eine Gefahr. | **DECISION** | Auswirkung, Wahrscheinlichkeit | Quelle für `blocks` | | **`risk`** | Ein identifiziertes Risiko oder eine Gefahr. | **DECISION** | Auswirkung, Wahrscheinlichkeit | Quelle für `blocks` |
| **`belief`** | **NEU:** Glaubenssatz / Überzeugung. | **EMPATHY** | Ursprung, Mantra | - | | **`belief`** | Glaubenssatz / Überzeugung. | **EMPATHY** | Ursprung, Mantra | - |
| **`person`** | Eine reale Person (Netzwerk, Autor). | **FACT** | Rolle, Kontext | - | | **`person`** | Eine reale Person (Netzwerk, Autor). | **FACT** | Rolle, Kontext | - |
| **`journal`** | Zeitbezogener Log-Eintrag, Daily Note. | **FACT** | Datum, Tags | - | | **`journal`** | Zeitbezogener Log-Eintrag, Daily Note. | **FACT** | Datum, Tags | - |
| **`source`** | Externe Quelle (Buch, PDF, Artikel). | **FACT** | Autor, URL | - | | **`source`** | Externe Quelle (Buch, PDF, Artikel). | **FACT** | Autor, URL | - |
@ -107,13 +107,11 @@ Der `type` steuert im Hintergrund drei technische Mechanismen:
1. **`retriever_weight` (Wichtigkeit):** 1. **`retriever_weight` (Wichtigkeit):**
* Ein `concept` (0.6) wiegt weniger als ein `project` (0.97) oder eine `decision` (1.0). * Ein `concept` (0.6) wiegt weniger als ein `project` (0.97) oder eine `decision` (1.0).
* **Warum?** Bei einer Suche nach "Datenbank" soll Mindnet bevorzugt deine *Entscheidung* ("Warum wir X nutzen") anzeigen.
2. **`chunk_profile` (Textzerlegung):** 2. **`chunk_profile` (Textzerlegung):**
* `journal` (short): Wird fein zerlegt. * `journal` (short): Wird fein zerlegt.
* `project` (long): Längere Kontext-Fenster. * `experience` (sliding_smart_edges): Wird intelligent analysiert.
3. **`edge_defaults` (Automatische Vernetzung):** 3. **`enable_smart_edge_allocation` (WP15):**
* Mindnet ergänzt automatisch Kanten. * 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.
* Beispiel: Ein Link in einem `project` wird automatisch als `depends_on` (Abhängigkeit) interpretiert.
--- ---
@ -291,4 +289,4 @@ Wir vermeiden es, Logik in den Markdown-Dateien hart zu kodieren.
### 7.2 Was bedeutet das für dich? ### 7.2 Was bedeutet das für dich?
* Du kannst dich auf den Inhalt konzentrieren. * Du kannst dich auf den Inhalt konzentrieren.
* Wenn wir in Zukunft (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 # Mindnet v2.4 Overview & Einstieg
**Datei:** `docs/mindnet_overview_v2.4.md` **Datei:** `docs/mindnet_overview_v2.6.md`
**Stand:** 2025-12-11 **Stand:** 2025-12-12
**Status:** **FINAL** (Inkl. Async Intelligence & Editor) **Status:** **FINAL** (Inkl. Smart Edges, Traffic Control & Healing UI)
**Version:** 2.4.0 **Version:** 2.6.0
--- ---
@ -28,14 +28,14 @@ Mindnet arbeitet auf drei Schichten, die aufeinander aufbauen:
### Ebene 1: Content (Das Gedächtnis) ### Ebene 1: Content (Das Gedächtnis)
* **Quelle:** Dein lokaler Obsidian-Vault (Markdown). * **Quelle:** Dein lokaler Obsidian-Vault (Markdown).
* **Funktion:** Speicherung von Fakten, Projekten und Logs. * **Funktion:** Speicherung von Fakten, Projekten und Logs.
* **Technik:** Async Import-Pipeline, Chunking, Vektor-Datenbank (Qdrant). * **Technik:** Async Import-Pipeline, Smart Chunking (LLM-gestützte Kantenzuweisung), Vektor-Datenbank (Qdrant).
* **Status:** 🟢 Live (WP01WP03, WP11). * **Status:** 🟢 Live (WP01WP03, WP11, WP15).
### Ebene 2: Semantik (Das Verstehen) ### Ebene 2: Semantik (Das Verstehen)
* **Funktion:** Verknüpfung von isolierten Notizen zu einem Netzwerk. * **Funktion:** Verknüpfung von isolierten Notizen zu einem Netzwerk.
* **Logik:** "Projekt A *hängt ab von* Entscheidung B". * **Logik:** "Projekt A *hängt ab von* Entscheidung B".
* **Technik:** Hybrider Retriever (Graph + Nomic Embeddings), Explanation Engine. * **Technik:** Hybrider Retriever (Graph + Nomic Embeddings), Explanation Engine.
* **Status:** 🟢 Live (WP04, WP11). * **Status:** 🟢 Live (WP04, WP11, WP15).
### Ebene 3: Identität & Interaktion (Die Persönlichkeit) ### Ebene 3: Identität & Interaktion (Die Persönlichkeit)
* **Funktion:** Interaktion, Bewertung und Co-Creation. * **Funktion:** Interaktion, Bewertung und Co-Creation.
@ -43,11 +43,11 @@ Mindnet arbeitet auf drei Schichten, die aufeinander aufbauen:
* "Ich empfehle Lösung X, weil sie unserem Wert 'Datensparsamkeit' entspricht." * "Ich empfehle Lösung X, weil sie unserem Wert 'Datensparsamkeit' entspricht."
* "Ich sehe, du willst ein Projekt starten. Lass uns die Eckdaten erfassen." * "Ich sehe, du willst ein Projekt starten. Lass uns die Eckdaten erfassen."
* **Technik:** * **Technik:**
* **Intent Router:** Erkennt Absichten (Fakt vs. Gefühl vs. Entscheidung vs. Interview). * **Hybrid Router v5:** Erkennt Absichten (Frage vs. Befehl) und unterscheidet Objekte (`types.yaml`) von Handlungen (`decision_engine.yaml`).
* **Strategic Retrieval:** Lädt gezielt Werte oder Erfahrungen nach. * **Traffic Control:** Priorisiert Chat-Anfragen ("Realtime") vor Hintergrund-Jobs ("Background").
* **One-Shot Extraction:** Generiert Entwürfe für neue Notizen. * **One-Shot Extraction:** Generiert Entwürfe für neue Notizen.
* **Active Intelligence:** Schlägt Links während des Schreibens vor. * **Active Intelligence:** Schlägt Links während des Schreibens vor.
* **Status:** 🟢 Live (WP05WP07, WP10). * **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. 1. **Input:** Du schreibst Notizen in Obsidian **ODER** lässt sie von Mindnet im Chat entwerfen.
2. **Intelligence (Live):** Während du schreibst, analysiert Mindnet den Text und schlägt Verknüpfungen vor (Sliding Window Analyse). 2. **Intelligence (Live):** Während du schreibst, analysiert Mindnet den Text und schlägt Verknüpfungen vor (Sliding Window Analyse).
3. **Ingest:** Ein asynchrones Python-Skript importiert, zerlegt (Chunking) und vernetzt (Edges) die Daten in Qdrant. 3. **Ingest:** Ein asynchrones Python-Skript importiert und zerlegt die Daten.
4. **Intent Recognition:** Der Router analysiert deine Frage: Willst du Fakten, Code, Empathie oder etwas dokumentieren? * **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:** 5. **Retrieval / Action:**
* Bei Fragen: Das System sucht Inhalte passend zum Intent. * Bei Fragen: Das System sucht Inhalte passend zum Intent.
* Bei Interviews: Das System wählt das passende Schema (z.B. Projekt-Vorlage). * Bei Interviews: Das System wählt das passende Schema (z.B. Projekt-Vorlage).
@ -69,7 +70,8 @@ Der Datenfluss in Mindnet ist zyklisch ("Data Flywheel"):
* **Backend:** Python 3.10+, FastAPI (Async). * **Backend:** Python 3.10+, FastAPI (Async).
* **Datenbank:** Qdrant (Vektor & Graph, 768 Dim). * **Datenbank:** Qdrant (Vektor & Graph, 768 Dim).
* **KI:** Ollama (Phi-3 Mini für Chat, Nomic für Embeddings) 100% lokal. * **KI:** Ollama (Phi-3 Mini für Chat, Nomic für Embeddings) 100% lokal.
* **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 | | Wenn du... | ...lies dieses Dokument |
| :--- | :--- | | :--- | :--- |
| **...wissen willst, wie man Notizen schreibt.** | `mindnet_knowledge_design_manual_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.4.md` | | **...das System installieren oder betreiben musst.** | `mindnet_admin_guide_v2.6.md` |
| **...am Python-Code entwickeln willst.** | `mindnet_developer_guide_v2.4.md` | | **...am Python-Code entwickeln willst.** | `mindnet_developer_guide_v2.6.md` |
| **...die Pipeline (Import -> RAG) verstehen willst.** | `mindnet_pipeline_playbook_v2.4.md` | | **...die Pipeline (Import -> RAG) verstehen willst.** | `mindnet_pipeline_playbook_v2.6.md` |
| **...die genaue JSON-Struktur oder APIs suchst.** | `mindnet_technical_architecture.md` | | **...die genaue JSON-Struktur oder APIs suchst.** | `mindnet_technical_architecture_v2.6.md` |
| **...verstehen willst, was fachlich passiert.** | `mindnet_functional_architecture.md` | | **...verstehen willst, was fachlich passiert.** | `mindnet_functional_architecture_v2.6.md` |
| **...den aktuellen Projektstatus suchst.** | `mindnet_appendices_v2.4.md` | | **...den aktuellen Projektstatus suchst.** | `mindnet_appendices_v2.6.md` |
--- ---
@ -99,5 +101,5 @@ Wo findest du was?
## 6. Aktueller Fokus ## 6. Aktueller Fokus
Wir haben den **Interview-Assistenten (WP07)** und die **Backend Intelligence (WP11)** erfolgreich integriert. Wir haben die **Smart Edge Allocation (WP15)** und die **System-Stabilisierung (Traffic Control)** erfolgreich abgeschlossen.
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. 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 # Mindnet v2.4 Admin Guide
**Datei:** `docs/mindnet_admin_guide_v2.4.md` **Datei:** `docs/mindnet_admin_guide_v2.6.md`
**Stand:** 2025-12-11 **Stand:** 2025-12-12
**Status:** **FINAL** (Inkl. Async Architecture & Nomic Model) **Status:** **FINAL** (Inkl. Traffic Control & Smart Edge Config)
**Quellen:** `Handbuch.md`, `mindnet_developer_guide_v2.4.md`. **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). > Dieses Handbuch richtet sich an **Administratoren**. Es beschreibt Installation, Konfiguration, Backup-Strategien, Monitoring und den sicheren Betrieb der Mindnet-Instanz (API + UI + DB).
@ -55,7 +55,7 @@ Wir nutzen Qdrant als Vektor-Datenbank. Persistenz ist wichtig.
### 2.4 Ollama Setup (LLM & Embeddings) ### 2.4 Ollama Setup (LLM & Embeddings)
Mindnet benötigt einen lokalen LLM-Server für Chat UND Embeddings. Mindnet benötigt einen lokalen LLM-Server für Chat UND Embeddings.
**WICHTIG (Update v2.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) # 1. Installieren (Linux Script)
curl -fsSL https://ollama.com/install.sh | sh curl -fsSL https://ollama.com/install.sh | sh
@ -68,7 +68,7 @@ Mindnet benötigt einen lokalen LLM-Server für Chat UND Embeddings.
curl http://localhost:11434/api/generate -d '{"model": "phi3:mini", "prompt":"Hi"}' curl http://localhost:11434/api/generate -d '{"model": "phi3:mini", "prompt":"Hi"}'
### 2.5 Konfiguration (ENV) ### 2.5 Konfiguration (ENV)
Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM` und `MINDNET_EMBEDDING_MODEL`. Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM` und das neue `MINDNET_LLM_BACKGROUND_LIMIT`.
# Server Config # Server Config
UVICORN_HOST=0.0.0.0 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) # AI Modelle (Ollama)
MINDNET_OLLAMA_URL="http://127.0.0.1:11434" MINDNET_OLLAMA_URL="http://127.0.0.1:11434"
MINDNET_LLM_MODEL="phi3:mini" MINDNET_LLM_MODEL="phi3:mini"
MINDNET_EMBEDDING_MODEL="nomic-embed-text" # 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_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 # Configs
MINDNET_PROMPTS_PATH="./config/prompts.yaml" 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. Betrieb im Alltag
### 3.1 Regelmäßige Importe ### 3.1 Regelmäßige Importe
Der Vault-Zustand sollte regelmäßig (z.B. stündlich per Cronjob) nach Qdrant synchronisiert werden. Das Skript nutzt nun **AsyncIO** 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):** **Cronjob-Beispiel (stündlich):**
0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1 0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1
**Hinweis zu Smart Edges:**
Der Import dauert nun länger (ca. 2-3 Min pro Datei), da das LLM intensiv genutzt wird. Dank `MINDNET_LLM_BACKGROUND_LIMIT=2` bleibt der Server dabei aber für den Chat responsive.
### 3.2 Health-Checks ### 3.2 Health-Checks
Prüfe regelmäßig, ob alle Komponenten laufen. Prüfe regelmäßig, ob alle Komponenten laufen.
@ -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" ### "Vector dimension error: expected dim: 768, got 384"
* **Ursache:** Du versuchst, in eine alte Qdrant-Collection (mit 384 Dim aus v2.2) neue Embeddings (mit 768 Dim von Nomic) zu schreiben. * **Ursache:** Du versuchst, in eine alte Qdrant-Collection (mit 384 Dim aus v2.2) neue Embeddings (mit 768 Dim von Nomic) zu schreiben.
@ -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). 1. `python -m scripts.reset_qdrant --mode wipe --prefix mindnet --yes` (Löscht DB).
2. `python -m scripts.import_markdown ...` (Baut neu auf). 2. `python -m scripts.import_markdown ...` (Baut neu auf).
### "500 Internal Server Error" beim Speichern ### "500 Internal Server Error" beim Speichern/Chatten
* **Ursache:** Oft Timeout bei Ollama, wenn `nomic-embed-text` noch nicht im RAM geladen ist ("Cold Start"). * **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:** * **Lösung:**
1. Sicherstellen, dass Modell existiert: `ollama list`. 1. `MINDNET_LLM_TIMEOUT` in `.env` auf `300.0` setzen.
2. API neustarten (re-initialisiert Async Clients). 2. `MINDNET_LLM_BACKGROUND_LIMIT` auf `1` reduzieren (falls Hardware schwach).
### "NameError: name 'os' is not defined" ### Import ist sehr langsam
* **Ursache:** Fehlender Import in Skripten nach Updates. * **Ursache:** Smart Edges sind aktiv (`types.yaml`) und analysieren jeden Chunk.
* **Lösung:** `git pull` (Fix wurde in v2.3.10 deployed). * **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) # 1. DB komplett leeren (Wipe)
python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes
# 2. Alles neu importieren # 2. Alles neu importieren (Force re-calculates Hashes)
python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
--- ---

View File

@ -1,7 +1,7 @@
# Mindnet v2.4 Appendices & Referenzen # Mindnet v2.4 Appendices & Referenzen
**Datei:** `docs/mindnet_appendices_v2.4.md` **Datei:** `docs/mindnet_appendices_v2.6.md`
**Stand:** 2025-12-11 **Stand:** 2025-12-12
**Status:** **FINAL** (Integrierter Stand WP01WP11) **Status:** **FINAL** (Integrierter Stand WP01WP15)
**Quellen:** `TYPE_REGISTRY_MANUAL.md`, `chunking_strategy.md`, `mindnet_technical_architecture.md`, `Handbuch.md`. **Quellen:** `TYPE_REGISTRY_MANUAL.md`, `chunking_strategy.md`, `mindnet_technical_architecture.md`, `Handbuch.md`.
> Dieses Dokument bündelt Tabellen, Schemata und technische Referenzen, die in den Prozess-Dokumenten (Playbook, Guides) den Lesefluss stören würden. > Dieses Dokument bündelt Tabellen, Schemata und technische Referenzen, die in den Prozess-Dokumenten (Playbook, Guides) den Lesefluss stören würden.
@ -10,22 +10,27 @@
## Anhang A: Typ-Registry Referenz (Default-Werte) ## Anhang A: Typ-Registry Referenz (Default-Werte)
Diese Tabelle zeigt die Standard-Konfiguration der `types.yaml` (Stand v2.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. | | **concept** | `sliding_smart_edges` | 0.60 | **Ja** | Abstrakte Begriffe, Theorien. |
| **project** | `long` | 0.97 | `references`, `depends_on` | Aktive Vorhaben. Hohe Priorität. | | **project** | `sliding_smart_edges` | 0.97 | **Ja** | Aktive Vorhaben. Hohe Priorität. |
| **decision** | `long` | 1.00 | `caused_by`, `references` | Entscheidungen (ADRs). Höchste Prio. | | **decision** | `structured_smart_edges` | 1.00 | **Ja** | Entscheidungen (ADRs). Höchste Prio. |
| **experience** | `medium` | 0.90 | `derived_from`, `inspired_by` | Persönliche Learnings. | | **experience** | `sliding_smart_edges` | 0.90 | **Ja** | Persönliche Learnings. Intensiv analysiert. |
| **journal** | `short` | 0.80 | `related_to` | Zeitgebundene Logs. Fein granular. | | **journal** | `sliding_standard` | 0.80 | Nein | Zeitgebundene Logs. Performance-optimiert. |
| **person** | `short` | 0.50 | `related_to` | Personen-Profile. | | **person** | `sliding_standard` | 0.50 | Nein | Personen-Profile. |
| **source** | `long` | 0.50 | *(keine)* | Externe Quellen (Bücher, PDFs). | | **source** | `sliding_standard` | 0.50 | Nein | Externe Quellen (Bücher, PDFs). |
| **event** | `short` | 0.60 | `related_to` | Meetings, Konferenzen. | | **event** | `sliding_standard` | 0.60 | Nein | Meetings, Konferenzen. |
| **value** | `medium` | 1.00 | `related_to` | Persönliche Werte/Prinzipien. | | **value** | `structured_smart_edges` | 1.00 | **Ja** | Persönliche Werte/Prinzipien. |
| **goal** | `medium` | 0.95 | `depends_on` | Strategische Ziele. | | **principle** | `structured_smart_edges` | 1.00 | **Ja** | Handlungsleitlinien. |
| **belief** | `medium` | 0.90 | `related_to` | Glaubenssätze. | | **profile** | `structured_smart_edges` | 0.80 | **Ja** | Eigene Identitäts-Beschreibungen. |
| **default** | `medium` | 1.00 | `references` | Fallback, wenn Typ unbekannt. | | **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". | | `similar_to` | Inline | Ja | Inhaltliche Ähnlichkeit. "Ist wie X". |
| `caused_by` | Inline | Nein | Kausalität. "X ist der Grund für Y". | | `caused_by` | Inline | Nein | Kausalität. "X ist der Grund für Y". |
| `solves` | Inline | Nein | Lösung. "Tool X löst Problem Y". | | `solves` | Inline | Nein | Lösung. "Tool X löst Problem Y". |
| `blocks` | Inline | Nein | **NEU:** Blockade. "Risiko X blockiert Projekt Y". |
| `derived_from` | Matrix / Default | Nein | Herkunft. "Erkenntnis stammt aus Prinzip X". | | `derived_from` | Matrix / Default | Nein | Herkunft. "Erkenntnis stammt aus Prinzip X". |
| `based_on` | Matrix | Nein | Fundament. "Erfahrung basiert auf Wert Y". | | `based_on` | Matrix | Nein | Fundament. "Erfahrung basiert auf Wert Y". |
| `uses` | Matrix | Nein | Nutzung. "Projekt nutzt Konzept Z". | | `uses` | Matrix | Nein | Nutzung. "Projekt nutzt Konzept Z". |
@ -60,7 +66,7 @@ Diese sind die Felder, die effektiv in Qdrant gespeichert werden.
"title": "string (text)", // Titel aus Frontmatter "title": "string (text)", // Titel aus Frontmatter
"type": "string (keyword)", // Typ (z.B. 'project') "type": "string (keyword)", // Typ (z.B. 'project')
"retriever_weight": "float", // Numerische Wichtigkeit (0.0-1.0) "retriever_weight": "float", // Numerische Wichtigkeit (0.0-1.0)
"chunk_profile": "string", // Genutztes Profil (z.B. 'long') "chunk_profile": "string", // Genutztes Profil (z.B. 'sliding_smart_edges')
"edge_defaults": ["string"], // Liste der aktiven Default-Kanten "edge_defaults": ["string"], // Liste der aktiven Default-Kanten
"tags": ["string"], // Liste von Tags "tags": ["string"], // Liste von Tags
"created": "string (iso-date)", // Erstellungsdatum "created": "string (iso-date)", // Erstellungsdatum
@ -78,6 +84,7 @@ Diese sind die Felder, die effektiv in Qdrant gespeichert werden.
"window": "string (text)", // Text + Overlap (für Embedding) "window": "string (text)", // Text + Overlap (für Embedding)
"ord": "integer", // Laufende Nummer (1..N) "ord": "integer", // Laufende Nummer (1..N)
"retriever_weight": "float", // Kopie aus Note (für Query-Speed) "retriever_weight": "float", // Kopie aus Note (für Query-Speed)
"chunk_profile": "string", // Vererbt von Note
"neighbors_prev": ["string"], // ID des Vorgängers "neighbors_prev": ["string"], // ID des Vorgängers
"neighbors_next": ["string"] // ID des Nachfolgers "neighbors_next": ["string"] // ID des Nachfolgers
} }
@ -92,7 +99,8 @@ Diese sind die Felder, die effektiv in Qdrant gespeichert werden.
"scope": "string (keyword)", // Immer 'chunk' "scope": "string (keyword)", // Immer 'chunk'
"rule_id": "string (keyword)", // Traceability: 'inline:rel', 'explicit:wikilink' "rule_id": "string (keyword)", // Traceability: 'inline:rel', 'explicit:wikilink'
"confidence": "float", // 0.0 - 1.0 "confidence": "float", // 0.0 - 1.0
"note_id": "string (keyword)" // Owner Note ID "note_id": "string (keyword)", // Owner Note ID
"provenance": "keyword" // 'explicit', 'rule', 'smart' (NEU)
} }
--- ---
@ -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_EMBEDDING_MODEL` | `nomic-embed-text` | **NEU:** Name des Vektor-Modells. |
| `MINDNET_OLLAMA_URL` | `http://127.0.0.1:11434`| URL zum LLM-Server (Neu in v2.2). | | `MINDNET_OLLAMA_URL` | `http://127.0.0.1:11434`| URL zum LLM-Server (Neu in v2.2). |
| `MINDNET_LLM_TIMEOUT` | `300.0` | Timeout für Ollama (Erhöht für CPU-Inference). | | `MINDNET_LLM_TIMEOUT` | `300.0` | Timeout für Ollama (Erhöht für CPU-Inference). |
| `MINDNET_API_TIMEOUT` | `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_VAULT_ROOT` | `./vault` | **NEU:** Pfad für Write-Back Operationen. |
| `MINDNET_HASH_COMPARE` | `Body` | Vergleichsmodus für Import (`Body`, `Frontmatter`, `Full`). | | `MINDNET_HASH_COMPARE` | `Body` | Vergleichsmodus für Import (`Body`, `Frontmatter`, `Full`). |
| `MINDNET_HASH_SOURCE` | `parsed` | Quelle für Hash (`parsed`, `raw`, `file`). | | `MINDNET_HASH_SOURCE` | `parsed` | Quelle für Hash (`parsed`, `raw`, `file`). |
@ -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). * **Decision Engine:** Komponente, die den Intent prüft und Strategien wählt (WP06).
* **Draft Editor:** Web-Komponente zur Bearbeitung generierter Notizen (WP10a). * **Draft Editor:** Web-Komponente zur Bearbeitung generierter Notizen (WP10a).
* **Explanation Layer:** Komponente, die Scores und Graphen als Begründung liefert. * **Explanation Layer:** Komponente, die Scores und Graphen als Begründung liefert.
* **Healing Parser:** UI-Funktion, die defektes YAML von LLMs repariert (v2.5).
* **Hybrid Router:** Kombination aus Keyword-Matching und LLM-Klassifizierung für Intents. * **Hybrid Router:** Kombination aus Keyword-Matching und LLM-Klassifizierung für Intents.
* **Matrix Logic:** Regelwerk, das Kanten-Typen basierend auf Quell- und Ziel-Typ bestimmt. * **Matrix Logic:** Regelwerk, das Kanten-Typen basierend auf Quell- und Ziel-Typ bestimmt.
* **Nomic:** Das neue, hochpräzise Embedding-Modell (768 Dim). * **Nomic:** Das neue, hochpräzise Embedding-Modell (768 Dim).
* **One-Shot Extractor:** LLM-Strategie zur sofortigen Generierung von Drafts ohne Rückfragen (WP07). * **One-Shot Extractor:** LLM-Strategie zur sofortigen Generierung von Drafts ohne Rückfragen (WP07).
* **RAG (Retrieval Augmented Generation):** Kombination aus Suche und Text-Generierung. * **RAG (Retrieval Augmented Generation):** Kombination aus Suche und Text-Generierung.
* **Resurrection Pattern:** UI-Technik, um Eingaben bei Tab-Wechseln zu erhalten. * **Resurrection Pattern:** UI-Technik, um Eingaben bei Tab-Wechseln zu erhalten.
* **Smart Edge Allocation:** LLM-basierte Prüfung, ob ein Link zu einem Chunk passt (WP15).
* **Traffic Control:** Priorisierung von Chat-Anfragen ("Realtime") vor Import-Last ("Background") im LLM-Service (v2.6).
--- ---
## Anhang F: Workpackage Status (v2.4.0) ## Anhang F: Workpackage Status (v2.6.0)
Aktueller Implementierungsstand der Module. Aktueller Implementierungsstand der Module.
@ -151,9 +163,12 @@ Aktueller Implementierungsstand der Module.
| **WP04b**| Explanation Layer | 🟢 Live | API liefert Reasons & Breakdown. | | **WP04b**| Explanation Layer | 🟢 Live | API liefert Reasons & Breakdown. |
| **WP04c**| Feedback Loop | 🟢 Live | Logging (JSONL) & Traceability aktiv. | | **WP04c**| Feedback Loop | 🟢 Live | Logging (JSONL) & Traceability aktiv. |
| **WP05** | Persönlichkeit / Chat | 🟢 Live | RAG-Integration mit Context Enrichment. | | **WP05** | Persönlichkeit / Chat | 🟢 Live | RAG-Integration mit Context Enrichment. |
| **WP06** | Decision Engine | 🟢 Live | Intent-Router & Strategic Retrieval. | | **WP06** | Decision Engine | 🟢 Live | Hybrid Router v5, Strategic Retrieval. |
| **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** | | **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** |
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. | | **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). | | **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** | | **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI & Healing Parser.** |
| **WP11** | Backend Intelligence | 🟢 Live | **Async Core, Nomic, Matrix.** | | **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 # Mindnet v2.4 Entwickler-Workflow
**Datei:** `docs/DEV_WORKFLOW.md` **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). Dieses Handbuch beschreibt den Entwicklungszyklus zwischen **Windows PC** (IDE), **Raspberry Pi** (Gitea) und **Beelink** (Runtime/Server).
@ -35,14 +35,14 @@ Hier erstellst du die neue Funktion in einer sicheren Umgebung.
2. **Branch erstellen:** 2. **Branch erstellen:**
* Klicke wieder unten links auf `main`. * Klicke wieder unten links auf `main`.
* Wähle `+ Create new branch...`. * Wähle `+ Create new branch...`.
* Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp11-async-fix`). * Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp15-traffic-control`).
* Drücke **Enter**. * Drücke **Enter**.
3. **Sicherheits-Check:** 3. **Sicherheits-Check:**
* Steht unten links jetzt dein Feature-Branch? **Nur dann darfst du Code ändern!** * Steht unten links jetzt dein Feature-Branch? **Nur dann darfst du Code ändern!**
4. **Coden:** 4. **Coden:**
* Nimm deine Änderungen vor (z.B. neue Schemas in `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:** 5. **Sichern & Hochladen:**
* **Source Control** Icon (Gabel-Symbol) -> Nachricht eingeben -> **Commit**. * **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 ```bash
git fetch git fetch
# Tipp: 'git branch -r' zeigt alle verfügbaren Branches an # Tipp: 'git branch -r' zeigt alle verfügbaren Branches an
git checkout feature/wp11-async-fix git checkout feature/wp15-traffic-control
git pull 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 ```bash
source .venv/bin/activate source .venv/bin/activate
pip install -r requirements.txt # HTTPX usw. pip install -r requirements.txt
# Sicherstellen, dass das neue Embedding-Modell da ist:
ollama pull nomic-embed-text 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 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!). * **Test A: Last-Test (Traffic Control):**
* **CLI:** Führe Testskripte in einem **zweiten Terminal** aus: 1. Starte einen Import im Terminal: `python3 -m scripts.import_markdown ...`
2. Öffne **gleichzeitig** `http://<IP>:8502` im Browser.
**Test A: Intelligence / Aliases (Neu in WP11)** 3. Stelle eine Chat-Frage ("Was ist Mindnet?").
```bash 4. **Erwartung:** Der Chat antwortet sofort (Realtime Lane), während der Import im Hintergrund weiterläuft (Background Lane).
python debug_analysis.py
# Erwartung: "✅ ALIAS GEFUNDEN"
```
**Test B: API Check** * **Test B: API Check**
```bash ```bash
curl -X POST "http://localhost:8002/ingest/analyze" -d '{"text": "mindnet", "type": "journal"}' curl -X POST "http://localhost:8002/ingest/analyze" -d '{"text": "mindnet", "type": "journal"}'
``` ```
@ -147,6 +156,9 @@ Jetzt bringen wir die Änderung in das Live-System (Port 8001 / 8501).
pip install -r requirements.txt pip install -r requirements.txt
ollama pull nomic-embed-text ollama pull nomic-embed-text
# .env prüfen (Traffic Control)
# Ggf. MINDNET_LLM_BACKGROUND_LIMIT=2 hinzufügen
# Falls sich die Vektor-Dimension geändert hat (v2.4 Upgrade): # Falls sich die Vektor-Dimension geändert hat (v2.4 Upgrade):
# python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes # python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes
# python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force # python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
@ -168,7 +180,7 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch.
cd ~/mindnet_dev cd ~/mindnet_dev
git checkout main git checkout main
git pull git pull
git branch -d feature/wp11-async-fix git branch -d feature/wp15-traffic-control
``` ```
3. **VS Code:** 3. **VS Code:**
* Auf `main` wechseln. * Auf `main` wechseln.
@ -190,15 +202,15 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch.
## 4. Troubleshooting ## 4. Troubleshooting
**"Vector dimension error: expected 768, got 384"** **"Read timed out" im Frontend**
* **Ursache:** Du hast `nomic-embed-text` (768) aktiviert, aber die DB ist noch alt (384). * **Ursache:** Backend braucht für Smart Edges länger als 60s.
* **Lösung:** `scripts.reset_qdrant` ausführen und neu importieren. * **Lösung:** `MINDNET_API_TIMEOUT=300.0` in `.env` setzen und Services neustarten.
**"Read timed out (300s)" / 500 Error beim Interview** **Import ist extrem langsam**
* **Ursache:** Das LLM (Ollama) braucht für den One-Shot Draft länger als das Timeout erlaubt. * **Ursache:** Smart Edges analysieren jeden Chunk mit LLM.
* **Lösung:** * **Lösung:**
1. Erhöhe in `.env` den Wert: `MINDNET_LLM_TIMEOUT=300.0`. * Akzeptieren (Qualität vor Speed).
2. Starte die Server neu. * Oder temporär in `config/types.yaml`: `enable_smart_edge_allocation: false`.
**"UnicodeDecodeError in .env"** **"UnicodeDecodeError in .env"**
* **Ursache:** Umlaute oder Sonderzeichen in der `.env` Datei. * **Ursache:** Umlaute oder Sonderzeichen in der `.env` Datei.

View File

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

View File

@ -1,9 +1,9 @@
# Mindnet v2.4 Fachliche Architektur # Mindnet v2.4 Fachliche Architektur
**Datei:** `docs/mindnet_functional_architecture_v2.4.md` **Datei:** `docs/mindnet_functional_architecture_v2.6.md`
**Stand:** 2025-12-11 **Stand:** 2025-12-12
**Status:** **FINAL** (Integrierter Stand WP01WP11: Async Intelligence) **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> <details>
@ -19,7 +19,8 @@
- [2.1 Struktur-Kanten (Das Skelett)](#21-struktur-kanten-das-skelett) - [2.1 Struktur-Kanten (Das Skelett)](#21-struktur-kanten-das-skelett)
- [2.2 Inhalts-Kanten (explizit)](#22-inhalts-kanten-explizit) - [2.2 Inhalts-Kanten (explizit)](#22-inhalts-kanten-explizit)
- [2.3 Typ-basierte Default-Kanten (Regelbasiert)](#23-typ-basierte-default-kanten-regelbasiert) - [2.3 Typ-basierte Default-Kanten (Regelbasiert)](#23-typ-basierte-default-kanten-regelbasiert)
- [2.4 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) - [3) Edge-Payload Felder \& Semantik](#3-edge-payload--felder--semantik)
- [4) Typ-Registry (`config/types.yaml`)](#4-typ-registry-configtypesyaml) - [4) Typ-Registry (`config/types.yaml`)](#4-typ-registry-configtypesyaml)
- [4.1 Zweck](#41-zweck) - [4.1 Zweck](#41-zweck)
@ -27,13 +28,15 @@
- [5) Der Retriever (Funktionaler Layer)](#5-der-retriever-funktionaler-layer) - [5) Der Retriever (Funktionaler Layer)](#5-der-retriever-funktionaler-layer)
- [5.1 Scoring-Modell](#51-scoring-modell) - [5.1 Scoring-Modell](#51-scoring-modell)
- [5.2 Erklärbarkeit (Explainability) WP04b](#52-erklärbarkeit-explainability--wp04b) - [5.2 Erklärbarkeit (Explainability) WP04b](#52-erklärbarkeit-explainability--wp04b)
- [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.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.2 Der Hybrid Router v5 (Action vs. Question)](#62-der-hybrid-router-v5-action-vs-question)
- [6.3 Strategic Retrieval (Injektion von Werten)](#63-strategic-retrieval-injektion-von-werten) - [6.3 Traffic Control (Realtime vs. Background)](#63-traffic-control-realtime-vs-background)
- [6.4 Reasoning (Das Gewissen)](#64-reasoning-das-gewissen) - [6.4 Strategic Retrieval (Injektion von Werten)](#64-strategic-retrieval-injektion-von-werten)
- [6.5 Der Interview-Modus (One-Shot Extraction)](#65-der-interview-modus-one-shot-extraction) - [6.5 Reasoning (Das Gewissen)](#65-reasoning-das-gewissen)
- [6.6 Active Intelligence (Link Suggestions) Neu in v2.4](#66-active-intelligence-link-suggestions--neu-in-v24) - [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) Future Concepts: The Empathic Digital Twin (Ausblick)](#7-future-concepts-the-empathic-digital-twin-ausblick)
- [7.1 Antizipation durch Erfahrung](#71-antizipation-durch-erfahrung) - [7.1 Antizipation durch Erfahrung](#71-antizipation-durch-erfahrung)
- [7.2 Empathie \& "Ich"-Modus](#72-empathie--ich-modus) - [7.2 Empathie \& "Ich"-Modus](#72-empathie--ich-modus)
@ -49,7 +52,7 @@
- [13) Lösch-/Update-Garantien (Idempotenz)](#13-lösch-update-garantien-idempotenz) - [13) Lösch-/Update-Garantien (Idempotenz)](#13-lösch-update-garantien-idempotenz)
- [14) Beispiel Von Markdown zu Kanten](#14-beispiel--von-markdown-zu-kanten) - [14) Beispiel Von Markdown zu Kanten](#14-beispiel--von-markdown-zu-kanten)
- [15) Referenzen (Projektdateien \& Leitlinien)](#15-referenzen-projektdateien--leitlinien) - [15) Referenzen (Projektdateien \& Leitlinien)](#15-referenzen-projektdateien--leitlinien)
- [16) Workpackage Status (v2.4.0)](#16-workpackage-status-v240) - [16) Workpackage Status (v2.6.0)](#16-workpackage-status-v260)
</details> </details>
--- ---
@ -63,7 +66,7 @@ Die drei zentralen Artefakt-Sammlungen lauten:
- `mindnet_chunks` semantische Teilstücke einer Note (Fenster/„Chunks“) - `mindnet_chunks` semantische Teilstücke einer Note (Fenster/„Chunks“)
- `mindnet_edges` gerichtete Beziehungen zwischen Knoten (Chunks/Notes) - `mindnet_edges` gerichtete Beziehungen zwischen Knoten (Chunks/Notes)
Die Import-Pipeline (seit v2.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. - Jeder Chunk gehört **genau einer** Note.
- Chunks bilden eine Sequenz (1…N) das ermöglicht *next/prev*. - Chunks bilden eine Sequenz (1…N) das ermöglicht *next/prev*.
- **Update v2.4:** Chunks werden jetzt durch das Modell `nomic-embed-text` in **768-dimensionale Vektoren** umgewandelt. Dies erlaubt eine deutlich höhere semantische Auflösung als frühere Modelle (384 Dim). - **Update v2.4:** Chunks werden jetzt durch das Modell `nomic-embed-text` in **768-dimensionale Vektoren** umgewandelt. Dies erlaubt eine deutlich höhere semantische Auflösung als frühere Modelle (384 Dim).
- **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. Diese Kanten entstehen immer, unabhängig von Inhalten.
### 2.2 Inhalts-Kanten (explizit) ### 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):** 1. **Explizite Inline-Relationen (Höchste Priorität):**
Im Fließtext notierte, semantisch qualifizierte Relationen. 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. Beispiel: Ein *project* mit `edge_defaults=["depends_on"]` erzeugt zu *jedem* explizit referenzierten Ziel **zusätzlich** eine `depends_on`-Kante.
Diese Kanten tragen *provenance=rule* und eine **rule_id** der Form `edge_defaults:{note_type}:{relation}` sowie eine geringere Confidence (~0.7). Diese Kanten tragen *provenance=rule* und eine **rule_id** der Form `edge_defaults:{note_type}:{relation}` sowie eine geringere Confidence (~0.7).
### 2.4 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"). Mit WP-11 wurde eine Intelligenz eingeführt, die Kanten-Typen nicht nur anhand des Quell-Typs, sondern auch anhand des Ziel-Typs bestimmt ("Matrix").
**Beispiel für `Source Type: experience`:** **Beispiel für `Source Type: experience`:**
@ -154,7 +169,7 @@ Jede Kante hat mindestens:
Erweiterte/abgeleitete Felder (WP03 Superset): 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`) - `rule_id` maschinenlesbare Regelquelle (z.B. `inline:rel`, `edge_defaults:project:depends_on`)
- `confidence` 0.01.0; Heuristik zur Gewichtung im Scoring. - `confidence` 0.01.0; Heuristik zur Gewichtung im Scoring.
@ -168,20 +183,24 @@ Erweiterte/abgeleitete Felder (WP03 Superset):
- Steuert **Chunking-Profile** (`short|medium|long`) **pro Typ** - Steuert **Chunking-Profile** (`short|medium|long`) **pro Typ**
- Liefert **retriever_weight** (Note-/Chunk-Gewichtung im Ranking) **pro Typ** - Liefert **retriever_weight** (Note-/Chunk-Gewichtung im Ranking) **pro Typ**
- Definiert **edge_defaults** je Typ (s. o.) - Definiert **edge_defaults** je Typ (s. o.)
- **Neu in v2.6:** Steuert `enable_smart_edge_allocation` (LLM-Filter) und `detection_keywords` für den Hybrid Router.
Der Importer lädt die Registry aus `MINDNET_TYPES_FILE` oder nutzt Fallbacks. **Frontmatter-Overrides** für Profile werden in v2.2 weitgehend ignoriert zugunsten einer zentralen Governance in der YAML-Datei. Der Importer lädt die Registry aus `MINDNET_TYPES_FILE` oder nutzt Fallbacks. **Frontmatter-Overrides** für Profile werden in v2.2 weitgehend ignoriert zugunsten einer zentralen Governance in der YAML-Datei.
### 4.2 Beispiel (auslieferungsnah) ### 4.2 Beispiel (auslieferungsnah)
version: 1.0 version: 2.6.0
types: types:
concept: concept:
chunk_profile: medium chunk_profile: medium
edge_defaults: ["references", "related_to"] edge_defaults: ["references", "related_to"]
retriever_weight: 0.60 retriever_weight: 0.60
project: experience:
chunk_profile: long chunk_profile: sliding_smart_edges
edge_defaults: ["references", "depends_on"] enable_smart_edge_allocation: true # WP15: LLM prüft Kanten
retriever_weight: 0.97 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** **Auflösung im Importer**
- `effective_chunk_profile(note_type, registry)``"short|medium|long"|None` - `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. 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. 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. * **Früher (Pre-WP06):** Jede Frage ("Was ist X?" oder "Soll ich X?") wurde gleich behandelt -> Fakten-Retrieval.
* **Heute (WP06):** Das System erkennt, *was* der User will (Rat, Fakten oder Datenerfassung) und wechselt den Modus. * **Heute (WP06):** Das System erkennt, *was* der User will (Rat, Fakten oder Datenerfassung) und wechselt den Modus.
### 6.2 Der Intent-Router (Keyword & Semantik) ### 6.2 Der Hybrid Router v5 (Action vs. Question)
Der Router prüft vor jeder Antwort die Absicht über konfigurierbare Strategien (`config/decision_engine.yaml`): Der Router wurde in v2.6 (WP15) weiterentwickelt, um Fehlalarme zu vermeiden.
1. **FACT:** Reine Wissensfrage ("Was ist Qdrant?"). → Standard RAG. 1. **Frage-Erkennung:**
2. **DECISION:** Frage nach Rat oder Strategie ("Soll ich Qdrant nutzen?"). → Aktiviert die Decision Engine. * Das System prüft zuerst: Enthält der Satz ein `?` oder typische W-Wörter (Wer, Wie, Was)?
3. **EMPATHY:** Emotionale Zustände ("Ich bin gestresst"). → Aktiviert den empathischen Modus. * Wenn **JA** -> Gehe in den **RAG Modus** (Intent `FACT` oder `DECISION`). Interviews werden hier blockiert.
4. **INTERVIEW (WP07):** Wunsch, Wissen zu erfassen ("Neues Projekt anlegen"). → Aktiviert den Draft-Generator.
5. **CODING:** Technische Anfragen. 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: Im Modus `DECISION` führt das System eine **zweite Suchstufe** aus. Es sucht nicht nur nach semantisch passenden Texten zur Frage, sondern erzwingt das Laden von strategischen Notizen wie:
* **Values (`type: value`):** Moralische Werte (z.B. "Privacy First"). * **Values (`type: value`):** Moralische Werte (z.B. "Privacy First").
* **Principles (`type: principle`):** Handlungsanweisungen. * **Principles (`type: principle`):** Handlungsanweisungen.
* **Goals (`type: goal`):** Strategische Ziele. * **Goals (`type: goal`):** Strategische Ziele.
### 6.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."* Das LLM erhält im Prompt die explizite Anweisung: *"Wäge die Fakten (aus der Suche) gegen die injizierten Werte ab."*
Dadurch entstehen Antworten, die nicht nur technisch korrekt sind, sondern subjektiv passend ("Tool X passt nicht zu deinem Ziel Z"). Dadurch entstehen Antworten, die nicht nur technisch korrekt sind, sondern subjektiv passend ("Tool X passt nicht zu deinem Ziel Z").
### 6.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**. Wenn der User Wissen erfassen will ("Ich möchte ein neues Projekt anlegen"), wechselt Mindnet in den **Interview-Modus**.
* **Late Binding Schema:** Das System lädt ein konfiguriertes Schema für den Ziel-Typ (z.B. `project`: Pflichtfelder sind Titel, Ziel, Status). * **Late Binding Schema:** Das System lädt ein konfiguriertes Schema für den Ziel-Typ (z.B. `project`: Pflichtfelder sind Titel, Ziel, Status).
* **One-Shot Extraction:** Statt eines langen Dialogs extrahiert das LLM **sofort** alle verfügbaren Infos aus dem Prompt und generiert einen validen Markdown-Draft mit Frontmatter. * **One-Shot Extraction:** Statt eines langen Dialogs extrahiert das LLM **sofort** alle verfügbaren Infos aus dem Prompt und generiert einen validen Markdown-Draft mit Frontmatter.
* **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. * **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. Im **Draft Editor** (Frontend) unterstützt das System den Autor aktiv.
* **Analyse:** Ein "Sliding Window" scannt den Text im Hintergrund (auch lange Entwürfe). * **Analyse:** Ein "Sliding Window" scannt den Text im Hintergrund (auch lange Entwürfe).
* **Erkennung:** Es findet Begriffe ("Mindnet") und semantische Konzepte ("Autofahrt in Italien"). * **Erkennung:** Es findet Begriffe ("Mindnet") und semantische Konzepte ("Autofahrt in Italien").
* **Matching:** Es prüft gegen den Index (Aliases und Vektoren). * **Matching:** Es prüft gegen den Index (Aliases und Vektoren).
* **Vorschlag:** Es bietet fertige Markdown-Links an (z.B. `[[rel:related_to ...]]`), die per Klick eingefügt werden. * **Vorschlag:** Es bietet fertige Markdown-Links an (z.B. `[[rel:related_to ...]]`), die per Klick eingefügt werden.
* **Logik:** Dabei kommt die in 2.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 chunk_profile: short # Risiken sind oft kurze Statements
retriever_weight: 0.90 # Hohe Priorität im Ranking retriever_weight: 0.90 # Hohe Priorität im Ranking
edge_defaults: ["blocks"] # Automatische Kante zu verlinkten Projekten edge_defaults: ["blocks"] # Automatische Kante zu verlinkten Projekten
detection_keywords: ["gefahr", "risiko"] # Für den Hybrid Router
``` ```
*Effekt:* Der 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`)** 2. **Semantik erklären (`config/prompts.yaml` / `decision_engine.yaml`)**
Bringe dem LLM bei, wie es mit diesem Typ umgehen soll. Füge `risk` zur `DECISION`-Strategie hinzu (`inject_types`), damit es bei Entscheidungen geladen wird. Bringe dem LLM bei, wie es mit diesem Typ umgehen soll. Füge `risk` zur `DECISION`-Strategie hinzu (`inject_types`), damit es bei Entscheidungen geladen wird.
@ -359,12 +393,12 @@ Die Interaktion erfolgt primär über das **Web-Interface (WP10)**.
## 10) Confidence & Provenance wozu? ## 10) Confidence & Provenance wozu?
Der Retriever kann Edges gewichten: Der Retriever kann Edges gewichten:
- **provenance**: *explicit* > *rule* - **provenance**: *explicit* > *smart* (Neu) > *rule*
- **confidence**: numerische Feinsteuerung - **confidence**: numerische Feinsteuerung
- **retriever_weight (Note/Chunk)**: skaliert die Relevanz des gesamten Knotens - **retriever_weight (Note/Chunk)**: skaliert die Relevanz des gesamten Knotens
Eine typische Gewichtung (konfigurierbar in `retriever.yaml`) ist: Eine typische Gewichtung (konfigurierbar in `retriever.yaml`) ist:
`explicit: 1.0`, `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]] > [!edge] related_to: [[Vector DB Basics]]
**Ergebnis (fachlich)** **Ergebnis (fachlich - Smart Edges)**
1. `depends_on(Chunk→Qdrant)` mit `rule_id=inline:rel`, `confidence≈0.95`. Das LLM analysiert jeden Chunk.
2. `references(Chunk→Embeddings 101)` mit `rule_id=explicit:wikilink`, `confidence=1.0`. 1. Chunk 1 ("Wir nutzen..."): Enthält `depends_on(Chunk→Qdrant)`. Das LLM bestätigt: Relevant. -> Kante wird erstellt.
3. `related_to(Chunk→Vector DB Basics)` via Callout; `rule_id=callout:edge`, `confidence≈0.90`. 2. Chunk 2 ("Siehe auch..."): Enthält `references(Chunk→Embeddings)`. Das LLM bestätigt.
4. **Typ-Defaults:** Falls die Note vom Typ `project` ist, entstehen zusätzlich `depends_on`-Kanten zu den Zielen aus (2) und (3). 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`. - Import-Pipeline & Registry-Auflösung: `scripts/import_markdown.py`.
- Kantenbildung (V2-Logic): `app/core/derive_edges.py`. - Kantenbildung (V2-Logic): `app/core/derive_edges.py`.
- Smart Chunking & Traffic Control: `app/core/chunker.py` & `app/services/llm_service.py`.
- Typ-Registry: `config/types.yaml` & `TYPE_REGISTRY_MANUAL.md`. - Typ-Registry: `config/types.yaml` & `TYPE_REGISTRY_MANUAL.md`.
- Retriever-Scoring & Explanation: `app/core/retriever.py`. - Retriever-Scoring & Explanation: `app/core/retriever.py`.
- Persönlichkeit & Chat: `config/prompts.yaml` & `app/routers/chat.py`. - Persönlichkeit & Chat: `config/prompts.yaml` & `app/routers/chat.py`.
@ -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. Aktueller Implementierungsstand der Module.
@ -448,9 +483,12 @@ Aktueller Implementierungsstand der Module.
| **WP04b**| Explanation Layer | 🟢 Live | API liefert Reasons & Breakdown. | | **WP04b**| Explanation Layer | 🟢 Live | API liefert Reasons & Breakdown. |
| **WP04c**| Feedback Loop | 🟢 Live | Logging (JSONL) & Traceability aktiv. | | **WP04c**| Feedback Loop | 🟢 Live | Logging (JSONL) & Traceability aktiv. |
| **WP05** | Persönlichkeit / Chat | 🟢 Live | RAG-Integration mit Context Enrichment. | | **WP05** | Persönlichkeit / Chat | 🟢 Live | RAG-Integration mit Context Enrichment. |
| **WP06** | Decision Engine | 🟢 Live | Intent-Router & Strategic Retrieval. | | **WP06** | Decision Engine | 🟢 Live | Hybrid Router, Strategic Retrieval. |
| **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** | | **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** |
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. | | **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). | | **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** | | **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI & Healing Parser.** |
| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** | | **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** |
| **WP15** | Smart Edge Allocation | 🟢 Live | **LLM-Filter & Traffic Control aktiv.** |
| **WP16** | Auto-Discovery | 🟡 Geplant | UX & Retrieval Tuning. |
| **WP17** | Conversational Memory | 🟡 Geplant | Dialog-Gedächtnis. |

View File

@ -1,21 +1,21 @@
# Mindnet v2.4 Technische Architektur # Mindnet v2.6 Technische Architektur
**Datei:** `docs/mindnet_technical_architecture_v2.4.md` **Datei:** `docs/mindnet_technical_architecture_v2.6.md`
**Stand:** 2025-12-11 **Stand:** 2025-12-12
**Status:** **FINAL** (Integrierter Stand WP01WP11: Async Intelligence) **Status:** **FINAL** (Integrierter Stand WP01WP15: Smart Edges & Traffic Control)
**Quellen:** `Programmplan_V2.2.md`, `Handbuch.md`, `chunking_strategy.md`, `wp04_retriever_scoring.md`. **Quellen:** `Programmplan_V2.6.md`, `Handbuch.md`, `chunking_strategy.md`, `wp04_retriever_scoring.md`.
> **Ziel dieses Dokuments:** > **Ziel dieses Dokuments:**
> Vollständige Beschreibung der technischen Architektur inkl. Graph-Datenbank, Retrieval-Logik, der **RAG-Komponenten (Decision Engine & Hybrid Router)**, des **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> <details>
<summary>📖 <b>Inhaltsverzeichnis (Klicken zum Öffnen)</b></summary> <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. Systemüberblick](#1-systemüberblick)
- [1.1 Architektur-Zielbild](#11-architektur-zielbild) - [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. Datenmodell \& Collections (Qdrant)](#2-datenmodell--collections-qdrant)
- [2.1 Notes Collection (`<prefix>_notes`)](#21-notes-collection-prefix_notes) - [2.1 Notes Collection (`<prefix>_notes`)](#21-notes-collection-prefix_notes)
- [2.2 Chunks Collection (`<prefix>_chunks`)](#22-chunks-collection-prefix_chunks) - [2.2 Chunks Collection (`<prefix>_chunks`)](#22-chunks-collection-prefix_chunks)
@ -27,22 +27,23 @@
- [3.4 Prompts (`config/prompts.yaml`)](#34-prompts-configpromptsyaml) - [3.4 Prompts (`config/prompts.yaml`)](#34-prompts-configpromptsyaml)
- [3.5 Environment (`.env`)](#35-environment-env) - [3.5 Environment (`.env`)](#35-environment-env)
- [4. Import-Pipeline (Markdown → Qdrant)](#4-import-pipeline-markdown--qdrant) - [4. Import-Pipeline (Markdown → Qdrant)](#4-import-pipeline-markdown--qdrant)
- [4.1 Verarbeitungsschritte (Async)](#41-verarbeitungsschritte-async) - [4.1 Verarbeitungsschritte (Async + Smart)](#41-verarbeitungsschritte-async--smart)
- [5. Retriever-Architektur \& Scoring](#5-retriever-architektur--scoring) - [5. Retriever-Architektur \& Scoring](#5-retriever-architektur--scoring)
- [5.1 Betriebsmodi](#51-betriebsmodi) - [5.1 Betriebsmodi](#51-betriebsmodi)
- [5.2 Scoring-Formel (WP04a)](#52-scoring-formel-wp04a) - [5.2 Scoring-Formel (WP04a)](#52-scoring-formel-wp04a)
- [5.3 Explanation Layer (WP04b)](#53-explanation-layer-wp04b) - [5.3 Explanation Layer (WP04b)](#53-explanation-layer-wp04b)
- [5.4 Graph-Expansion](#54-graph-expansion) - [5.4 Graph-Expansion](#54-graph-expansion)
- [6. RAG \& Chat Architektur (WP06 Hybrid Router + WP07 Interview)](#6-rag--chat-architektur-wp06-hybrid-router--wp07-interview) - [6. RAG \& Chat Architektur (WP06 Hybrid Router + WP07 Interview)](#6-rag--chat-architektur-wp06-hybrid-router--wp07-interview)
- [6.1 Architektur-Pattern: Intent Router](#61-architektur-pattern-intent-router) - [6.1 Architektur-Pattern: Intent Router v5](#61-architektur-pattern-intent-router-v5)
- [6.2 Schritt 1: Intent Detection (Hybrid)](#62-schritt-1-intent-detection-hybrid) - [6.2 Traffic Control (Priorisierung)](#62-traffic-control-priorisierung)
- [6.3 Schritt 2: Strategy Resolution (Late Binding)](#63-schritt-2-strategy-resolution-late-binding) - [6.3 Schritt 1: Intent Detection (Question vs. Action)](#63-schritt-1-intent-detection-question-vs-action)
- [6.4 Schritt 3: Retrieval vs. Extraction](#64-schritt-3-retrieval-vs-extraction) - [6.4 Schritt 2: Strategy Resolution](#64-schritt-2-strategy-resolution)
- [6.5 Schritt 4: Generation \& Response](#65-schritt-4-generation--response) - [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. Frontend Architektur (WP10)](#7-frontend-architektur-wp10)
- [7.1 Kommunikation](#71-kommunikation) - [7.1 Kommunikation](#71-kommunikation)
- [7.2 Features \& UI-Logik](#72-features--ui-logik) - [7.2 Features \& UI-Logik](#72-features--ui-logik)
- [7.3 Draft-Editor \& 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.4 State Management (Resurrection Pattern)](#74-state-management-resurrection-pattern)
- [7.5 Deployment Ports](#75-deployment-ports) - [7.5 Deployment Ports](#75-deployment-ports)
- [8. Feedback \& Logging Architektur (WP04c)](#8-feedback--logging-architektur-wp04c) - [8. Feedback \& Logging Architektur (WP04c)](#8-feedback--logging-architektur-wp04c)
@ -62,57 +63,63 @@
Mindnet ist ein **lokales RAG-System (Retrieval Augmented Generation)** mit Web-Interface. Mindnet ist ein **lokales RAG-System (Retrieval Augmented Generation)** mit Web-Interface.
1. **Source:** Markdown-Notizen in einem Vault (Obsidian-kompatibel). 1. **Source:** Markdown-Notizen in einem Vault (Obsidian-kompatibel).
2. **Pipeline:** Ein Python-Importer transformiert diese in **Notes**, **Chunks** und **Edges**. 2. **Pipeline:** Ein Python-Importer transformiert diese in **Notes**, **Chunks** und **Edges**.
* **Neu in v2.6:** Intelligente Kanten-Zuweisung durch lokale LLMs (**Smart Edge Allocation**).
3. **Storage:** 3. **Storage:**
* **Qdrant:** Vektor-Datenbank für Graph und Semantik (Collections: notes, chunks, edges). * **Qdrant:** Vektor-Datenbank für Graph und Semantik (Collections: notes, chunks, edges).
* **Local Files (JSONL):** Append-Only Logs für Feedback und Search-History (Data Flywheel). * **Local Files (JSONL):** Append-Only Logs für Feedback und Search-History (Data Flywheel).
4. **Backend:** Eine FastAPI-Anwendung stellt Endpunkte für **Semantische** und **Hybride Suche** sowie **Feedback** bereit. 4. **Backend:** Eine FastAPI-Anwendung stellt Endpunkte für **Semantische** und **Hybride Suche** sowie **Feedback** bereit.
* **Update v2.3.10:** Der Core arbeitet nun vollständig **asynchron (AsyncIO)**, um Blockaden bei Embedding-Requests zu vermeiden. * **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** und **Intelligence-Features**. 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`. 6. **Inference:** Lokales LLM (Ollama: Phi-3 Mini) für RAG-Chat und Antwortgenerierung. Embedding via `nomic-embed-text`.
Das System arbeitet **deterministisch** (stabile IDs) und ist **konfigurationsgetrieben** (`types.yaml`, `retriever.yaml`, `decision_engine.yaml`, `prompts.yaml`). Das System arbeitet **deterministisch** (stabile IDs) und ist **konfigurationsgetrieben** (`types.yaml`, `retriever.yaml`, `decision_engine.yaml`, `prompts.yaml`).
### 1.2 Verzeichnisstruktur & Komponenten (Post-WP10) ### 1.2 Verzeichnisstruktur & Komponenten (Post-WP15)
/mindnet/ /mindnet/
├── app/ ├── app/
│ ├── main.py # FastAPI Einstiegspunkt │ ├── main.py # FastAPI Einstiegspunkt
│ ├── core/ │ ├── core/
│ │ ├── ingestion.py # NEU: Async Ingestion Service (WP11) │ │ ├── ingestion.py # NEU: Async Ingestion mit Change Detection
│ │ ├── qdrant.py # Client-Factory & Connection │ │ ├── qdrant.py # Client-Factory & Connection
│ │ ├── qdrant_points.py # Low-Level Point Operations (Upsert/Delete) │ │ ├── qdrant_points.py # Low-Level Point Operations (Upsert/Delete)
│ │ ├── note_payload.py # Bau der Note-Objekte │ │ ├── note_payload.py # Bau der Note-Objekte
│ │ ├── chunk_payload.py # Bau der Chunk-Objekte │ │ ├── chunk_payload.py # Bau der Chunk-Objekte
│ │ ├── chunker.py # Text-Zerlegung (Profiling) │ │ ├── chunker.py # Smart Chunker Orchestrator (WP15)
│ │ ├── edges.py # Edge-Datenstrukturen │ │ ├── edges.py # Edge-Datenstrukturen
│ │ ├── derive_edges.py # Logik der Kantenableitung (WP03) │ │ ├── derive_edges.py # Logik der Kantenableitung (WP03)
│ │ ├── graph_adapter.py # Subgraph & Reverse-Lookup (WP04b) │ │ ├── graph_adapter.py # Subgraph & Reverse-Lookup (WP04b)
│ │ └── retriever.py # Scoring, Expansion & Explanation (WP04a/b) │ │ └── retriever.py # Scoring, Expansion & Explanation (WP04a/b)
│ ├── models/ # Pydantic DTOs │ ├── models/ # Pydantic DTOs
│ │ └── dto.py # Zentrale DTO-Definition
│ ├── routers/ │ ├── routers/
│ │ ├── query.py # Such-Endpunkt │ │ ├── query.py # Such-Endpunkt
│ │ ├── ingest.py # NEU: API für Save & Analyze (WP11) │ │ ├── ingest.py # NEU: API für Save & Analyze (WP11)
│ │ ├── chat.py # Hybrid Router & Interview Logic (WP06/07) │ │ ├── chat.py # Hybrid Router v5 & Interview Logic (WP06/07)
│ │ ├── feedback.py # Feedback-Endpunkt (WP04c) │ │ ├── feedback.py # Feedback-Endpunkt (WP04c)
│ │ └── ... │ │ └── ...
│ ├── services/ │ ├── services/
│ │ ├── llm_service.py # Ollama Chat Client │ │ ├── llm_service.py # Ollama Chat Client mit Traffic Control (v2.8.0)
│ │ ├── embeddings_client.py# NEU: Async Embedding Client (HTTPX) │ │ ├── semantic_analyzer.py# NEU: LLM-Filter für Edges (WP15)
│ │ └── feedback_service.py # JSONL Logging (WP04c) │ │ ├── embeddings_client.py# NEU: Async Embeddings (HTTPX)
│ │ ├── feedback_service.py # Logging (JSONL Writer)
│ │ └── discovery.py # NEU: Intelligence Logic (WP11)
│ ├── frontend/ # NEU (WP10) │ ├── frontend/ # NEU (WP10)
│ └── ui.py # Streamlit Application inkl. Sanitizer │ │ └── ui.py # Streamlit Application inkl. Healing Parser
├── config/ │ └── main.py # Entrypoint der API
│ ├── types.yaml # Typ-Definitionen (Import-Zeit) ├── config/ # YAML-Konfigurationen (Single Source of Truth)
│ ├── retriever.yaml # Scoring-Gewichte (Laufzeit) │ ├── types.yaml # Import-Regeln & Smart-Edge Config
│ ├── decision_engine.yaml # Strategien & Schemas (WP06/WP07) │ ├── prompts.yaml # LLM Prompts & Interview Templates (WP06/07)
│ └── prompts.yaml # LLM System-Prompts & Templates (WP06) │ ├── decision_engine.yaml # Router-Strategien (Actions only)
│ └── retriever.yaml # Scoring-Regeln & Kantengewichte
├── data/ ├── data/
│ └── logs/ # Lokale JSONL-Logs (WP04c) │ └── logs/ # Lokale Logs (search_history.jsonl, feedback.jsonl)
├── scripts/ ├── scripts/ # CLI-Tools (Import, Diagnose, Reset)
│ ├── import_markdown.py # Haupt-Importer CLI (Async) │ ├── import_markdown.py # Haupt-Importer CLI (Async)
│ ├── payload_dryrun.py # Diagnose: JSON-Generierung ohne DB │ ├── payload_dryrun.py # Diagnose: JSON-Generierung ohne DB
│ └── edges_full_check.py # Diagnose: Graph-Integrität │ └── edges_full_check.py # Diagnose: Graph-Integrität
└── tests/ # Pytest Suite ├── 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`) ### 2.2 Chunks Collection (`<prefix>_chunks`)
Die atomaren Sucheinheiten. Die atomaren Sucheinheiten.
* **Zweck:** Vektorsuche (Embeddings), Granulares Ergebnis. * **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):** * **Schema (Payload):**
| Feld | Datentyp | Beschreibung | | Feld | Datentyp | Beschreibung |
@ -171,6 +178,7 @@ Gerichtete Kanten. Massiv erweitert in WP03 für Provenienz-Tracking.
| `scope` | Keyword | Geltungsbereich. | Immer `"chunk"` (v2.2). | | `scope` | Keyword | Geltungsbereich. | Immer `"chunk"` (v2.2). |
| `rule_id` | Keyword | Herkunftsregel. | `explicit:wikilink`, `inline:rel` | | `rule_id` | Keyword | Herkunftsregel. | `explicit:wikilink`, `inline:rel` |
| `confidence` | Float | Vertrauenswürdigkeit (0.0-1.0). | 1.0, 0.95, 0.7 | | `confidence` | Float | Vertrauenswürdigkeit (0.0-1.0). | 1.0, 0.95, 0.7 |
| `provenance` | Keyword | Quelle der Kante (Neu in v2.6). | `explicit`, `rule`, `smart` |
--- ---
@ -181,13 +189,20 @@ Die Logik ist ausgelagert in YAML-Dateien.
### 3.1 Typ-Registry (`config/types.yaml`) ### 3.1 Typ-Registry (`config/types.yaml`)
Steuert den Import-Prozess. Steuert den Import-Prozess.
* **Priorität:** Frontmatter > Pfad > Default. * **Priorität:** Frontmatter > Pfad > Default.
* **Neu in v2.6:**
* `enable_smart_edge_allocation`: (Bool) Aktiviert den LLM-Filter für diesen Typ.
* `detection_keywords`: (List) Keywords für den Hybrid Router ("Objekterkennung").
* **Inhalt (Beispiel):** * **Inhalt (Beispiel):**
types: types:
concept: concept:
chunk_profile: medium chunk_profile: sliding_standard
edge_defaults: ["references", "related_to"] edge_defaults: ["references", "related_to"]
retriever_weight: 0.60 retriever_weight: 0.60
experience:
chunk_profile: sliding_smart_edges
enable_smart_edge_allocation: true
detection_keywords: ["erfahrung", "erlebt"]
### 3.2 Retriever-Config (`config/retriever.yaml`) ### 3.2 Retriever-Config (`config/retriever.yaml`)
Steuert das Scoring zur Laufzeit (WP04a). Steuert das Scoring zur Laufzeit (WP04a).
@ -199,14 +214,14 @@ Steuert das Scoring zur Laufzeit (WP04a).
centrality_weight: 0.5 centrality_weight: 0.5
### 3.3 Decision Engine (`config/decision_engine.yaml`) ### 3.3 Decision Engine (`config/decision_engine.yaml`)
**Neu in WP06/07:** Steuert den Intent-Router und die Interview-Schemas. **Neu in WP06/07:** Steuert den Intent-Router.
* Definiert Strategien (`DECISION`, `INTERVIEW`, etc.). * 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`). * Definiert LLM-Router-Settings (`llm_fallback_enabled`).
### 3.4 Prompts (`config/prompts.yaml`) ### 3.4 Prompts (`config/prompts.yaml`)
Steuert die LLM-Persönlichkeit und Templates. Steuert die LLM-Persönlichkeit und Templates.
* Enthält Templates für alle Strategien inkl. `interview_template` mit One-Shot Logik. * Enthält Templates für alle Strategien inkl. `interview_template` und neu `router_prompt`.
### 3.5 Environment (`.env`) ### 3.5 Environment (`.env`)
Erweiterung für LLM-Steuerung und Embedding-Modell: 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_EMBEDDING_MODEL=nomic-embed-text # NEU in v2.3.10
MINDNET_OLLAMA_URL=http://127.0.0.1:11434 MINDNET_OLLAMA_URL=http://127.0.0.1:11434
MINDNET_LLM_TIMEOUT=300.0 # Neu: Erhöht für CPU-Inference Cold-Starts MINDNET_LLM_TIMEOUT=300.0 # Neu: Erhöht für CPU-Inference Cold-Starts
MINDNET_API_TIMEOUT=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_DECISION_CONFIG="config/decision_engine.yaml"
MINDNET_VAULT_ROOT="./vault" # Neu: Pfad für Write-Back MINDNET_VAULT_ROOT="./vault" # Neu: Pfad für Write-Back
MINDNET_LLM_BACKGROUND_LIMIT=2 # Neu: Limit für parallele Hintergrund-Jobs (Traffic Control)
--- ---
## 4. Import-Pipeline (Markdown → Qdrant) ## 4. Import-Pipeline (Markdown → Qdrant)
Das Skript `scripts/import_markdown.py` orchestriert den Prozess. Das Skript `scripts/import_markdown.py` orchestriert den Prozess.
**Neu in v2.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:** 1. **Discovery & Parsing:** Hash-Vergleich zur Erkennung von Änderungen.
* Einlesen der `.md` Dateien. Hash-Vergleich (Body/Frontmatter) zur Erkennung von Änderungen. 2. **Typauflösung:** Bestimmung des `type` via `types.yaml`.
2. **Typauflösung:** 3. **Config Check:** Laden des `chunk_profile` und `enable_smart_edge_allocation`.
* Bestimmung des `type` via `types.yaml`. 4. **Chunking & Smart Edges (WP15):**
3. **Chunking:** * Zerlegung des Textes via `chunker.py`.
* Zerlegung via `chunker.py` basierend auf `chunk_profile`. * Wenn Smart Edges aktiv: Der `SemanticAnalyzer` sendet Chunks an das LLM.
4. **Embedding (Async):** * **Traffic Control:** Der Request nutzt `priority="background"`. Die Semaphore (Limit: 2) drosselt die Parallelität.
* 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.
5. **Kantenableitung (Edge Derivation):** 5. **Kantenableitung (Edge Derivation):**
* `derive_edges.py` erzeugt Inline-, Callout- und Default-Edges. * `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`. * Schreiben in Qdrant. Nutzung von `--purge-before-upsert`.
* **Strict Mode:** Der Prozess bricht ab, wenn Embeddings leer sind oder Dimension `0` haben. * **Strict Mode:** Der Prozess bricht ab, wenn Embeddings leer sind oder Dimension `0` haben.
@ -269,49 +285,55 @@ $$
* **Gewichte ($W$):** Stammen aus `retriever.yaml`. * **Gewichte ($W$):** Stammen aus `retriever.yaml`.
### 5.3 Explanation Layer (WP04b) ### 5.3 Explanation Layer (WP04b)
Der Retriever kann Ergebnisse erklären (`explain=True`). Der Retriever ist keine Blackbox mehr. Er liefert auf Wunsch (`explain=True`) eine strukturierte Begründung (`Explanation`-Objekt).
* **Logik:**
* Berechnung des `ScoreBreakdown` (Anteile von Semantik, Graph, Typ). **Die "Warum"-Logik:**
* Analyse des lokalen Subgraphen mittels `graph_adapter.py`. 1. **Semantik:** Prüfung der Cosine-Similarity ("Sehr hohe textuelle Übereinstimmung").
* **Incoming Edges (Authority):** Wer zeigt auf diesen Treffer? (z.B. "Referenziert von...") 2. **Typ:** Prüfung des `retriever_weight` ("Bevorzugt, da Entscheidung").
* **Outgoing Edges (Hub):** Worauf zeigt dieser Treffer? (z.B. "Verweist auf...") 3. **Graph (Kontext):**
* **Output:** `QueryHit` enthält ein `explanation` Objekt mit menschenlesbaren `reasons` und `related_edges`. * **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 ### 5.4 Graph-Expansion
Der Hybrid-Modus lädt dynamisch die Nachbarschaft der Top-K Vektor-Treffer ("Seeds") über `graph_adapter.expand`. Dies baut einen temporären `NetworkX`-artigen Graphen im Speicher (Klasse `Subgraph`), auf dem Boni berechnet werden. Der Hybrid-Modus lädt dynamisch die Nachbarschaft der Top-K Vektor-Treffer ("Seeds") über `graph_adapter.expand`. Dies baut einen temporären `NetworkX`-artigen Graphen im Speicher (Klasse `Subgraph`), auf dem Boni berechnet werden.
--- ---
## 6. RAG \& Chat Architektur (WP06 Hybrid Router + WP07 Interview) ## 6. RAG & Chat Architektur (WP06 Hybrid Router + WP07 Interview)
Der Flow für eine Chat-Anfrage (`/chat`) wurde in WP06 auf eine **Configuration-Driven Architecture** umgestellt. 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. Die Behandlung einer Anfrage ist nicht mehr hartkodiert, sondern wird dynamisch zur Laufzeit entschieden.
* **Input:** User Message. * **Input:** User Message.
* **Config:** `config/decision_engine.yaml` (Strategien & Keywords). * **Config:** `decision_engine.yaml` (Strategien) + `types.yaml` (Objekte).
* **Komponenten:** * **Komponenten:**
* **Fast Path:** Keyword Matching (CPU-schonend). * **Traffic Control:** `LLMService` priorisiert Chat-Anfragen.
* **Slow Path:** LLM-basierter Semantic Router (für subtile Intents). * **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. Der Router ermittelt die Absicht (`Intent`) des Nutzers.
1. **Keyword Scan (Fast Path):** 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.
* Iteration über alle Strategien in `decision_engine.yaml`. 2. **Keyword Scan (Fast Path):**
* Prüfung auf `trigger_keywords`. * Prüfung auf `trigger_keywords` (Handlung) in `decision_engine.yaml`.
* **Best Match:** Bei mehreren Treffern gewinnt das längste/spezifischste Keyword (Robustheit gegen Shadowing). * Prüfung auf `detection_keywords` (Objekt) in `types.yaml`.
2. **LLM Fallback (Slow Path):** * Treffer -> **INTERVIEW Modus** (Erfassen).
* Nur aktiv, wenn `llm_fallback_enabled: true`. 3. **LLM Fallback (Slow Path):**
* Greift, wenn keine Keywords gefunden wurden. * Greift, wenn keine Keywords passen. Sendet Query an LLM Router.
* Sendet die Query an das LLM mit einem Klassifizierungs-Prompt (`llm_router_prompt`).
* Ergebnis: `EMPATHY`, `DECISION`, `INTERVIEW`, `CODING` oder `FACT`.
### 6.3 Schritt 2: Strategy Resolution (Late Binding) ### 6.4 Schritt 2: Strategy Resolution
Basierend auf dem Intent lädt der Router die Parameter: Basierend auf dem Intent lädt der Router die Parameter:
* **Bei RAG (FACT/DECISION):** `inject_types` für Strategic Retrieval. * **Bei RAG (FACT/DECISION):** `inject_types` für Strategic Retrieval.
* **Bei INTERVIEW (WP07):** `schemas` (Pflichtfelder) basierend auf der erkannten Ziel-Entität (`_detect_target_type`). * **Bei INTERVIEW (WP07):** `schemas` (Pflichtfelder) basierend auf der erkannten Ziel-Entität (`_detect_target_type`).
### 6.4 Schritt 3: Retrieval vs. Extraction ### 6.5 Schritt 3: Retrieval vs. Extraction
Der Router verzweigt hier: Der Router verzweigt hier:
**A) RAG Modus (FACT, DECISION, EMPATHY):** **A) RAG Modus (FACT, DECISION, EMPATHY):**
@ -324,9 +346,9 @@ Der Router verzweigt hier:
2. **Schema Injection:** Das Schema für den erkannten Typ (z.B. "Project") wird geladen. 2. **Schema Injection:** Das Schema für den erkannten Typ (z.B. "Project") wird geladen.
3. **Prompt Assembly:** Der `interview_template` Prompt wird mit der Schema-Definition ("Ziel", "Status") gefüllt. 3. **Prompt Assembly:** Der `interview_template` Prompt wird mit der Schema-Definition ("Ziel", "Status") gefüllt.
### 6.5 Schritt 4: Generation & Response ### 6.6 Schritt 4: Generation & Response
* **Templating:** Das LLM erhält den Prompt basierend auf dem gewählten Template. * **Templating:** Das LLM erhält den Prompt basierend auf dem gewählten Template.
* **Execution:** Der `LLMService` führt den Call 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`. * **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 ### 7.1 Kommunikation
* **Backend-URL:** Konfiguriert via `MINDNET_API_URL` (Default: `http://localhost:8002`). * **Backend-URL:** Konfiguriert via `MINDNET_API_URL` (Default: `http://localhost:8002`).
* **Endpoints:** Nutzt `/chat` für Interaktion, `/feedback` für Bewertungen und `/ingest/analyze` für Intelligence. * **Endpoints:**
* **Resilienz:** Das Frontend implementiert eigene Timeouts (`MINDNET_API_TIMEOUT`, Default 300s). * `/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 ### 7.2 Features & UI-Logik
* **State Management:** Session-State speichert Chat-Verlauf und `query_id`. * **State Management:** Session-State speichert Chat-Verlauf und `query_id`.
@ -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. * **Source Expanders:** Zeigt verwendete Chunks inkl. Score und "Why"-Explanation.
* **Sidebar:** Zeigt Suchhistorie (Log-basiert) und Konfiguration. * **Sidebar:** Zeigt Suchhistorie (Log-basiert) und Konfiguration.
### 7.3 Draft-Editor & 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**. Wenn der Intent `INTERVIEW` ist, rendert die UI statt einer Textblase den **Draft-Editor**.
1. **Parsing:** Die Funktion `parse_markdown_draft` extrahiert den Codeblock aus der LLM-Antwort. 1. **Parsing:** Die Funktion `parse_markdown_draft` extrahiert den Codeblock aus der LLM-Antwort.
* **Healing:** Erkennt und repariert defekte YAML-Frontmatter (z.B. fehlendes schließendes `---`), falls das LLM unter Last Fehler macht.
2. **Sanitization (`normalize_meta_and_body`):** 2. **Sanitization (`normalize_meta_and_body`):**
* Prüft den YAML-Frontmatter auf unerlaubte Felder (Halluzinationen des LLMs). * Prüft den YAML-Frontmatter auf unerlaubte Felder (Halluzinationen des LLMs).
* Verschiebt ungültige Felder (z.B. "Situation") in den Body der Notiz. * Verschiebt ungültige Felder (z.B. "Situation") in den Body der Notiz.
* Stellt sicher, dass das Markdown valide bleibt.
3. **Editor Widget:** `st.text_area` erlaubt das Bearbeiten des Inhalts vor dem Speichern. 3. **Editor Widget:** `st.text_area` erlaubt das Bearbeiten des Inhalts vor dem Speichern.
4. **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) ### 7.4 State Management (Resurrection Pattern)
Um Datenverlust bei Tab-Wechseln (Chat <-> Editor) zu verhindern, nutzt `ui.py` ein Persistenz-Muster: Um Datenverlust bei Tab-Wechseln (Chat <-> Editor) zu verhindern, nutzt `ui.py` ein Persistenz-Muster:
@ -420,5 +446,5 @@ Validierung erfolgt über `tests/ensure_indexes_and_show.py`.
2. **Unresolved Targets:** 2. **Unresolved Targets:**
* Kanten zu Notizen, die noch nicht existieren, werden mit `target_id="Titel"` angelegt. * Kanten zu Notizen, die noch nicht existieren, werden mit `target_id="Titel"` angelegt.
* Heilung durch `scripts/resolve_unresolved_references.py` möglich. * Heilung durch `scripts/resolve_unresolved_references.py` möglich.
3. **Vektor-Konfiguration für Edges:** 3. **Hardware-Last bei Smart Import:**
* `mindnet_edges` hat aktuell keine Vektoren (`vectors = null`). Eine semantische Suche *auf Kanten* ist noch nicht möglich. * 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 # mindnet v2.4 Pipeline Playbook
**Datei:** `docs/mindnet_pipeline_playbook_v2.4.md` **Datei:** `docs/mindnet_pipeline_playbook_v2.6.md`
**Stand:** 2025-12-11 **Stand:** 2025-12-12
**Status:** **FINAL** (Inkl. Async Ingestion & Active Intelligence) **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`. **Quellen:** `mindnet_v2_implementation_playbook.md`, `Handbuch.md`, `chunking_strategy.md`, `docs_mindnet_retriever.md`, `mindnet_admin_guide_v2.4.md`.
--- ---
@ -12,7 +12,7 @@
- [](#) - [](#)
- [1. Zweck \& Einordnung](#1-zweck--einordnung) - [1. Zweck \& Einordnung](#1-zweck--einordnung)
- [2. Die Import-Pipeline (Runbook)](#2-die-import-pipeline-runbook) - [2. Die Import-Pipeline (Runbook)](#2-die-import-pipeline-runbook)
- [2.1 Der 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.2 Standard-Betrieb (Inkrementell)](#22-standard-betrieb-inkrementell)
- [2.3 Deployment \& Restart (Systemd)](#23-deployment--restart-systemd) - [2.3 Deployment \& Restart (Systemd)](#23-deployment--restart-systemd)
- [2.4 Full Rebuild (Clean Slate)](#24-full-rebuild-clean-slate) - [2.4 Full Rebuild (Clean Slate)](#24-full-rebuild-clean-slate)
@ -21,12 +21,13 @@
- [3.2 Payload-Felder](#32-payload-felder) - [3.2 Payload-Felder](#32-payload-felder)
- [4. Edge-Erzeugung (Die V2-Logik)](#4-edge-erzeugung-die-v2-logik) - [4. Edge-Erzeugung (Die V2-Logik)](#4-edge-erzeugung-die-v2-logik)
- [4.1 Prioritäten \& Provenance](#41-prioritäten--provenance) - [4.1 Prioritäten \& Provenance](#41-prioritäten--provenance)
- [4.2 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. Retriever, Chat \& Generation (RAG Pipeline)](#5-retriever-chat--generation-rag-pipeline)
- [5.1 Retrieval (Hybrid)](#51-retrieval-hybrid) - [5.1 Retrieval (Hybrid)](#51-retrieval-hybrid)
- [5.2 Intent Router (WP06/07)](#52-intent-router-wp0607) - [5.2 Intent Router (WP06/07)](#52-intent-router-wp0607)
- [5.3 Context Enrichment](#53-context-enrichment) - [5.3 Context Enrichment](#53-context-enrichment)
- [5.4 Generation (LLM)](#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) - [5.5 Active Intelligence Pipeline (Neu in v2.4)](#55-active-intelligence-pipeline-neu-in-v24)
- [6. Feedback \& Lernen (WP04c)](#6-feedback--lernen-wp04c) - [6. Feedback \& Lernen (WP04c)](#6-feedback--lernen-wp04c)
- [7. Quality Gates \& Tests](#7-quality-gates--tests) - [7. Quality Gates \& Tests](#7-quality-gates--tests)
@ -34,7 +35,7 @@
- [7.2 Smoke-Test (E2E)](#72-smoke-test-e2e) - [7.2 Smoke-Test (E2E)](#72-smoke-test-e2e)
- [8. Ausblick \& Roadmap (Technische Skizzen)](#8-ausblick--roadmap-technische-skizzen) - [8. Ausblick \& Roadmap (Technische Skizzen)](#8-ausblick--roadmap-technische-skizzen)
- [8.1 WP-08: Self-Tuning (Skizze)](#81-wp-08-self-tuning-skizze) - [8.1 WP-08: Self-Tuning (Skizze)](#81-wp-08-self-tuning-skizze)
- [16. Workpackage Status (v2.4.0)](#16-workpackage-status-v240) - [16. Workpackage Status (v2.6.0)](#16-workpackage-status-v260)
</details> </details>
--- ---
@ -45,7 +46,7 @@ Dieses Playbook ist das zentrale operative Handbuch für die **mindnet-Pipeline*
**Zielgruppe:** Dev/Ops, Tech-Leads. **Zielgruppe:** Dev/Ops, Tech-Leads.
**Scope:** **Scope:**
* **Ist-Stand (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). * **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. 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) ### 2.1 Der 13-Schritte-Prozess (Async + Smart)
Seit v2.3.10 läuft der Import **asynchron**, um Netzwerk-Blockaden bei der Embedding-Generierung zu vermeiden. 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. 1. **Markdown lesen:** Rekursives Scannen des Vaults.
2. **Frontmatter extrahieren:** Validierung von Pflichtfeldern (`id`, `type`, `title`). 2. **Frontmatter extrahieren:** Validierung von Pflichtfeldern (`id`, `type`, `title`).
3. **Typauflösung:** Bestimmung des `type` via `types.yaml` (Prio: Frontmatter > Pfad > Default). 3. **Typauflösung:** Bestimmung des `type` via `types.yaml` (Prio: Frontmatter > Pfad > Default).
4. **Note-Payload generieren:** Erstellen des JSON-Objekts für `mindnet_notes`. 4. **Note-Payload generieren:** Erstellen des JSON-Objekts für `mindnet_notes`.
5. **Chunking anwenden:** Zerlegung des Textes basierend auf dem `chunk_profile` des Typs. 5. **Chunking anwenden:** Zerlegung des Textes basierend auf dem `chunk_profile` des Typs (z.B. `sliding_smart_edges`).
6. **Inline-Kanten finden:** Parsing von `[[rel:...]]` im Fließtext. 6. **Smart Edge Allocation (Neu in WP15):**
7. **Callout-Kanten finden:** Parsing von `> [!edge]` Blöcken. * Wenn in `types.yaml` aktiviert (`enable_smart_edge_allocation`):
8. **Default-Edges erzeugen:** Anwendung der `edge_defaults` aus der Typ-Registry. * Der `SemanticAnalyzer` sendet jeden Chunk an das LLM.
9. **Strukturkanten erzeugen:** `belongs_to` (Chunk->Note), `next`/`prev` (Sequenz). * **Resilienz & Traffic Control:**
10. **Embedding & Upsert (Async):** * **Priority:** Der Request nutzt `priority="background"`.
* Das System nutzt eine **Semaphore** (Limit: 5 Files concurrent), um Ollama nicht zu überlasten. * **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). * 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. **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. 13. **Diagnose:** Automatischer Check der Integrität nach dem Lauf.
### 2.2 Standard-Betrieb (Inkrementell) ### 2.2 Standard-Betrieb (Inkrementell)
Für regelmäßige Updates (z.B. Cronjob). Erkennt Änderungen via Hash. Für regelmäßige Updates (z.B. Cronjob). Erkennt Änderungen via Hash.
@ -102,7 +112,7 @@ Nach einem Import oder Code-Update müssen die API-Prozesse neu gestartet werden
sudo systemctl status mindnet-prod sudo systemctl status mindnet-prod
### 2.4 Full Rebuild (Clean Slate) ### 2.4 Full Rebuild (Clean Slate)
Notwendig bei Änderungen an `types.yaml` (z.B. 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! **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 ### 3.1 Chunk-Profile
In `types.yaml` definiert. Standard-Profile (in `chunk_config.py` implementiert): In `types.yaml` definiert. Standard-Profile (in `chunk_config.py` implementiert):
* `short`: Max 128 Tokens (z.B. für Logs, Chats). * `sliding_short`: Max 128 Tokens (z.B. für Logs, Chats).
* `medium`: Max 256 Tokens (z.B. für Konzepte). * `sliding_standard`: Max 512 Tokens (Standard für Massendaten).
* `long`: Max 512 Tokens (z.B. für Essays, Projekte). * `sliding_smart_edges`: Sliding Window, optimiert für LLM-Analyse (Fließtext).
* `by_heading`: Trennt strikt an Überschriften. * `structured_smart_edges`: Trennt strikt an Überschriften (für strukturierte Daten).
### 3.2 Payload-Felder ### 3.2 Payload-Felder
Jeder Chunk erhält zwei Text-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) ## 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 ### 4.1 Prioritäten & Provenance
Der Importer setzt `provenance`, `rule_id` und `confidence` automatisch: 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 | | **1** | Inline | `[[rel:depends_on X]]` | `inline:rel` | ~0.95 |
| **2** | Callout | `> [!edge] related_to: [[X]]` | `callout:edge` | ~0.90 | | **2** | Callout | `> [!edge] related_to: [[X]]` | `callout:edge` | ~0.90 |
| **3** | Wikilink | `[[X]]` | `explicit:wikilink` | 1.00 | | **3** | Wikilink | `[[X]]` | `explicit:wikilink` | 1.00 |
| **4** | Default | *(via types.yaml)* | `edge_defaults:...` | ~0.70 | | **4** | Smart | *(via LLM Filter)* | `smart:llm_filter` | 0.90 |
| **5** | Struktur | *(automatisch)* | `structure:...` | 1.00 | | **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. Wenn in `types.yaml` für einen Typ `edge_defaults` definiert sind, werden diese **additiv** zu expliziten Links erzeugt.
* *Beispiel:* Note Typ `project` verlinkt `[[Tool A]]`. * *Beispiel:* Note Typ `project` verlinkt `[[Tool A]]`.
* *Ergebnis:* Kante `references` (explizit) UND Kante `depends_on` (Default). * *Ergebnis:* Kante `references` (explizit) UND Kante `depends_on` (Default).
@ -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). Der `/chat` Endpunkt nutzt **Hybrid Retrieval** (Semantic + Graph), um auch logisch verbundene, aber textlich unterschiedliche Notizen zu finden (z.B. Decisions zu einem Projekt).
### 5.2 Intent Router (WP06/07) ### 5.2 Intent Router (WP06/07)
Der Request durchläuft den **Hybrid Router**: Der Request durchläuft den **Hybrid Router v5**:
1. **Fast Path:** Prüfung auf `trigger_keywords` aus `decision_engine.yaml`. 1. **Question Detection:** Ist es eine Frage (`?`, W-Wörter)? -> RAG Modus. Interviews werden hier blockiert.
2. **Slow Path:** Falls kein Keyword matched und `llm_fallback_enabled=true`, klassifiziert das LLM den Intent. 2. **Keyword Scan:** Enthält es Keywords aus `types.yaml` (Objekt, z.B. "Projekt") oder `decision_engine.yaml` (Action, z.B. "erstellen")? -> INTERVIEW Modus.
* `FACT`: Wissen abfragen. 3. **LLM Fallback:** Wenn unklar, entscheidet das LLM.
* `DECISION`: Rat suchen.
* `EMPATHY`: Trost suchen.
* `INTERVIEW`: Wissen eingeben (Neu in WP07).
3. **Result:** Auswahl der Strategie und der `inject_types` oder `schemas`.
### 5.3 Context Enrichment ### 5.3 Context Enrichment
Der Router (`chat.py`) reichert die gefundenen Chunks mit Metadaten an: Der Router (`chat.py`) reichert die gefundenen Chunks mit Metadaten an:
* **Typ-Injection:** `[DECISION]`, `[PROJECT]`. * **Typ-Injection:** `[DECISION]`, `[PROJECT]`.
* **Reasoning-Infos:** `(Score: 0.75)`. * **Reasoning-Infos:** `(Score: 0.75)`.
### 5.4 Generation (LLM) ### 5.4 Generation (LLM) mit Traffic Control
* **Engine:** Ollama (lokal). * **Engine:** Ollama (lokal).
* **Modell:** `phi3:mini` (Standard). * **Modell:** `phi3:mini` (Standard).
* **Prompting:** Template wird basierend auf Intent gewählt (`decision_template`, `interview_template` etc.). * **Prompting:** Template wird basierend auf Intent gewählt (`decision_template`, `interview_template` etc.).
* **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) ### 5.5 Active Intelligence Pipeline (Neu in v2.4)
Ein paralleler Datenfluss im Frontend ("Draft Editor") zur Unterstützung des Autors. Ein paralleler Datenfluss im Frontend ("Draft Editor") zur Unterstützung des Autors.
@ -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. Aktueller Implementierungsstand der Module.
@ -270,4 +285,7 @@ Aktueller Implementierungsstand der Module.
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. | | **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). | | **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** | | **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** | | **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** |
| **WP15** | Smart Edge Allocation | 🟢 Live | **LLM-Filter & Traffic Control aktiv.** |
| **WP16** | Auto-Discovery | 🟡 Geplant | UX & Retrieval Tuning. |
| **WP17** | Conversational Memory | 🟡 Geplant | Dialog-Gedächtnis. |

View File

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

View File

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

View File

@ -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()