diff --git a/Programmmanagement/Programmplan_V2.2.md b/Programmmanagement/Programmplan_V2.2.md index 9b09f5f..faf5cfe 100644 --- a/Programmmanagement/Programmplan_V2.2.md +++ b/Programmmanagement/Programmplan_V2.2.md @@ -1,6 +1,6 @@ # mindnet v2.4 — Programmplan -**Version:** 2.4.0 (Inkl. WP-11 Backend Intelligence) -**Stand:** 2025-12-11 +**Version:** 2.6.0 (Inkl. WP-15 Smart Edge Allocation) +**Stand:** 2025-12-12 **Status:** Aktiv --- @@ -33,6 +33,9 @@ - [WP-12 – Knowledge Rewriter (Soft Mode, geplant)](#wp-12--knowledge-rewriter-soft-mode-geplant) - [WP-13 – MCP-Integration \& Agenten-Layer (geplant)](#wp-13--mcp-integration--agenten-layer-geplant) - [WP-14 – Review / Refactoring / Dokumentation (geplant)](#wp-14--review--refactoring--dokumentation-geplant) + - [WP-15 – Smart Edge Allocation \& Chunking Strategies (abgeschlossen)](#wp-15--smart-edge-allocation--chunking-strategies-abgeschlossen) + - [WP-16 – Auto-Discovery \& Enrichment (geplant)](#wp-16--auto-discovery--enrichment-geplant) + - [WP-17 – Conversational Memory (Dialog-Gedächtnis) (geplant)](#wp-17--conversational-memory-dialog-gedächtnis-geplant) - [7. Abhängigkeiten (vereinfacht, aktualisiert)](#7-abhängigkeiten-vereinfacht-aktualisiert) - [8. Laufzeit- \& Komplexitätsindikatoren (aktualisiert)](#8-laufzeit---komplexitätsindikatoren-aktualisiert) - [9. Programmfortschritt (Ampel, aktualisiert)](#9-programmfortschritt-ampel-aktualisiert) @@ -514,6 +517,63 @@ Aufräumen, dokumentieren, stabilisieren – insbesondere für Onboarding Dritte --- +### WP-15 – Smart Edge Allocation & Chunking Strategies (abgeschlossen) + +**Phase:** D +**Status:** 🟢 abgeschlossen + +**Ziel:** +Einführung einer intelligenten Verteilung von Wissenskanten an spezifische Text-Chunks, um die Präzision des Retrievals zu erhöhen, ohne die Stabilität des Systems durch lange LLM-Verarbeitungszeiten zu gefährden. + +**Erreichte Ergebnisse:** +- **5-Stufen-Workflow:** Implementierung von "Smart Edge Allocation" (Global Scan -> Deterministic Split -> LLM Filter -> Injection -> Fallback). +- **Neue Chunking-Strategien:** Einführung von `by_heading` (für strukturierte Daten) und verbessertem `sliding_window` als deterministische Basis. +- **Robustheit:** Trennung von Zerlegung (Code) und Analyse (LLM). Bei LLM-Fehlern oder Timeouts greift ein Fallback-Mechanismus (Datenverlust ausgeschlossen). +- **Architektur:** Trennung der Orchestrierung (`chunker.py`) von der KI-Logik (`semantic_analyzer.py`). +- **Konfiguration:** Steuerung über `types.yaml` (`enable_smart_edge_allocation`) ermöglicht granulare Anpassung pro Notiztyp. + +**Aufwand / Komplexität:** +- Aufwand: Mittel +- Komplexität: Hoch + +--- + +### WP-16 – Auto-Discovery & Enrichment (geplant) + +**Phase:** D +**Status:** 🟡 geplant + +**Ziel:** +Automatisches Erkennen und Vorschlagen von fehlenden Kanten in "dummem" Text (ohne explizite Wikilinks) vor der Speicherung. Umwandlung von Text in "smarten Text" durch Nutzung des `DiscoveryService`. + +**Umfang:** +- Integration eines "Enrichers" in die Ingestion-Pipeline (Schritt 0). +- Unterscheidung zwischen "Hard Candidates" (explizite Links) und "Soft Candidates" (Vektor-basierte Vorschläge). +- LLM-basierte Verifikation der Vorschläge zur Vermeidung von Halluzinationen. + +**Aufwand / Komplexität:** +- Aufwand: Mittel +- Komplexität: Hoch + +--- +### WP-17 – Conversational Memory (Dialog-Gedächtnis) (geplant) + +**Phase:** D +**Status:** 🟡 geplant + +**Ziel:** +Erweiterung des Chat-Backends von einem statischen Request-Response-Modell zu einem zustandsbehafteten Dialogsystem ("Multi-Turn Conversation"). Das System soll sich an vorherige Aussagen im aktuellen Gesprächsverlauf erinnern, um Rückfragen ("Was meinst du damit?") korrekt zu interpretieren. + +**Umfang:** +- **API-Erweiterung:** `ChatRequest` DTO erhält ein `history`-Feld (Liste von Nachrichten). +- **Context Management:** Implementierung einer Token-Budget-Logik, die dynamisch zwischen RAG-Kontext (Dokumente) und Dialog-Verlauf (History) balanciert, um das Kontextfenster (8k) optimal zu nutzen. +- **Prompt Engineering:** Integration eines `{chat_history}` Platzhalters in den System-Prompt. +- **Frontend-Update:** Die `ui.py` muss die letzten N Nachrichtenpaare (User/AI) bei jedem Request mitsenden. + +**Aufwand / Komplexität:** +- Aufwand: Mittel +- Komplexität: Mittel +--- ## 7. Abhängigkeiten (vereinfacht, aktualisiert) WP01 → WP02 → WP03 → WP04a @@ -522,6 +582,8 @@ Aufräumen, dokumentieren, stabilisieren – insbesondere für Onboarding Dritte WP07 → WP10a WP03 → WP09 WP01/WP03 → WP10 → WP11 → WP12 + WP10 → WP17 + WP11 → WP15 → WP16 WP03/WP04 → WP13 Alles → WP14 @@ -544,6 +606,9 @@ Aufräumen, dokumentieren, stabilisieren – insbesondere für Onboarding Dritte | WP12 | Niedrig/Mittel | Mittel | | WP13 | Mittel | Mittel | | WP14 | Mittel | Niedrig/Mittel | +| WP15 | Mittel | Hoch | +| WP16 | Mittel | Hoch | +| WP17 | Mittel | Mittel | --- @@ -569,6 +634,9 @@ Aufräumen, dokumentieren, stabilisieren – insbesondere für Onboarding Dritte | WP12 | 🟡 | | WP13 | 🟡 | | WP14 | 🟡 | +| WP15 | 🟢 | +| WP16 | 🟡 | +| WP17 | 🟡 | --- diff --git a/app/core/chunk_config.py b/app/core/chunk_config.py deleted file mode 100644 index 6780b5d..0000000 --- a/app/core/chunk_config.py +++ /dev/null @@ -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) diff --git a/app/core/chunker.py b/app/core/chunker.py index 9375a9b..9e0c5fa 100644 --- a/app/core/chunker.py +++ b/app/core/chunker.py @@ -1,226 +1,330 @@ from __future__ import annotations from dataclasses import dataclass -from typing import List, Dict, Optional, Tuple +from typing import List, Dict, Optional, Tuple, Any, Set import re import math +import yaml +from pathlib import Path from markdown_it import MarkdownIt from markdown_it.token import Token -from .chunk_config import get_sizes +import asyncio +import logging -# --- Hilfen --- -_SENT_SPLIT = re.compile(r'(?<=[.!?])\s+(?=[A-ZÄÖÜ0-9„(])') -_WS = re.compile(r'\s+') +# Services +from app.services.semantic_analyzer import get_semantic_analyzer + +# Core Imports +try: + from app.core.derive_edges import build_edges_for_note +except ImportError: + # Mock für Tests + def build_edges_for_note(note_id, chunks, note_level_references=None, include_note_scope_refs=False): return [] + +logger = logging.getLogger(__name__) + +# ========================================== +# 1. HELPER & CONFIG +# ========================================== + +BASE_DIR = Path(__file__).resolve().parent.parent.parent +CONFIG_PATH = BASE_DIR / "config" / "types.yaml" +DEFAULT_PROFILE = {"strategy": "sliding_window", "target": 400, "max": 600, "overlap": (50, 80)} +_CONFIG_CACHE = None + +def _load_yaml_config() -> Dict[str, Any]: + global _CONFIG_CACHE + if _CONFIG_CACHE is not None: return _CONFIG_CACHE + if not CONFIG_PATH.exists(): return {} + try: + with open(CONFIG_PATH, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + _CONFIG_CACHE = data + return data + except Exception: return {} + +def get_chunk_config(note_type: str) -> Dict[str, Any]: + full_config = _load_yaml_config() + profiles = full_config.get("chunking_profiles", {}) + type_def = full_config.get("types", {}).get(note_type.lower(), {}) + profile_name = type_def.get("chunking_profile") + if not profile_name: + profile_name = full_config.get("defaults", {}).get("chunking_profile", "sliding_standard") + + config = profiles.get(profile_name, DEFAULT_PROFILE).copy() + if "overlap" in config and isinstance(config["overlap"], list): + config["overlap"] = tuple(config["overlap"]) + return config + +def extract_frontmatter_from_text(md_text: str) -> Tuple[Dict[str, Any], str]: + fm_match = re.match(r'^\s*---\s*\n(.*?)\n---', md_text, re.DOTALL) + if not fm_match: return {}, md_text + try: + frontmatter = yaml.safe_load(fm_match.group(1)) + if not isinstance(frontmatter, dict): frontmatter = {} + except yaml.YAMLError: + frontmatter = {} + text_without_fm = re.sub(r'^\s*---\s*\n(.*?)\n---', '', md_text, flags=re.DOTALL) + return frontmatter, text_without_fm.strip() + +# ========================================== +# 2. DATA CLASSES +# ========================================== + +_SENT_SPLIT = re.compile(r'(?<=[.!?])\s+(?=[A-ZÄÖÜ0-9„(])'); _WS = re.compile(r'\s+') def estimate_tokens(text: str) -> int: - # leichte Approximation: 1 Token ≈ 4 Zeichen; robust + schnell - t = len(text.strip()) - return max(1, math.ceil(t / 4)) + return max(1, math.ceil(len(text.strip()) / 4)) def split_sentences(text: str) -> list[str]: text = _WS.sub(' ', text.strip()) - if not text: - return [] + if not text: return [] parts = _SENT_SPLIT.split(text) return [p.strip() for p in parts if p.strip()] @dataclass class RawBlock: - kind: str # "heading" | "paragraph" | "list" | "code" | "table" | "thematic_break" | "blockquote" - text: str - level: Optional[int] # heading level (2,3,...) or None - section_path: str # e.g., "/H2 Title/H3 Subtitle" + kind: str; text: str; level: Optional[int]; section_path: str; section_title: Optional[str] @dataclass class Chunk: - id: str - note_id: str - index: int - text: str - token_count: int - section_title: Optional[str] - section_path: str - neighbors_prev: Optional[str] - neighbors_next: Optional[str] - char_start: int - char_end: int + id: str; note_id: str; index: int; text: str; window: str; token_count: int + section_title: Optional[str]; section_path: str + neighbors_prev: Optional[str]; neighbors_next: Optional[str] + suggested_edges: Optional[List[str]] = None -# --- Markdown zu RawBlocks: H2/H3 als Sections, andere Blöcke gruppiert --- -def parse_blocks(md_text: str) -> List[RawBlock]: - md = MarkdownIt("commonmark").enable("table") - tokens: List[Token] = md.parse(md_text) +# ========================================== +# 3. PARSING & STRATEGIES (SYNCHRON) +# ========================================== - blocks: List[RawBlock] = [] - h2, h3 = None, None +def parse_blocks(md_text: str) -> Tuple[List[RawBlock], str]: + """Zerlegt Text in logische Blöcke (Absätze, Header).""" + blocks = [] + h1_title = "Dokument" section_path = "/" - cur_text = [] - cur_kind = None + current_h2 = None + + fm, text_without_fm = extract_frontmatter_from_text(md_text) + + h1_match = re.search(r'^#\s+(.*)', text_without_fm, re.MULTILINE) + if h1_match: + h1_title = h1_match.group(1).strip() - def push(kind: str, txt: str, lvl: Optional[int]): - nonlocal section_path - txt = txt.strip() - if not txt: - return - title = None - if kind == "heading" and lvl: - title = txt - blocks.append(RawBlock(kind=kind, text=txt, level=lvl, section_path=section_path)) - - i = 0 - while i < len(tokens): - t = tokens[i] - if t.type == "heading_open": - lvl = int(t.tag[1]) - # Sammle heading inline - i += 1 - title_txt = "" - while i < len(tokens) and tokens[i].type != "heading_close": - if tokens[i].type == "inline": - title_txt += tokens[i].content - i += 1 - title_txt = title_txt.strip() - # Section-Pfad aktualisieren - if lvl == 2: - h2, h3 = title_txt, None - section_path = f"/{h2}" - elif lvl == 3: - h3 = title_txt - section_path = f"/{h2}/{h3}" if h2 else f"/{h3}" - push("heading", title_txt, lvl) - elif t.type in ("paragraph_open", "bullet_list_open", "ordered_list_open", - "fence", "code_block", "blockquote_open", "table_open", "hr"): - kind = { - "paragraph_open": "paragraph", - "bullet_list_open": "list", - "ordered_list_open": "list", - "fence": "code", - "code_block": "code", - "blockquote_open": "blockquote", - "table_open": "table", - "hr": "thematic_break", - }[t.type] - - if t.type in ("fence", "code_block"): - # Codeblock hat eigenen content im selben Token - content = t.content or "" - push(kind, content, None) - else: - # inline sammeln bis close - content = "" - i += 1 - depth = 1 - while i < len(tokens) and depth > 0: - tk = tokens[i] - if tk.type.endswith("_open"): - depth += 1 - elif tk.type.endswith("_close"): - depth -= 1 - elif tk.type == "inline": - content += tk.content - i += 1 - push(kind, content, None) - continue # wir sind schon auf nächstem Token - i += 1 - - return blocks - -def assemble_chunks(note_id: str, md_text: str, note_type: str) -> List[Chunk]: - sizes = get_sizes(note_type) - target = sum(sizes["target"]) // 2 # mittlerer Zielwert - max_tokens = sizes["max"] - ov_min, ov_max = sizes["overlap"] - overlap = (ov_min + ov_max) // 2 - - blocks = parse_blocks(md_text) - - chunks: List[Chunk] = [] - buf: List[Tuple[str, str, str]] = [] # (text, section_title, section_path) - char_pos = 0 - - def flush_buffer(force=False): - nonlocal buf, chunks, char_pos - if not buf: - return - text = "\n\n".join([b[0] for b in buf]).strip() - if not text: - buf = [] - return - - # Wenn zu groß, satzbasiert weich umbrechen - toks = estimate_tokens(text) - if toks > max_tokens: - sentences = split_sentences(text) - cur = [] - cur_tokens = 0 - for s in sentences: - st = estimate_tokens(s) - if cur_tokens + st > target and cur: - _emit("\n".join(cur)) - # Overlap: letzte Sätze wiederverwenden - ov_text = " ".join(cur)[-overlap*4:] # 4 chars/token Heuristik - cur = [ov_text, s] if ov_text else [s] - cur_tokens = estimate_tokens(" ".join(cur)) - else: - cur.append(s) - cur_tokens += st - if cur: - _emit("\n".join(cur)) + lines = text_without_fm.split('\n') + buffer = [] + + for line in lines: + stripped = line.strip() + if stripped.startswith('# '): + continue + elif stripped.startswith('## '): + if buffer: + content = "\n".join(buffer).strip() + if content: + blocks.append(RawBlock("paragraph", content, None, section_path, current_h2)) + buffer = [] + current_h2 = stripped[3:].strip() + section_path = f"/{current_h2}" + blocks.append(RawBlock("heading", stripped, 2, section_path, current_h2)) + elif not stripped: + if buffer: + content = "\n".join(buffer).strip() + if content: + blocks.append(RawBlock("paragraph", content, None, section_path, current_h2)) + buffer = [] else: - _emit(text) + buffer.append(line) + + if buffer: + content = "\n".join(buffer).strip() + if content: + blocks.append(RawBlock("paragraph", content, None, section_path, current_h2)) + + return blocks, h1_title + +def _strategy_sliding_window(blocks: List[RawBlock], config: Dict[str, Any], note_id: str, doc_title: str = "", context_prefix: str = "") -> List[Chunk]: + target = config.get("target", 400) + max_tokens = config.get("max", 600) + overlap_val = config.get("overlap", (50, 80)) + overlap = sum(overlap_val) // 2 if isinstance(overlap_val, tuple) else overlap_val + chunks = []; buf = [] + + def _create_chunk(txt, win, sec, path): + idx = len(chunks) + chunks.append(Chunk( + id=f"{note_id}#c{idx:02d}", note_id=note_id, index=idx, + text=txt, window=win, token_count=estimate_tokens(txt), + section_title=sec, section_path=path, neighbors_prev=None, neighbors_next=None, + suggested_edges=[] + )) + + def flush_buffer(): + nonlocal buf + if not buf: return + + text_body = "\n\n".join([b.text for b in buf]) + win_body = f"{context_prefix}\n{text_body}".strip() if context_prefix else text_body + + if estimate_tokens(text_body) <= max_tokens: + _create_chunk(text_body, win_body, buf[-1].section_title, buf[-1].section_path) + else: + sentences = split_sentences(text_body) + current_chunk_sents = [] + current_len = 0 + + for sent in sentences: + sent_len = estimate_tokens(sent) + if current_len + sent_len > target and current_chunk_sents: + c_txt = " ".join(current_chunk_sents) + c_win = f"{context_prefix}\n{c_txt}".strip() if context_prefix else c_txt + _create_chunk(c_txt, c_win, buf[-1].section_title, buf[-1].section_path) + + overlap_sents = [] + ov_len = 0 + for s in reversed(current_chunk_sents): + if ov_len + estimate_tokens(s) < overlap: + overlap_sents.insert(0, s) + ov_len += estimate_tokens(s) + else: + break + + current_chunk_sents = list(overlap_sents) + current_chunk_sents.append(sent) + current_len = ov_len + sent_len + else: + current_chunk_sents.append(sent) + current_len += sent_len + + if current_chunk_sents: + c_txt = " ".join(current_chunk_sents) + c_win = f"{context_prefix}\n{c_txt}".strip() if context_prefix else c_txt + _create_chunk(c_txt, c_win, buf[-1].section_title, buf[-1].section_path) + buf = [] - def _emit(text_block: str): - nonlocal chunks, char_pos - idx = len(chunks) - chunk_id = f"{note_id}#c{idx:02d}" - token_count = estimate_tokens(text_block) - # section aus letztem buffer-entry ableiten - sec_title = buf[-1][1] if buf else None - sec_path = buf[-1][2] if buf else "/" - start = char_pos - end = start + len(text_block) - chunks.append(Chunk( - id=chunk_id, - note_id=note_id, - index=idx, - text=text_block, - token_count=token_count, - section_title=sec_title, - section_path=sec_path, - neighbors_prev=None, - neighbors_next=None, - char_start=start, - char_end=end - )) - char_pos = end + 1 - - # Blocks in Puffer sammeln; bei Überschreiten Zielbereich flushen - cur_sec_title = None for b in blocks: - if b.kind == "heading" and b.level in (2, 3): - # Sectionwechsel ⇒ Buffer flushen + if b.kind == "heading": continue + current_buf_text = "\n\n".join([x.text for x in buf]) + if estimate_tokens(current_buf_text) + estimate_tokens(b.text) >= target: flush_buffer() - cur_sec_title = b.text.strip() - # Heading selbst nicht als Chunk, aber als Kontexttitel nutzen - continue - - txt = b.text.strip() - if not txt: - continue - - tentative = "\n\n".join([*(x[0] for x in buf), txt]).strip() - if estimate_tokens(tentative) > max(get_sizes(note_type)["target"]): - # weicher Schnitt vor Hinzufügen - flush_buffer() - buf.append((txt, cur_sec_title, b.section_path)) - - # bei Erreichen ~Target flushen - if estimate_tokens("\n\n".join([x[0] for x in buf])) >= target: + buf.append(b) + if estimate_tokens(b.text) >= target: flush_buffer() - flush_buffer(force=True) + flush_buffer() + return chunks + +def _strategy_by_heading(blocks: List[RawBlock], config: Dict[str, Any], note_id: str, doc_title: str = "") -> List[Chunk]: + return _strategy_sliding_window(blocks, config, note_id, doc_title, context_prefix=f"# {doc_title}") + +# ========================================== +# 4. ORCHESTRATION (ASYNC) - WP-15 CORE +# ========================================== + +async def assemble_chunks(note_id: str, md_text: str, note_type: str, config: Optional[Dict] = None) -> List[Chunk]: + if config is None: + config = get_chunk_config(note_type) + + fm, body_text = extract_frontmatter_from_text(md_text) + note_status = fm.get("status", "").lower() + + primary_strategy = config.get("strategy", "sliding_window") + enable_smart_edges = config.get("enable_smart_edge_allocation", False) + + if enable_smart_edges and note_status in ["draft", "initial_gen"]: + logger.info(f"Chunker: Skipping Smart Edges for draft '{note_id}'.") + enable_smart_edges = False + + blocks, doc_title = parse_blocks(md_text) + + if primary_strategy == "by_heading": + chunks = await asyncio.to_thread(_strategy_by_heading, blocks, config, note_id, doc_title) + else: + chunks = await asyncio.to_thread(_strategy_sliding_window, blocks, config, note_id, doc_title) + + if not chunks: + return [] + + if enable_smart_edges: + # Hier rufen wir nun die Smart Edge Allocation auf + chunks = await _run_smart_edge_allocation(chunks, md_text, note_id, note_type) - # neighbors setzen for i, ch in enumerate(chunks): ch.neighbors_prev = chunks[i-1].id if i > 0 else None ch.neighbors_next = chunks[i+1].id if i < len(chunks)-1 else None + return chunks + +def _extract_all_edges_from_md(md_text: str, note_id: str, note_type: str) -> List[str]: + """ + Hilfsfunktion: Erstellt einen Dummy-Chunk für den gesamten Text und ruft + den Edge-Parser auf, um ALLE Kanten der Notiz zu finden. + """ + # 1. Dummy Chunk erstellen, der den gesamten Text enthält + # Das ist notwendig, da build_edges_for_note Kanten nur aus Chunks extrahiert. + dummy_chunk = { + "chunk_id": f"{note_id}#full", + "text": md_text, + "content": md_text, # Sicherstellen, dass der Parser Text findet + "window": md_text, + "type": note_type + } + + # 2. Aufruf des Parsers (Signatur-Fix!) + # derive_edges.py: build_edges_for_note(note_id, chunks, note_level_references=None, include_note_scope_refs=False) + raw_edges = build_edges_for_note( + note_id, + [dummy_chunk], + note_level_references=None, + include_note_scope_refs=False + ) + + # 3. Kanten extrahieren + all_candidates = set() + for e in raw_edges: + kind = e.get("kind") + target = e.get("target_id") + if target and kind not in ["belongs_to", "next", "prev", "backlink"]: + all_candidates.add(f"{kind}:{target}") + + return list(all_candidates) + +async def _run_smart_edge_allocation(chunks: List[Chunk], full_text: str, note_id: str, note_type: str) -> List[Chunk]: + analyzer = get_semantic_analyzer() + + # A. Alle potenziellen Kanten der Notiz sammeln (über den Dummy-Chunk Trick) + candidate_list = _extract_all_edges_from_md(full_text, note_id, note_type) + + if not candidate_list: + return chunks + + # B. LLM Filterung pro Chunk (Parallel) + tasks = [] + for chunk in chunks: + tasks.append(analyzer.assign_edges_to_chunk(chunk.text, candidate_list, note_type)) + + results_per_chunk = await asyncio.gather(*tasks) + + # C. Injection & Fallback + assigned_edges_global = set() + + for i, confirmed_edges in enumerate(results_per_chunk): + chunk = chunks[i] + chunk.suggested_edges = confirmed_edges + assigned_edges_global.update(confirmed_edges) + + if confirmed_edges: + injection_str = "\n" + " ".join([f"[[rel:{e.split(':')[0]}|{e.split(':')[1]}]]" for e in confirmed_edges if ':' in e]) + chunk.text += injection_str + chunk.window += injection_str + + # D. Fallback: Unassigned Kanten überall hin + unassigned = set(candidate_list) - assigned_edges_global + if unassigned: + fallback_str = "\n" + " ".join([f"[[rel:{e.split(':')[0]}|{e.split(':')[1]}]]" for e in unassigned if ':' in e]) + for chunk in chunks: + chunk.text += fallback_str + chunk.window += fallback_str + if chunk.suggested_edges is None: chunk.suggested_edges = [] + chunk.suggested_edges.extend(list(unassigned)) + + return chunks \ No newline at end of file diff --git a/app/core/ingestion.py b/app/core/ingestion.py index cd6b293..8035c5c 100644 --- a/app/core/ingestion.py +++ b/app/core/ingestion.py @@ -1,14 +1,13 @@ """ app/core/ingestion.py -Zentraler Service für die Transformation von Markdown-Dateien in Qdrant-Objekte (Notes, Chunks, Edges). -Dient als Shared Logic für: -1. CLI-Imports (scripts/import_markdown.py) -2. API-Uploads (WP-11) -Refactored for Async Embedding Support. +Zentraler Service für die Transformation von Markdown-Dateien in Qdrant-Objekte. +Version: 2.5.2 (Full Feature: Change Detection + Robust IO + Clean Config) """ import os import logging +import asyncio +import time from typing import Dict, List, Optional, Tuple, Any # Core Module Imports @@ -18,22 +17,14 @@ from app.core.parser import ( validate_required_frontmatter, ) from app.core.note_payload import make_note_payload -from app.core.chunker import assemble_chunks +from app.core.chunker import assemble_chunks, get_chunk_config from app.core.chunk_payload import make_chunk_payloads -# Fallback für Edges Import (Robustheit) +# Fallback für Edges try: from app.core.derive_edges import build_edges_for_note except ImportError: - try: - from app.core.derive_edges import derive_edges_for_note as build_edges_for_note - except ImportError: - try: - from app.core.edges import build_edges_for_note - except ImportError: - # Fallback Mock - logging.warning("Could not import edge derivation logic. Edges will be empty.") - def build_edges_for_note(*args, **kwargs): return [] + def build_edges_for_note(*args, **kwargs): return [] from app.core.qdrant import QdrantConfig, get_client, ensure_collections, ensure_payload_indexes from app.core.qdrant_points import ( @@ -43,30 +34,22 @@ from app.core.qdrant_points import ( upsert_batch, ) -# WICHTIG: Wir nutzen den API-Client für Embeddings (Async Support) from app.services.embeddings_client import EmbeddingsClient logger = logging.getLogger(__name__) -# --- Helper für Type-Registry --- +# --- Helper --- def load_type_registry(custom_path: Optional[str] = None) -> dict: import yaml path = custom_path or os.getenv("MINDNET_TYPES_FILE", "config/types.yaml") - if not os.path.exists(path): - if os.path.exists("types.yaml"): - path = "types.yaml" - else: - return {} + if not os.path.exists(path): return {} try: - with open(path, "r", encoding="utf-8") as f: - return yaml.safe_load(f) or {} - except Exception: - return {} + with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {} + except Exception: return {} def resolve_note_type(requested: Optional[str], reg: dict) -> str: types = reg.get("types", {}) - if requested and requested in types: - return requested + if requested and requested in types: return requested return "concept" def effective_chunk_profile(note_type: str, reg: dict) -> str: @@ -84,7 +67,6 @@ def effective_retriever_weight(note_type: str, reg: dict) -> float: class IngestionService: def __init__(self, collection_prefix: str = None): - # Prefix Logik vereinheitlichen env_prefix = os.getenv("COLLECTION_PREFIX", "mindnet") self.prefix = collection_prefix or env_prefix @@ -92,19 +74,14 @@ class IngestionService: self.cfg.prefix = self.prefix self.client = get_client(self.cfg) self.dim = self.cfg.dim - - # Registry laden self.registry = load_type_registry() - - # Embedding Service initialisieren (Async Client) self.embedder = EmbeddingsClient() - # Init DB Checks (Fehler abfangen, falls DB nicht erreichbar) try: ensure_collections(self.client, self.prefix, self.dim) ensure_payload_indexes(self.client, self.prefix) except Exception as e: - logger.warning(f"DB initialization warning: {e}") + logger.warning(f"DB init warning: {e}") async def process_file( self, @@ -119,7 +96,8 @@ class IngestionService: hash_normalize: str = "canonical" ) -> Dict[str, Any]: """ - Verarbeitet eine einzelne Datei (ASYNC Version). + Verarbeitet eine einzelne Datei (ASYNC). + Inklusive Change Detection (Hash-Check) gegen Qdrant. """ result = { "path": file_path, @@ -128,7 +106,7 @@ class IngestionService: "error": None } - # 1. Parse & Frontmatter + # 1. Parse & Frontmatter Validation try: parsed = read_markdown(file_path) if not parsed: @@ -169,7 +147,7 @@ class IngestionService: logger.error(f"Payload build failed: {e}") return {**result, "error": f"Payload build failed: {str(e)}"} - # 4. Change Detection + # 4. Change Detection (Das fehlende Stück!) old_payload = None if not force_replace: old_payload = self._fetch_note_payload(note_id) @@ -193,47 +171,38 @@ class IngestionService: # 5. Processing (Chunking, Embedding, Edges) try: body_text = getattr(parsed, "body", "") or "" - chunks = assemble_chunks(fm["id"], body_text, fm["type"]) + + # --- Config Loading (Clean) --- + chunk_config = get_chunk_config(note_type) + # Hier greift die Logik aus types.yaml (smart=True/False) + + chunks = await assemble_chunks(fm["id"], body_text, fm["type"], config=chunk_config) chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text) - # --- EMBEDDING FIX (ASYNC) --- + # Embedding vecs = [] if chunk_pls: texts = [c.get("window") or c.get("text") or "" for c in chunk_pls] try: - # Async Aufruf des Embedders (via Batch oder Loop) if hasattr(self.embedder, 'embed_documents'): vecs = await self.embedder.embed_documents(texts) else: - # Fallback Loop falls Client kein Batch unterstützt for t in texts: v = await self.embedder.embed_query(t) vecs.append(v) - - # Validierung der Dimensionen - if vecs and len(vecs) > 0: - dim_got = len(vecs[0]) - if dim_got != self.dim: - # Wirf keinen Fehler, aber logge Warnung. Qdrant Upsert wird failen wenn 0. - logger.warning(f"Vector dimension mismatch. Expected {self.dim}, got {dim_got}") - if dim_got == 0: - raise ValueError("Embedding returned empty vectors (Dim 0)") except Exception as e: - logger.error(f"Embedding generation failed: {e}") + logger.error(f"Embedding failed: {e}") raise RuntimeError(f"Embedding failed: {e}") # Edges - note_refs = note_pl.get("references") or [] - # Versuche flexible Signatur für Edges (V1 vs V2) try: edges = build_edges_for_note( note_id, chunk_pls, - note_level_references=note_refs, + note_level_references=note_pl.get("references", []), include_note_scope_refs=note_scope_refs ) except TypeError: - # Fallback für ältere Signatur edges = build_edges_for_note(note_id, chunk_pls) except Exception as e: @@ -251,7 +220,7 @@ class IngestionService: if chunk_pls and vecs: c_name, c_pts = points_for_chunks(self.prefix, chunk_pls, vecs) upsert_batch(self.client, c_name, c_pts) - + if edges: e_name, e_pts = points_for_edges(self.prefix, edges) upsert_batch(self.client, e_name, e_pts) @@ -268,7 +237,7 @@ class IngestionService: logger.error(f"Upsert failed: {e}", exc_info=True) return {**result, "error": f"DB Upsert failed: {e}"} - # --- Interne Qdrant Helper --- + # --- Qdrant Helper (Restored) --- def _fetch_note_payload(self, note_id: str) -> Optional[dict]: from qdrant_client.http import models as rest @@ -297,8 +266,7 @@ class IngestionService: for suffix in ["chunks", "edges"]: try: self.client.delete(collection_name=f"{self.prefix}_{suffix}", points_selector=selector) - except Exception: - pass + except Exception: pass async def create_from_text( self, @@ -309,35 +277,29 @@ class IngestionService: ) -> Dict[str, Any]: """ WP-11 Persistence API Entrypoint. - Schreibt Text in Vault und indiziert ihn sofort. """ - # 1. Zielordner target_dir = os.path.join(vault_root, folder) - try: - os.makedirs(target_dir, exist_ok=True) - except Exception as e: - return {"status": "error", "error": f"Could not create folder {target_dir}: {e}"} + os.makedirs(target_dir, exist_ok=True) - # 2. Dateiname - safe_filename = os.path.basename(filename) - if not safe_filename.endswith(".md"): - safe_filename += ".md" - file_path = os.path.join(target_dir, safe_filename) + file_path = os.path.join(target_dir, filename) - # 3. Schreiben try: + # Robust Write: Ensure Flush & Sync with open(file_path, "w", encoding="utf-8") as f: f.write(markdown_content) + f.flush() + os.fsync(f.fileno()) + + await asyncio.sleep(0.1) + logger.info(f"Written file to {file_path}") except Exception as e: - return {"status": "error", "error": f"Disk write failed at {file_path}: {str(e)}"} + return {"status": "error", "error": f"Disk write failed: {str(e)}"} - # 4. Indizieren (Async Aufruf!) - # Wir rufen process_file auf, das jetzt ASYNC ist return await self.process_file( file_path=file_path, vault_root=vault_root, apply=True, force_replace=True, - purge_before=True + purge_before=True ) \ No newline at end of file diff --git a/app/frontend/ui.py b/app/frontend/ui.py index 733bcb4..1752718 100644 --- a/app/frontend/ui.py +++ b/app/frontend/ui.py @@ -5,6 +5,7 @@ import os import json import re import yaml +import unicodedata from datetime import datetime from pathlib import Path from dotenv import load_dotenv @@ -23,7 +24,7 @@ timeout_setting = os.getenv("MINDNET_API_TIMEOUT") or os.getenv("MINDNET_LLM_TIM API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0 # --- PAGE SETUP --- -st.set_page_config(page_title="mindnet v2.3.10", page_icon="🧠", layout="wide") +st.set_page_config(page_title="mindnet v2.5", page_icon="🧠", layout="wide") # --- CSS STYLING --- st.markdown(""" @@ -53,15 +54,6 @@ st.markdown(""" background-color: white; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; } - - .suggestion-card { - border-left: 3px solid #1a73e8; - background-color: #ffffff; - padding: 10px; - margin-bottom: 8px; - border-radius: 4px; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); - } """, unsafe_allow_html=True) @@ -71,8 +63,18 @@ if "user_id" not in st.session_state: st.session_state.user_id = str(uuid.uuid4( # --- HELPER FUNCTIONS --- +def slugify(value): + if not value: return "" + value = str(value).lower() + replacements = {'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 'ß': 'ss', '&': 'und', '+': 'und'} + for k, v in replacements.items(): + value = value.replace(k, v) + + value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii') + value = re.sub(r'[^\w\s-]', '', value).strip() + return re.sub(r'[-\s]+', '-', value) + def normalize_meta_and_body(meta, body): - """Sanitizer: Stellt sicher, dass nur erlaubte Felder im Frontmatter bleiben.""" ALLOWED_KEYS = {"title", "type", "status", "tags", "id", "created", "updated", "aliases", "lang"} clean_meta = {} extra_content = [] @@ -99,7 +101,11 @@ def normalize_meta_and_body(meta, body): extra_content.append(f"## {header}\n{val}\n") if all_tags: - clean_meta["tags"] = list(set(all_tags)) + clean_tags = [] + for t in all_tags: + t_clean = str(t).replace("#", "").strip() + if t_clean: clean_tags.append(t_clean) + clean_meta["tags"] = list(set(clean_tags)) if extra_content: new_section = "\n".join(extra_content) @@ -110,37 +116,67 @@ def normalize_meta_and_body(meta, body): return clean_meta, final_body def parse_markdown_draft(full_text): - """Robustes Parsing + Sanitization.""" - clean_text = full_text + """ + HEALING PARSER: Repariert kaputten LLM Output (z.B. fehlendes schließendes '---'). + """ + clean_text = full_text.strip() - pattern_block = r"```(?:markdown|md)?\s*(.*?)\s*```" - match_block = re.search(pattern_block, full_text, re.DOTALL | re.IGNORECASE) + # 1. Code-Block Wrapper entfernen + pattern_block = r"```(?:markdown|md|yaml)?\s*(.*?)\s*```" + match_block = re.search(pattern_block, clean_text, re.DOTALL | re.IGNORECASE) if match_block: clean_text = match_block.group(1).strip() - parts = re.split(r"^---+\s*$", clean_text, maxsplit=2, flags=re.MULTILINE) - meta = {} body = clean_text + yaml_str = "" + # 2. Versuch A: Standard Split (Idealfall) + parts = re.split(r"^---+\s*$", clean_text, maxsplit=2, flags=re.MULTILINE) + if len(parts) >= 3: yaml_str = parts[1] - body_candidate = parts[2] + body = parts[2] + + # 3. Versuch B: Healing (Wenn LLM das schließende --- vergessen hat) + elif clean_text.startswith("---"): + # Wir suchen die erste Überschrift '#', da Frontmatter davor sein muss + # Pattern: Suche --- am Anfang, dann nimm alles bis zum ersten # am Zeilenanfang + fallback_match = re.search(r"^---\s*(.*?)(?=\n#)", clean_text, re.DOTALL | re.MULTILINE) + if fallback_match: + yaml_str = fallback_match.group(1) + # Der Body ist alles NACH dem YAML String (inklusive dem #) + body = clean_text.replace(f"---{yaml_str}", "", 1).strip() + + # 4. YAML Parsing + if yaml_str: + yaml_str_clean = yaml_str.replace("#", "") # Tags cleanen try: - parsed = yaml.safe_load(yaml_str) + parsed = yaml.safe_load(yaml_str_clean) if isinstance(parsed, dict): meta = parsed - body = body_candidate.strip() - except Exception: - pass - + except Exception as e: + print(f"YAML Parsing Warning: {e}") + + # Fallback: Titel aus H1 + if not meta.get("title"): + h1_match = re.search(r"^#\s+(.*)$", body, re.MULTILINE) + if h1_match: + meta["title"] = h1_match.group(1).strip() + + # Correction: type/status swap + if meta.get("type") == "draft": + meta["status"] = "draft" + meta["type"] = "experience" + return normalize_meta_and_body(meta, body) def build_markdown_doc(meta, body): """Baut das finale Dokument zusammen.""" if "id" not in meta or meta["id"] == "generated_on_save": - safe_title = re.sub(r'[^a-zA-Z0-9]', '-', meta.get('title', 'note')).lower()[:30] - meta["id"] = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}-{uuid.uuid4().hex[:4]}" + raw_title = meta.get('title', 'note') + clean_slug = slugify(raw_title)[:50] or "note" + meta["id"] = f"{datetime.now().strftime('%Y%m%d')}-{clean_slug}" meta["updated"] = datetime.now().strftime("%Y-%m-%d") @@ -189,7 +225,6 @@ def send_chat_message(message: str, top_k: int, explain: bool): return {"error": str(e)} def analyze_draft_text(text: str, n_type: str): - """Ruft den neuen Intelligence-Service (WP-11) auf.""" try: response = requests.post( INGEST_ANALYZE_ENDPOINT, @@ -202,12 +237,11 @@ def analyze_draft_text(text: str, n_type: str): return {"error": str(e)} def save_draft_to_vault(markdown_content: str, filename: str = None): - """Ruft den neuen Persistence-Service (WP-11) auf.""" try: response = requests.post( INGEST_SAVE_ENDPOINT, json={"markdown_content": markdown_content, "filename": filename}, - timeout=60 + timeout=API_TIMEOUT ) response.raise_for_status() return response.json() @@ -225,7 +259,7 @@ def submit_feedback(query_id, node_id, score, comment=None): def render_sidebar(): with st.sidebar: st.title("🧠 mindnet") - st.caption("v2.3.10 | Mode Switch Fix") + st.caption("v2.5 | Healing Parser") mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0) st.divider() st.subheader("⚙️ Settings") @@ -240,7 +274,6 @@ def render_sidebar(): return mode, top_k, explain def render_draft_editor(msg): - # Ensure ID Stability if "query_id" not in msg or not msg["query_id"]: msg["query_id"] = str(uuid.uuid4()) @@ -253,7 +286,7 @@ def render_draft_editor(msg): widget_body_key = f"{key_base}_widget_body" data_body_key = f"{key_base}_data_body" - # --- 1. INIT STATE (Nur beim allerersten Laden der Message) --- + # --- 1. INIT STATE --- if f"{key_base}_init" not in st.session_state: meta, body = parse_markdown_draft(msg["content"]) if "type" not in meta: meta["type"] = "default" @@ -261,26 +294,34 @@ def render_draft_editor(msg): tags = meta.get("tags", []) meta["tags_str"] = ", ".join(tags) if isinstance(tags, list) else str(tags) - # Persistent Data (Source of Truth) + # Persistent Data st.session_state[data_meta_key] = meta st.session_state[data_sugg_key] = [] st.session_state[data_body_key] = body.strip() + # Init Widgets Keys + st.session_state[f"{key_base}_wdg_title"] = meta["title"] + st.session_state[f"{key_base}_wdg_type"] = meta["type"] + st.session_state[f"{key_base}_wdg_tags"] = meta["tags_str"] + st.session_state[f"{key_base}_init"] = True - # --- 2. RESURRECTION FIX (WICHTIG!) --- - # Wenn wir vom Manuellen Editor zurückkommen, wurde der widget_key von Streamlit gelöscht. - # Wir müssen ihn aus dem persistenten data_body_key wiederherstellen. + # --- 2. RESURRECTION --- if widget_body_key not in st.session_state and data_body_key in st.session_state: st.session_state[widget_body_key] = st.session_state[data_body_key] # --- CALLBACKS --- + def _sync_meta(): + meta = st.session_state[data_meta_key] + meta["title"] = st.session_state.get(f"{key_base}_wdg_title", "") + meta["type"] = st.session_state.get(f"{key_base}_wdg_type", "default") + meta["tags_str"] = st.session_state.get(f"{key_base}_wdg_tags", "") + st.session_state[data_meta_key] = meta + def _sync_body(): - # Sync Widget -> Data (Source of Truth) st.session_state[data_body_key] = st.session_state[widget_body_key] def _insert_text(text_to_insert): - # Insert in Widget Key und Sync Data current = st.session_state.get(widget_body_key, "") new_text = f"{current}\n\n{text_to_insert}" st.session_state[widget_body_key] = new_text @@ -296,33 +337,23 @@ def render_draft_editor(msg): st.markdown(f'
', unsafe_allow_html=True) st.markdown("### 📝 Entwurf bearbeiten") - # Metadata Form meta_ref = st.session_state[data_meta_key] c1, c2 = st.columns([2, 1]) with c1: - # Auch hier Keys für Widgets nutzen, um Resets zu vermeiden - title_key = f"{key_base}_wdg_title" - if title_key not in st.session_state: st.session_state[title_key] = meta_ref["title"] - meta_ref["title"] = st.text_input("Titel", key=title_key) + st.text_input("Titel", key=f"{key_base}_wdg_title", on_change=_sync_meta) with c2: - known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle"] - curr = meta_ref["type"] - if curr not in known_types: known_types.append(curr) - type_key = f"{key_base}_wdg_type" - if type_key not in st.session_state: st.session_state[type_key] = meta_ref["type"] - meta_ref["type"] = st.selectbox("Typ", known_types, index=known_types.index(curr) if curr in known_types else 0, key=type_key) + known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle", "risk", "belief"] + curr_type = st.session_state.get(f"{key_base}_wdg_type", meta_ref["type"]) + if curr_type not in known_types: known_types.append(curr_type) + st.selectbox("Typ", known_types, key=f"{key_base}_wdg_type", on_change=_sync_meta) - tags_key = f"{key_base}_wdg_tags" - if tags_key not in st.session_state: st.session_state[tags_key] = meta_ref.get("tags_str", "") - meta_ref["tags_str"] = st.text_input("Tags", key=tags_key) + st.text_input("Tags", key=f"{key_base}_wdg_tags", on_change=_sync_meta) - # Tabs tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"]) # --- TAB 1: EDITOR --- with tab_edit: - # Hier kein 'value' Argument mehr, da wir den Key oben (Resurrection) initialisiert haben. st.text_area( "Body", key=widget_body_key, @@ -338,11 +369,11 @@ def render_draft_editor(msg): if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"): st.session_state[data_sugg_key] = [] - # Lese vom Widget (aktuell) oder Data (Fallback) text_to_analyze = st.session_state.get(widget_body_key, st.session_state.get(data_body_key, "")) - + current_doc_type = st.session_state.get(f"{key_base}_wdg_type", "concept") + with st.spinner("Analysiere..."): - analysis = analyze_draft_text(text_to_analyze, meta_ref["type"]) + analysis = analyze_draft_text(text_to_analyze, current_doc_type) if "error" in analysis: st.error(f"Fehler: {analysis['error']}") @@ -354,7 +385,6 @@ def render_draft_editor(msg): else: st.success(f"{len(suggestions)} Vorschläge gefunden.") - # Render List suggestions = st.session_state[data_sugg_key] if suggestions: current_text_state = st.session_state.get(widget_body_key, "") @@ -380,16 +410,24 @@ def render_draft_editor(msg): st.button("➕ Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,)) # --- TAB 3: SAVE --- - final_tags = [t.strip() for t in meta_ref["tags_str"].split(",") if t.strip()] + final_tags_str = st.session_state.get(f"{key_base}_wdg_tags", "") + final_tags = [t.strip() for t in final_tags_str.split(",") if t.strip()] + final_meta = { "id": "generated_on_save", - "type": meta_ref["type"], - "title": meta_ref["title"], + "type": st.session_state.get(f"{key_base}_wdg_type", "default"), + "title": st.session_state.get(f"{key_base}_wdg_title", "").strip(), "status": "draft", "tags": final_tags } - # Final Doc aus Data + final_body = st.session_state.get(widget_body_key, st.session_state[data_body_key]) + + if not final_meta["title"]: + h1_match = re.search(r"^#\s+(.*)$", final_body, re.MULTILINE) + if h1_match: + final_meta["title"] = h1_match.group(1).strip() + final_doc = build_markdown_doc(final_meta, final_body) with tab_view: @@ -403,7 +441,13 @@ def render_draft_editor(msg): with b1: if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"): with st.spinner("Speichere im Vault..."): - safe_title = re.sub(r'[^a-zA-Z0-9]', '-', meta_ref["title"]).lower()[:30] or "draft" + + raw_title = final_meta.get("title", "") + if not raw_title: + clean_body = re.sub(r"[#*_\[\]()]", "", final_body).strip() + raw_title = clean_body[:40] if clean_body else "draft" + + safe_title = slugify(raw_title)[:60] or "draft" fname = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md" result = save_draft_to_vault(final_doc, filename=fname) @@ -423,7 +467,6 @@ def render_chat_interface(top_k, explain): for idx, msg in enumerate(st.session_state.messages): with st.chat_message(msg["role"]): if msg["role"] == "assistant": - # Header intent = msg.get("intent", "UNKNOWN") src = msg.get("intent_source", "?") icon = {"EMPATHY":"❤️", "DECISION":"⚖️", "CODING":"💻", "FACT":"📚", "INTERVIEW":"📝"}.get(intent, "🧠") @@ -432,13 +475,11 @@ def render_chat_interface(top_k, explain): with st.expander("🐞 Debug Raw Payload", expanded=False): st.json(msg) - # Logic if intent == "INTERVIEW": render_draft_editor(msg) else: st.markdown(msg["content"]) - # Sources if "sources" in msg and msg["sources"]: for hit in msg["sources"]: with st.expander(f"📄 {hit.get('note_id', '?')} ({hit.get('total_score', 0):.2f})"): diff --git a/app/routers/chat.py b/app/routers/chat.py index 45cb679..598bd79 100644 --- a/app/routers/chat.py +++ b/app/routers/chat.py @@ -1,21 +1,15 @@ """ -app/routers/chat.py — RAG Endpunkt (WP-06 Hybrid Router + WP-07 Interview Mode) -Version: 2.4.0 (Interview Support) - -Features: -- Hybrid Intent Router (Keyword + LLM) -- Strategic Retrieval (Late Binding via Config) -- Interview Loop (Schema-driven Data Collection) -- Context Enrichment (Payload/Source Fallback) -- Data Flywheel (Feedback Logging Integration) +app/routers/chat.py — RAG Endpunkt +Version: 2.5.0 (Fix: Question Detection protects against False-Positive Interviews) """ from fastapi import APIRouter, HTTPException, Depends -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional import time import uuid import logging import yaml +import os from pathlib import Path from app.config import get_settings @@ -30,6 +24,7 @@ logger = logging.getLogger(__name__) # --- Helper: Config Loader --- _DECISION_CONFIG_CACHE = None +_TYPES_CONFIG_CACHE = None def _load_decision_config() -> Dict[str, Any]: settings = get_settings() @@ -51,12 +46,27 @@ def _load_decision_config() -> Dict[str, Any]: logger.error(f"Failed to load decision config: {e}") return default_config +def _load_types_config() -> Dict[str, Any]: + """Lädt die types.yaml für Keyword-Erkennung.""" + path = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml") + try: + with open(path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) or {} + except Exception: + return {} + def get_full_config() -> Dict[str, Any]: global _DECISION_CONFIG_CACHE if _DECISION_CONFIG_CACHE is None: _DECISION_CONFIG_CACHE = _load_decision_config() return _DECISION_CONFIG_CACHE +def get_types_config() -> Dict[str, Any]: + global _TYPES_CONFIG_CACHE + if _TYPES_CONFIG_CACHE is None: + _TYPES_CONFIG_CACHE = _load_types_config() + return _TYPES_CONFIG_CACHE + def get_decision_strategy(intent: str) -> Dict[str, Any]: config = get_full_config() strategies = config.get("strategies", {}) @@ -67,40 +77,40 @@ def get_decision_strategy(intent: str) -> Dict[str, Any]: def _detect_target_type(message: str, configured_schemas: Dict[str, Any]) -> str: """ Versucht zu erraten, welchen Notiz-Typ der User erstellen will. - Nutzt Keywords und Mappings. + Nutzt Keywords aus types.yaml UND Mappings. """ message_lower = message.lower() - # 1. Direkter Match mit Schema-Keys (z.B. "projekt", "entscheidung") - # Ignoriere 'default' hier + # 1. Check types.yaml detection_keywords (Priority!) + types_cfg = get_types_config() + types_def = types_cfg.get("types", {}) + + for type_name, type_data in types_def.items(): + keywords = type_data.get("detection_keywords", []) + for kw in keywords: + if kw.lower() in message_lower: + return type_name + + # 2. Direkter Match mit Schema-Keys for type_key in configured_schemas.keys(): - if type_key == "default": - continue + if type_key == "default": continue if type_key in message_lower: return type_key - # 2. Synonym-Mapping (Deutsch -> Schema Key) - # Dies verbessert die UX, falls User deutsche Begriffe nutzen + # 3. Synonym-Mapping (Legacy Fallback) synonyms = { - "projekt": "project", - "vorhaben": "project", - "entscheidung": "decision", - "beschluss": "decision", + "projekt": "project", "vorhaben": "project", + "entscheidung": "decision", "beschluss": "decision", "ziel": "goal", - "erfahrung": "experience", - "lektion": "experience", + "erfahrung": "experience", "lektion": "experience", "wert": "value", "prinzip": "principle", - "grundsatz": "principle", - "notiz": "default", - "idee": "default" + "notiz": "default", "idee": "default" } for term, schema_key in synonyms.items(): if term in message_lower: - # Prüfen, ob der gemappte Key auch konfiguriert ist - if schema_key in configured_schemas: - return schema_key + return schema_key return "default" @@ -126,7 +136,6 @@ def _build_enriched_context(hits: List[QueryHit]) -> str: ) title = hit.note_id or "Unbekannt" - # [FIX] Robustes Auslesen des Typs (Payload > Source > Unknown) payload = hit.payload or {} note_type = payload.get("type") or source.get("type", "unknown") note_type = str(note_type).upper() @@ -140,54 +149,77 @@ def _build_enriched_context(hits: List[QueryHit]) -> str: return "\n\n".join(context_parts) +def _is_question(query: str) -> bool: + """Prüft, ob der Input wahrscheinlich eine Frage ist.""" + q = query.strip().lower() + if "?" in q: return True + + # W-Fragen Indikatoren (falls User das ? vergisst) + starters = ["wer", "wie", "was", "wo", "wann", "warum", "weshalb", "wozu", "welche", "bist du", "entspricht"] + if any(q.startswith(s + " ") for s in starters): + return True + + return False + async def _classify_intent(query: str, llm: LLMService) -> tuple[str, str]: """ - Hybrid Router v3: - Gibt Tuple zurück: (Intent, Source) + Hybrid Router v5: + 1. Decision Keywords (Strategie) -> Prio 1 + 2. Type Keywords (Interview Trigger) -> Prio 2, ABER NUR WENN KEINE FRAGE! + 3. LLM (Fallback) -> Prio 3 """ config = get_full_config() strategies = config.get("strategies", {}) settings = config.get("settings", {}) query_lower = query.lower() - best_intent = None - max_match_length = 0 - # 1. FAST PATH: Keywords + # 1. FAST PATH A: Strategie Keywords (z.B. "Soll ich...") for intent_name, strategy in strategies.items(): if intent_name == "FACT": continue keywords = strategy.get("trigger_keywords", []) for k in keywords: if k.lower() in query_lower: - if len(k) > max_match_length: - max_match_length = len(k) - best_intent = intent_name + return intent_name, "Keyword (Strategy)" - if best_intent: - return best_intent, "Keyword (Fast Path)" + # 2. FAST PATH B: Type Keywords (z.B. "Projekt", "Werte") -> INTERVIEW + # FIX: Wir prüfen, ob es eine Frage ist. Fragen zu Typen sollen RAG (FACT/DECISION) sein, + # keine Interviews. Wir überlassen das dann dem LLM Router (Slow Path). + + if not _is_question(query_lower): + types_cfg = get_types_config() + types_def = types_cfg.get("types", {}) + + for type_name, type_data in types_def.items(): + keywords = type_data.get("detection_keywords", []) + for kw in keywords: + if kw.lower() in query_lower: + return "INTERVIEW", f"Keyword (Type: {type_name})" - # 2. SLOW PATH: LLM Router + # 3. SLOW PATH: LLM Router if settings.get("llm_fallback_enabled", False): - router_prompt_template = settings.get("llm_router_prompt", "") + # Nutze Prompts aus prompts.yaml (via LLM Service) + router_prompt_template = llm.prompts.get("router_prompt", "") + if router_prompt_template: prompt = router_prompt_template.replace("{query}", query) - logger.info("Keywords failed. Asking LLM for Intent...") + logger.info("Keywords failed (or Question detected). Asking LLM for Intent...") - raw_response = await llm.generate_raw_response(prompt) - - # Parsing logic - llm_output_upper = raw_response.upper() - found_intents = [] - for strat_key in strategies.keys(): - if strat_key in llm_output_upper: - found_intents.append(strat_key) - - if len(found_intents) == 1: - return found_intents[0], "LLM Router (Slow Path)" - elif len(found_intents) > 1: - return found_intents[0], f"LLM Ambiguous {found_intents}" - else: - return "FACT", "LLM Fallback (No Match)" + try: + # Nutze priority="realtime" für den Router, damit er nicht wartet + raw_response = await llm.generate_raw_response(prompt, priority="realtime") + llm_output_upper = raw_response.upper() + + # Zuerst INTERVIEW prüfen + if "INTERVIEW" in llm_output_upper or "CREATE" in llm_output_upper: + return "INTERVIEW", "LLM Router" + + for strat_key in strategies.keys(): + if strat_key in llm_output_upper: + return strat_key, "LLM Router" + + except Exception as e: + logger.error(f"Router LLM failed: {e}") return "FACT", "Default (No Match)" @@ -202,7 +234,7 @@ async def chat_endpoint( logger.info(f"Chat request [{query_id}]: {request.message[:50]}...") try: - # 1. Intent Detection (mit Source) + # 1. Intent Detection intent, intent_source = await _classify_intent(request.message, llm) logger.info(f"[{query_id}] Final Intent: {intent} via {intent_source}") @@ -210,57 +242,41 @@ async def chat_endpoint( strategy = get_decision_strategy(intent) prompt_key = strategy.get("prompt_template", "rag_template") - # --- SPLIT LOGIC: INTERVIEW vs. RAG --- - sources_hits = [] final_prompt = "" if intent == "INTERVIEW": - # --- WP-07: INTERVIEW MODE --- - # Kein Retrieval. Wir nutzen den Dialog-Kontext. + # --- INTERVIEW MODE --- + target_type = _detect_target_type(request.message, strategy.get("schemas", {})) - # 1. Schema Loading (Late Binding) - schemas = strategy.get("schemas", {}) - target_type = _detect_target_type(request.message, schemas) - active_schema = schemas.get(target_type, schemas.get("default")) + types_cfg = get_types_config() + type_def = types_cfg.get("types", {}).get(target_type, {}) + fields_list = type_def.get("schema", []) - logger.info(f"[{query_id}] Starting Interview for Type: {target_type}") - - # Robustes Schema-Parsing (Dict vs List) - if isinstance(active_schema, dict): - fields_list = active_schema.get("fields", []) - hint_str = active_schema.get("hint", "") - else: - fields_list = active_schema # Fallback falls nur Liste definiert - hint_str = "" - + if not fields_list: + configured_schemas = strategy.get("schemas", {}) + fallback_schema = configured_schemas.get(target_type, configured_schemas.get("default")) + if isinstance(fallback_schema, dict): + fields_list = fallback_schema.get("fields", []) + else: + fields_list = fallback_schema or [] + + logger.info(f"[{query_id}] Interview Type: {target_type}. Fields: {len(fields_list)}") fields_str = "\n- " + "\n- ".join(fields_list) - # 2. Context Logic - # Hinweis: In einer Stateless-API ist {context_str} idealerweise die History. - # Da ChatRequest (noch) kein History-Feld hat, nutzen wir einen Placeholder - # oder verlassen uns darauf, dass der Client die History im Prompt mitschickt - # (Streamlit Pattern: Appends history to prompt). - # Wir labeln es hier explizit. - context_str = "Bisheriger Verlauf (falls vorhanden): Siehe oben/unten." - - # 3. Prompt Assembly template = llm.prompts.get(prompt_key, "") - final_prompt = template.replace("{context_str}", context_str) \ + final_prompt = template.replace("{context_str}", "Dialogverlauf...") \ .replace("{query}", request.message) \ .replace("{target_type}", target_type) \ .replace("{schema_fields}", fields_str) \ - .replace("{schema_hint}", hint_str) - - # Keine Hits im Interview + .replace("{schema_hint}", "") sources_hits = [] else: - # --- WP-06: STANDARD RAG MODE --- + # --- RAG MODE --- inject_types = strategy.get("inject_types", []) prepend_instr = strategy.get("prepend_instruction", "") - # 2. Primary Retrieval query_req = QueryRequest( query=request.message, mode="hybrid", @@ -270,9 +286,7 @@ async def chat_endpoint( retrieve_result = await retriever.search(query_req) hits = retrieve_result.results - # 3. Strategic Retrieval (WP-06 Kernfeature) if inject_types: - logger.info(f"[{query_id}] Executing Strategic Retrieval for types: {inject_types}...") strategy_req = QueryRequest( query=request.message, mode="hybrid", @@ -281,19 +295,16 @@ async def chat_endpoint( explain=False ) strategy_result = await retriever.search(strategy_req) - existing_ids = {h.node_id for h in hits} for strat_hit in strategy_result.results: if strat_hit.node_id not in existing_ids: hits.append(strat_hit) - # 4. Context Building if not hits: context_str = "Keine relevanten Notizen gefunden." else: context_str = _build_enriched_context(hits) - # 5. Generation Setup template = llm.prompts.get(prompt_key, "{context_str}\n\n{query}") if prepend_instr: @@ -302,35 +313,29 @@ async def chat_endpoint( final_prompt = template.replace("{context_str}", context_str).replace("{query}", request.message) sources_hits = hits - # --- COMMON GENERATION --- - + # --- GENERATION --- system_prompt = llm.prompts.get("system_prompt", "") - logger.info(f"[{query_id}] Sending to LLM (Intent: {intent}, Template: {prompt_key})...") - - # System-Prompt separat übergeben - answer_text = await llm.generate_raw_response(prompt=final_prompt, system=system_prompt) + # Chat nutzt IMMER realtime priority + answer_text = await llm.generate_raw_response( + prompt=final_prompt, + system=system_prompt, + priority="realtime" + ) duration_ms = int((time.time() - start_time) * 1000) - # 6. Logging (Fire & Forget) + # Logging try: log_search( query_id=query_id, query_text=request.message, results=sources_hits, mode="interview" if intent == "INTERVIEW" else "chat_rag", - metadata={ - "intent": intent, - "intent_source": intent_source, - "generated_answer": answer_text, - "model": llm.settings.LLM_MODEL - } + metadata={"intent": intent, "source": intent_source} ) - except Exception as e: - logger.error(f"Logging failed: {e}") + except: pass - # 7. Response return ChatResponse( query_id=query_id, answer=answer_text, diff --git a/app/services/llm_service.py b/app/services/llm_service.py index 90dd5d8..bfbe343 100644 --- a/app/services/llm_service.py +++ b/app/services/llm_service.py @@ -1,80 +1,142 @@ """ -app/services/llm_service.py — LLM Client (Ollama) -Version: 0.2.1 (Fix: System Prompt Handling for Phi-3) +app/services/llm_service.py — LLM Client +Version: 2.8.0 (Configurable Concurrency Limit) """ import httpx import yaml import logging import os +import asyncio from pathlib import Path -from app.config import get_settings +from typing import Optional, Dict, Any, Literal logger = logging.getLogger(__name__) +class Settings: + OLLAMA_URL = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434") + LLM_TIMEOUT = float(os.getenv("MINDNET_LLM_TIMEOUT", 300.0)) + LLM_MODEL = os.getenv("MINDNET_LLM_MODEL", "phi3:mini") + PROMPTS_PATH = os.getenv("MINDNET_PROMPTS_PATH", "./config/prompts.yaml") + + # NEU: Konfigurierbares Limit für Hintergrund-Last + # Default auf 2 (konservativ), kann in .env erhöht werden. + BACKGROUND_LIMIT = int(os.getenv("MINDNET_LLM_BACKGROUND_LIMIT", "2")) + +def get_settings(): + return Settings() + class LLMService: + # GLOBALER SEMAPHOR (Lazy Initialization) + # Wir initialisieren ihn erst, wenn wir die Settings kennen. + _background_semaphore = None + def __init__(self): self.settings = get_settings() self.prompts = self._load_prompts() + # Initialisiere Semaphore einmalig auf Klassen-Ebene basierend auf Config + if LLMService._background_semaphore is None: + limit = self.settings.BACKGROUND_LIMIT + logger.info(f"🚦 LLMService: Initializing Background Semaphore with limit: {limit}") + LLMService._background_semaphore = asyncio.Semaphore(limit) + + self.timeout = httpx.Timeout(self.settings.LLM_TIMEOUT, connect=10.0) + self.client = httpx.AsyncClient( base_url=self.settings.OLLAMA_URL, - timeout=self.settings.LLM_TIMEOUT + timeout=self.timeout ) def _load_prompts(self) -> dict: path = Path(self.settings.PROMPTS_PATH) - if not path.exists(): - return {} + if not path.exists(): return {} try: - with open(path, "r", encoding="utf-8") as f: - return yaml.safe_load(f) + with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) except Exception as e: logger.error(f"Failed to load prompts: {e}") return {} - async def generate_raw_response(self, prompt: str, system: str = None) -> str: + async def generate_raw_response( + self, + prompt: str, + system: str = None, + force_json: bool = False, + max_retries: int = 0, + base_delay: float = 2.0, + priority: Literal["realtime", "background"] = "realtime" + ) -> str: """ - Führt einen LLM Call aus. - Unterstützt nun explizite System-Prompts für sauberes Templating. + Führt einen LLM Call aus. + priority="realtime": Chat (Sofort, keine Bremse). + priority="background": Import/Analyse (Gedrosselt durch Semaphore). """ - payload = { + + use_semaphore = (priority == "background") + + if use_semaphore and LLMService._background_semaphore: + async with LLMService._background_semaphore: + return await self._execute_request(prompt, system, force_json, max_retries, base_delay) + else: + # Realtime oder Fallback (falls Semaphore Init fehlschlug) + return await self._execute_request(prompt, system, force_json, max_retries, base_delay) + + async def _execute_request(self, prompt, system, force_json, max_retries, base_delay): + payload: Dict[str, Any] = { "model": self.settings.LLM_MODEL, "prompt": prompt, "stream": False, "options": { - # Temperature etwas höher für Empathie, niedriger für Code? - # Wir lassen es auf Standard, oder steuern es später via Config. - "temperature": 0.7, - "num_ctx": 2048 + "temperature": 0.1 if force_json else 0.7, + "num_ctx": 8192 } } - # WICHTIG: System-Prompt separat übergeben, damit Ollama formatiert + if force_json: + payload["format"] = "json" + if system: payload["system"] = system - try: - response = await self.client.post("/api/generate", json=payload) - if response.status_code != 200: - logger.error(f"Ollama Error ({response.status_code}): {response.text}") - return "Fehler bei der Generierung." - - data = response.json() - return data.get("response", "").strip() - - except Exception as e: - logger.error(f"LLM Raw Gen Error: {e}") - return "Interner LLM Fehler." + attempt = 0 + + while True: + try: + response = await self.client.post("/api/generate", json=payload) + + if response.status_code == 200: + data = response.json() + return data.get("response", "").strip() + else: + response.raise_for_status() + + except Exception as e: + attempt += 1 + if attempt > max_retries: + logger.error(f"LLM Final Error (Versuch {attempt}): {e}") + raise e + + wait_time = base_delay * (2 ** (attempt - 1)) + logger.warning(f"⚠️ LLM Retry ({attempt}/{max_retries}) in {wait_time}s: {e}") + await asyncio.sleep(wait_time) async def generate_rag_response(self, query: str, context_str: str) -> str: - """Legacy Support""" + """ + Chat-Wrapper: Immer Realtime. + """ system_prompt = self.prompts.get("system_prompt", "") rag_template = self.prompts.get("rag_template", "{context_str}\n\n{query}") + final_prompt = rag_template.format(context_str=context_str, query=query) - # Leite an die neue Methode weiter - return await self.generate_raw_response(final_prompt, system=system_prompt) + return await self.generate_raw_response( + final_prompt, + system=system_prompt, + max_retries=0, + force_json=False, + priority="realtime" + ) async def close(self): - await self.client.aclose() \ No newline at end of file + if self.client: + await self.client.aclose() \ No newline at end of file diff --git a/app/services/semantic_analyzer.py b/app/services/semantic_analyzer.py new file mode 100644 index 0000000..3a971d6 --- /dev/null +++ b/app/services/semantic_analyzer.py @@ -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 \ No newline at end of file diff --git a/config/decision_engine.yaml b/config/decision_engine.yaml index 406ec25..278f582 100644 --- a/config/decision_engine.yaml +++ b/config/decision_engine.yaml @@ -1,32 +1,31 @@ # config/decision_engine.yaml -# Steuerung der Decision Engine (WP-06 + WP-07) -# Hybrid-Modus: Keywords (Fast) + LLM Router (Smart Fallback) -version: 1.3 +# Steuerung der Decision Engine (Intent Recognition) +# Version: 2.4.0 (Clean Architecture: Generic Intents only) + +version: 1.4 settings: llm_fallback_enabled: true - # Few-Shot Prompting für bessere SLM-Performance - # Erweitert um INTERVIEW Beispiele + # Few-Shot Prompting für den LLM-Router (Slow Path) llm_router_prompt: | Du bist ein Klassifikator. Analysiere die Nachricht und wähle die passende Strategie. Antworte NUR mit dem Namen der Strategie. STRATEGIEN: - - INTERVIEW: User will Wissen strukturieren, Notizen anlegen, Projekte starten ("Neu", "Festhalten"). + - INTERVIEW: User will Wissen erfassen, Notizen anlegen oder Dinge festhalten. - DECISION: Rat, Strategie, Vor/Nachteile, "Soll ich". - - EMPATHY: Gefühle, Frust, Freude, Probleme, "Alles ist sinnlos", "Ich bin traurig". - - CODING: Code, Syntax, Programmierung, Python. + - EMPATHY: Gefühle, Frust, Freude, Probleme. + - CODING: Code, Syntax, Programmierung. - FACT: Wissen, Fakten, Definitionen. BEISPIELE: User: "Wie funktioniert Qdrant?" -> FACT User: "Soll ich Qdrant nutzen?" -> DECISION - User: "Ich möchte ein neues Projekt anlegen" -> INTERVIEW - User: "Lass uns eine Entscheidung festhalten" -> INTERVIEW + User: "Ich möchte etwas notieren" -> INTERVIEW + User: "Lass uns das festhalten" -> INTERVIEW User: "Schreibe ein Python Script" -> CODING User: "Alles ist grau und sinnlos" -> EMPATHY - User: "Mir geht es heute gut" -> EMPATHY NACHRICHT: "{query}" @@ -51,11 +50,9 @@ strategies: - "empfehlung" - "strategie" - "entscheidung" - - "wert" - - "prinzip" - - "vor- und nachteile" - "abwägung" - inject_types: ["value", "principle", "goal"] + - "vergleich" + inject_types: ["value", "principle", "goal", "risk"] prompt_template: "decision_template" prepend_instruction: | !!! ENTSCHEIDUNGS-MODUS !!! @@ -72,6 +69,7 @@ strategies: - "angst" - "nervt" - "überfordert" + - "müde" inject_types: ["experience", "belief", "profile"] prompt_template: "empathy_template" prepend_instruction: null @@ -88,56 +86,37 @@ strategies: - "syntax" - "json" - "yaml" + - "bash" inject_types: ["snippet", "reference", "source"] prompt_template: "technical_template" prepend_instruction: null - # 5. Interview / Datenerfassung (WP-07) + # 5. Interview / Datenerfassung + # HINWEIS: Spezifische Typen (Projekt, Ziel etc.) werden automatisch + # über die types.yaml erkannt. Hier stehen nur generische Trigger. INTERVIEW: - description: "Der User möchte strukturiertes Wissen erfassen (Projekt, Notiz, Idee)." + description: "Der User möchte Wissen erfassen." trigger_keywords: - "neue notiz" - - "neues projekt" - - "neue entscheidung" - - "neues ziel" + - "etwas notieren" - "festhalten" - - "entwurf erstellen" - - "interview" + - "erstellen" - "dokumentieren" + - "anlegen" + - "interview" - "erfassen" - "idee speichern" - inject_types: [] # Keine RAG-Suche, reiner Kontext-Dialog + - "draft" + inject_types: [] prompt_template: "interview_template" prepend_instruction: null - # LATE BINDING SCHEMAS: - # Definition der Pflichtfelder pro Typ (korrespondiert mit types.yaml) - # Wenn ein Typ hier fehlt, wird 'default' genutzt. + # Schemas: Hier nur der Fallback. + # Spezifische Schemas (Project, Experience) kommen jetzt aus types.yaml! schemas: default: - fields: ["Titel", "Thema/Inhalt", "Tags"] - hint: "Halte es einfach und übersichtlich." - - project: - fields: ["Titel", "Zielsetzung (Goal)", "Status (draft/active)", "Wichtige Stakeholder", "Nächste Schritte"] - hint: "Achte darauf, Abhängigkeiten zu anderen Projekten mit [[rel:depends_on]] zu erfragen." - - decision: - fields: ["Titel", "Kontext (Warum entscheiden wir?)", "Getroffene Entscheidung", "Betrachtete Alternativen", "Status (proposed/final)"] - hint: "Wichtig: Frage explizit nach den Gründen gegen die Alternativen." - - goal: - fields: ["Titel", "Zeitrahmen (Deadline)", "Messkriterien (KPIs)", "Verbundene Werte"] - hint: "Ziele sollten SMART formuliert sein." - - experience: - fields: ["Titel", "Situation (Kontext)", "Erkenntnis (Learning)", "Emotionale Keywords (für Empathie-Suche)"] - hint: "Fokussiere dich auf die persönliche Lektion." - - value: - fields: ["Titel (Name des Werts)", "Definition (Was bedeutet das für uns?)", "Anti-Beispiel (Was ist es nicht?)"] - hint: "Werte dienen als Entscheidungsgrundlage." - - principle: - fields: ["Titel", "Handlungsanweisung", "Begründung"] - hint: "Prinzipien sind härter als Werte." \ No newline at end of file + fields: + - "Titel" + - "Thema/Inhalt" + - "Tags" + hint: "Halte es einfach und übersichtlich." \ No newline at end of file diff --git a/config/prompts.yaml b/config/prompts.yaml index f192109..3a06df2 100644 --- a/config/prompts.yaml +++ b/config/prompts.yaml @@ -97,44 +97,66 @@ technical_template: | # --------------------------------------------------------- # 5. INTERVIEW: Der "One-Shot Extractor" (Performance Mode) # --------------------------------------------------------- - interview_template: | TASK: - Erstelle einen Markdown-Entwurf für eine Notiz vom Typ '{target_type}'. + Du bist ein professioneller Ghostwriter. Verwandle den "USER INPUT" in eine strukturierte Notiz vom Typ '{target_type}'. - SCHEMA (Inhaltliche Pflichtfelder für den Body): + STRUKTUR (Nutze EXAKT diese Überschriften): {schema_fields} USER INPUT: "{query}" - ANWEISUNG: - 1. Extrahiere Informationen aus dem Input. - 2. Generiere validen Markdown. + ANWEISUNG ZUM INHALT: + 1. Analysiere den Input genau. + 2. Schreibe die Inhalte unter die passenden Überschriften aus der STRUKTUR-Liste oben. + 3. STIL: Schreibe flüssig, professionell und in der Ich-Perspektive. Korrigiere Grammatikfehler, aber behalte den persönlichen Ton bei. + 4. Wenn Informationen für einen Abschnitt fehlen, schreibe nur: "[TODO: Ergänzen]". Erfinde nichts dazu. - OUTPUT REGELN (STRIKT BEACHTEN): - A. FRONTMATTER (YAML): - - Darf NUR folgende Felder enthalten: [type, status, title, tags]. - - Schreibe KEINE inhaltlichen Sätze (wie 'Situation', 'Ziel') in das YAML! - - Setze 'status: draft'. - - B. BODY (Markdown): - - Nutze für jedes Schema-Feld eine Markdown-Überschrift (## Feldname). - - Schreibe den Inhalt DARUNTER. - - Nutze "[TODO: Ergänzen]", wenn Infos fehlen. - - HINWEIS ZUM TYP: - {schema_hint} - - OUTPUT FORMAT BEISPIEL: - ```markdown + OUTPUT FORMAT (YAML + MARKDOWN): --- type: {target_type} status: draft - title: ... - tags: [...] + title: (Erstelle einen treffenden, kurzen Titel für den Inhalt) + tags: [Tag1, Tag2] --- - # Titel der Notiz - ## Erstes Schema Feld - Der Inhalt hier... \ No newline at end of file + # (Wiederhole den Titel hier) + + ## (Erster Begriff aus STRUKTUR) + (Text...) + + ## (Zweiter Begriff aus STRUKTUR) + (Text...) + + (usw.) + + +# --------------------------------------------------------- +# 6. EDGE_ALLOCATION: Kantenfilter (Intent: OFFLINE_FILTER) +# --------------------------------------------------------- +edge_allocation_template: | + TASK: + Du bist ein strikter Selektor. Du erhältst eine Liste von "Kandidaten-Kanten" (Strings). + Wähle jene aus, die inhaltlich im "Textabschnitt" vorkommen oder relevant sind. + + TEXTABSCHNITT: + """ + {chunk_text} + """ + + KANDIDATEN (Auswahl-Pool): + {edge_list} + + REGELN: + 1. Die Kanten haben das Format "typ:ziel". Der "typ" ist variabel und kann ALLES sein (z.B. uses, blocks, inspired_by, loves, etc.). + 2. Gib NUR die Strings aus der Kandidaten-Liste zurück, die zum Text passen. + 3. Erfinde KEINE neuen Kanten. Nutze exakt die Schreibweise aus der Liste. + 4. Antworte als flache JSON-Liste. + + BEISPIEL (Zur Demonstration der Logik): + Input Text: "Das Projekt Alpha scheitert, weil Budget fehlt." + Input Kandidaten: ["blocks:Projekt Alpha", "inspired_by:Buch der Weisen", "needs:Budget"] + Output: ["blocks:Projekt Alpha", "needs:Budget"] + + DEIN OUTPUT (JSON): \ No newline at end of file diff --git a/config/types.yaml b/config/types.yaml index 5c3604b..a3385e0 100644 --- a/config/types.yaml +++ b/config/types.yaml @@ -1,86 +1,201 @@ -version: 1.1 # Update auf v1.1 für Mindnet v2.4 +version: 2.4.0 # Optimized for Async Intelligence & Hybrid Router +# ============================================================================== +# 1. CHUNKING PROFILES +# ============================================================================== + +chunking_profiles: + + # A. SHORT & FAST + # Für Glossar, Tasks, Risiken. Kleine Schnipsel. + sliding_short: + strategy: sliding_window + enable_smart_edge_allocation: false + target: 200 + max: 350 + overlap: [30, 50] + + # B. STANDARD & FAST + # Der "Traktor": Robust für Quellen, Journal, Daily Logs. + sliding_standard: + strategy: sliding_window + enable_smart_edge_allocation: false + target: 450 + max: 650 + overlap: [50, 100] + + # C. SMART FLOW (Performance-Safe Mode) + # Für Konzepte, Projekte, Erfahrungen. + # HINWEIS: 'enable_smart_edge_allocation' ist vorerst FALSE, um Ollama + # bei der Generierung nicht zu überlasten. Später wieder aktivieren. + sliding_smart_edges: + strategy: sliding_window + enable_smart_edge_allocation: true + target: 400 + max: 600 + overlap: [50, 80] + + # D. SMART STRUCTURE + # Für Profile, Werte, Prinzipien. Trennt hart an Überschriften (H2). + structured_smart_edges: + strategy: by_heading + enable_smart_edge_allocation: true + split_level: 2 + max: 600 + target: 400 + overlap: [50, 80] + +# ============================================================================== +# 2. DEFAULTS +# ============================================================================== defaults: retriever_weight: 1.0 - chunk_profile: default + chunking_profile: sliding_standard edge_defaults: [] +# ============================================================================== +# 3. TYPE DEFINITIONS +# ============================================================================== + types: - # --- WISSENSBAUSTEINE --- - concept: - chunk_profile: medium - retriever_weight: 0.60 - edge_defaults: ["references", "related_to"] - source: - chunk_profile: short - retriever_weight: 0.50 - edge_defaults: [] # Quellen sind passiv - - glossary: - chunk_profile: short - retriever_weight: 0.40 - edge_defaults: ["related_to"] - - # --- IDENTITÄT & PERSÖNLICHKEIT (Decision Engine Core) --- - profile: - chunk_profile: long - retriever_weight: 0.70 - edge_defaults: ["references", "related_to"] - - value: - chunk_profile: short - retriever_weight: 1.00 # MAX: Werte stechen Fakten im Decision-Mode - edge_defaults: ["related_to"] - - principle: - chunk_profile: short - retriever_weight: 0.95 # Sehr hoch: Handlungsleitlinien - edge_defaults: ["derived_from", "references"] # Prinzipien leiten sich oft woraus ab - - belief: # NEU: Glaubenssätze für Empathie-Modus - chunk_profile: short - retriever_weight: 0.90 - edge_defaults: ["related_to"] + # --- KERNTYPEN (Hoch priorisiert & Smart) --- experience: - chunk_profile: medium + chunking_profile: sliding_smart_edges retriever_weight: 0.90 - edge_defaults: ["derived_from", "references"] # Erfahrungen haben einen Ursprung + edge_defaults: ["derived_from", "references"] + # Hybrid Classifier: Wenn diese Worte fallen, ist es eine Experience + detection_keywords: + - "passiert" + - "erlebt" + - "gefühl" + - "situation" + - "stolz" + - "geärgert" + - "reaktion" + - "moment" + - "konflikt" + # Ghostwriter Schema: Sprechende Anweisungen für besseren Textfluss + schema: + - "Situation (Was ist passiert?)" + - "Meine Reaktion (Was habe ich getan?)" + - "Ergebnis & Auswirkung" + - "Reflexion & Learning (Was lerne ich daraus?)" + + project: + chunking_profile: sliding_smart_edges + retriever_weight: 0.97 + edge_defaults: ["references", "depends_on"] + detection_keywords: + - "projekt" + - "vorhaben" + - "ziel ist" + - "meilenstein" + - "planen" + - "starten" + - "mission" + schema: + - "Mission & Zielsetzung" + - "Aktueller Status & Blockaden" + - "Nächste konkrete Schritte" + - "Stakeholder & Ressourcen" + + decision: + chunking_profile: structured_smart_edges + retriever_weight: 1.00 # MAX: Entscheidungen sind Gesetz + edge_defaults: ["caused_by", "references"] + detection_keywords: + - "entschieden" + - "wahl" + - "optionen" + - "alternativen" + - "beschluss" + - "adr" + schema: + - "Kontext & Problemstellung" + - "Betrachtete Optionen (Alternativen)" + - "Die Entscheidung" + - "Begründung (Warum diese Wahl?)" + + # --- PERSÖNLICHKEIT & IDENTITÄT --- + + value: + chunking_profile: structured_smart_edges + retriever_weight: 1.00 + edge_defaults: ["related_to"] + detection_keywords: ["wert", "wichtig ist", "moral", "ethik"] + schema: ["Definition", "Warum mir das wichtig ist", "Leitsätze für den Alltag"] + + principle: + chunking_profile: structured_smart_edges + retriever_weight: 0.95 + edge_defaults: ["derived_from", "references"] + detection_keywords: ["prinzip", "regel", "grundsatz", "leitlinie"] + schema: ["Das Prinzip", "Anwendung & Beispiele"] + + belief: + chunking_profile: sliding_short + retriever_weight: 0.90 + edge_defaults: ["related_to"] + detection_keywords: ["glaube", "überzeugung", "denke dass", "meinung"] + schema: ["Der Glaubenssatz", "Ursprung & Reflexion"] + + profile: + chunking_profile: structured_smart_edges + retriever_weight: 0.70 + edge_defaults: ["references", "related_to"] + schema: ["Rolle / Identität", "Fakten & Daten", "Historie"] + + # --- STRATEGIE & RISIKO --- - # --- STRATEGIE & ENTSCHEIDUNG --- goal: - chunk_profile: medium + chunking_profile: sliding_smart_edges retriever_weight: 0.95 edge_defaults: ["depends_on", "related_to"] + schema: ["Zielzustand", "Zeitrahmen & KPIs", "Motivation"] - decision: # ADRs (Architecture Decision Records) - chunk_profile: long # Entscheidungen brauchen oft viel Kontext (Begründung) - retriever_weight: 1.00 # MAX: Getroffene Entscheidungen sind Gesetz - edge_defaults: ["caused_by", "references"] # Entscheidungen haben Gründe - - risk: # NEU: Risikomanagement - chunk_profile: short + risk: + chunking_profile: sliding_short retriever_weight: 0.85 - edge_defaults: ["related_to", "blocks"] # Risiken blockieren ggf. Projekte + edge_defaults: ["related_to", "blocks"] + detection_keywords: ["risiko", "gefahr", "bedrohung", "problem", "angst"] + schema: ["Beschreibung des Risikos", "Mögliche Auswirkungen", "Gegenmaßnahmen"] - milestone: - chunk_profile: short - retriever_weight: 0.70 - edge_defaults: ["related_to", "part_of"] + # --- BASIS & WISSEN --- - # --- OPERATIV --- - project: - chunk_profile: long - retriever_weight: 0.97 # Projekte sind der Kontext für alles - edge_defaults: ["references", "depends_on"] + concept: + chunking_profile: sliding_smart_edges + retriever_weight: 0.60 + edge_defaults: ["references", "related_to"] + schema: + - "Definition" + - "Kontext & Hintergrund" + - "Verwandte Konzepte" task: - chunk_profile: short + chunking_profile: sliding_short retriever_weight: 0.80 edge_defaults: ["depends_on", "part_of"] + schema: ["Aufgabe", "Kontext", "Definition of Done"] journal: - chunk_profile: medium + chunking_profile: sliding_standard retriever_weight: 0.80 - edge_defaults: ["references", "related_to"] \ No newline at end of file + 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"] \ No newline at end of file diff --git a/docs/Knowledge_Design_Manual.md b/docs/Knowledge_Design_Manual.md index 47936cf..2938662 100644 --- a/docs/Knowledge_Design_Manual.md +++ b/docs/Knowledge_Design_Manual.md @@ -1,7 +1,7 @@ # mindnet v2.4 – Knowledge Design Manual -**Datei:** `docs/mindnet_knowledge_design_manual_v2.4.md` -**Stand:** 2025-12-11 -**Status:** **FINAL** (Integrierter Stand WP01–WP11) +**Datei:** `docs/mindnet_knowledge_design_manual_v2.6.md` +**Stand:** 2025-12-12 +**Status:** **FINAL** (Integrierter Stand WP01–WP15) **Quellen:** `knowledge_design.md`, `TYPE_REGISTRY_MANUAL.md`, `chunking_strategy.md`, `mindnet_functional_architecture.md`. --- @@ -24,9 +24,9 @@ Dieses Handbuch ist die **primäre Arbeitsanweisung** für dich als Mindmaster ( ### 1.1 Zielsetzung Mindnet ist mehr als eine Dokumentablage. Es ist ein vernetztes System, das deine Persönlichkeit, Entscheidungen und Erfahrungen abbildet. -Seit Version 2.4 verfügt Mindnet über: +Seit Version 2.6 verfügt Mindnet über: * **Hybrid Router:** Das System erkennt, ob du Fakten, Entscheidungen oder Empathie brauchst. -* **Context Intelligence:** Das System lädt je nach Situation unterschiedliche Notiz-Typen (z.B. Werte bei Entscheidungen). +* **Smart Edge Allocation:** Das System prüft deine Links intelligent und bindet sie nur an die Textabschnitte (Chunks), wo sie wirklich relevant sind. * **Web UI (WP10):** Du kannst direkt sehen, welche Quellen genutzt wurden. * **Active Intelligence (WP11):** Das System hilft dir beim Schreiben und Vernetzen (Link-Vorschläge). @@ -69,7 +69,7 @@ Diese Felder sind technisch nicht zwingend, aber für bestimmte Typen sinnvoll: **Die ID (Identifikator):** * Muss global eindeutig und **stabil** sein. * Darf sich nicht ändern, wenn die Datei umbenannt oder verschoben wird. -* **Empfehlung:** `YYYYMMDD-slug-hash` (z. B. `20231027-vektor-db-a1b2`). Dies garantiert Eindeutigkeit und Chronologie. +* **Empfehlung:** `YYYYMMDD-slug` (z. B. `20231027-vektor-db`). Dies garantiert Eindeutigkeit und Chronologie. **Dateinamen & Pfade:** * Pfade dienen der menschlichen Ordnung (Ordnerstruktur), sind für Mindnet aber sekundär. @@ -80,7 +80,7 @@ Diese Felder sind technisch nicht zwingend, aber für bestimmte Typen sinnvoll: ## 3. Note-Typen & Typ-Registry -Der `type` ist der wichtigste Hebel im Knowledge Design. Er steuert nicht nur das Gewicht bei der Suche, sondern seit WP-06 auch, **wann** eine Notiz aktiv in den Chat geholt wird ("Strategic Retrieval") und **welche Felder** im Interview abgefragt werden. +Der `type` ist der wichtigste Hebel im Knowledge Design. Er steuert nicht nur das Gewicht bei der Suche, sondern seit WP-06 auch, **wann** eine Notiz aktiv in den Chat geholt wird ("Strategic Retrieval") und seit WP-15, **ob Smart Edges aktiviert sind**. ### 3.1 Übersicht der Kern-Typen @@ -95,8 +95,8 @@ Mindnet unterscheidet verschiedene Wissensarten. Wähle den Typ, der die **Rolle | **`value`** | Ein persönlicher Wert oder ein Prinzip. | **DECISION** | Definition, Anti-Beispiel | Ziel für `based_on` | | **`principle`** | Handlungsleitlinie. | **DECISION** | Regel, Ausnahme | Quelle für `derived_from` | | **`goal`** | Ein strategisches Ziel (kurz- oder langfristig). | **DECISION** | Zeitrahmen, KPIs, Werte | Ziel für `related_to` | -| **`risk`** | **NEU:** Ein identifiziertes Risiko oder eine Gefahr. | **DECISION** | Auswirkung, Wahrscheinlichkeit | Quelle für `blocks` | -| **`belief`** | **NEU:** Glaubenssatz / Überzeugung. | **EMPATHY** | Ursprung, Mantra | - | +| **`risk`** | Ein identifiziertes Risiko oder eine Gefahr. | **DECISION** | Auswirkung, Wahrscheinlichkeit | Quelle für `blocks` | +| **`belief`** | Glaubenssatz / Überzeugung. | **EMPATHY** | Ursprung, Mantra | - | | **`person`** | Eine reale Person (Netzwerk, Autor). | **FACT** | Rolle, Kontext | - | | **`journal`** | Zeitbezogener Log-Eintrag, Daily Note. | **FACT** | Datum, Tags | - | | **`source`** | Externe Quelle (Buch, PDF, Artikel). | **FACT** | Autor, URL | - | @@ -107,13 +107,11 @@ Der `type` steuert im Hintergrund drei technische Mechanismen: 1. **`retriever_weight` (Wichtigkeit):** * Ein `concept` (0.6) wiegt weniger als ein `project` (0.97) oder eine `decision` (1.0). - * **Warum?** Bei einer Suche nach "Datenbank" soll Mindnet bevorzugt deine *Entscheidung* ("Warum wir X nutzen") anzeigen. 2. **`chunk_profile` (Textzerlegung):** * `journal` (short): Wird fein zerlegt. - * `project` (long): Längere Kontext-Fenster. -3. **`edge_defaults` (Automatische Vernetzung):** - * Mindnet ergänzt automatisch Kanten. - * Beispiel: Ein Link in einem `project` wird automatisch als `depends_on` (Abhängigkeit) interpretiert. + * `experience` (sliding_smart_edges): Wird intelligent analysiert. +3. **`enable_smart_edge_allocation` (WP15):** + * Wenn `true`: Das System analysiert jeden Abschnitt mit KI, um zu prüfen, ob verlinkte Themen dort wirklich relevant sind. Das erhöht die Präzision massiv. --- @@ -291,4 +289,4 @@ Wir vermeiden es, Logik in den Markdown-Dateien hart zu kodieren. ### 7.2 Was bedeutet das für dich? * Du kannst dich auf den Inhalt konzentrieren. -* Wenn wir in Zukunft (WP08) basierend auf Feedback lernen, dass "Projekte" noch wichtiger sind, ändern wir **eine Zeile** in der Konfiguration, und das gesamte System passt sich beim nächsten Import an. \ No newline at end of file +* 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. \ No newline at end of file diff --git a/docs/Overview.md b/docs/Overview.md index b7cc8f0..87c9e57 100644 --- a/docs/Overview.md +++ b/docs/Overview.md @@ -1,8 +1,8 @@ # Mindnet v2.4 – Overview & Einstieg -**Datei:** `docs/mindnet_overview_v2.4.md` -**Stand:** 2025-12-11 -**Status:** **FINAL** (Inkl. Async Intelligence & Editor) -**Version:** 2.4.0 +**Datei:** `docs/mindnet_overview_v2.6.md` +**Stand:** 2025-12-12 +**Status:** **FINAL** (Inkl. Smart Edges, Traffic Control & Healing UI) +**Version:** 2.6.0 --- @@ -28,14 +28,14 @@ Mindnet arbeitet auf drei Schichten, die aufeinander aufbauen: ### Ebene 1: Content (Das Gedächtnis) * **Quelle:** Dein lokaler Obsidian-Vault (Markdown). * **Funktion:** Speicherung von Fakten, Projekten und Logs. -* **Technik:** Async Import-Pipeline, Chunking, Vektor-Datenbank (Qdrant). -* **Status:** 🟢 Live (WP01–WP03, WP11). +* **Technik:** Async Import-Pipeline, Smart Chunking (LLM-gestützte Kantenzuweisung), Vektor-Datenbank (Qdrant). +* **Status:** 🟢 Live (WP01–WP03, WP11, WP15). ### Ebene 2: Semantik (Das Verstehen) * **Funktion:** Verknüpfung von isolierten Notizen zu einem Netzwerk. * **Logik:** "Projekt A *hängt ab von* Entscheidung B". * **Technik:** Hybrider Retriever (Graph + Nomic Embeddings), Explanation Engine. -* **Status:** 🟢 Live (WP04, WP11). +* **Status:** 🟢 Live (WP04, WP11, WP15). ### Ebene 3: Identität & Interaktion (Die Persönlichkeit) * **Funktion:** Interaktion, Bewertung und Co-Creation. @@ -43,11 +43,11 @@ Mindnet arbeitet auf drei Schichten, die aufeinander aufbauen: * "Ich empfehle Lösung X, weil sie unserem Wert 'Datensparsamkeit' entspricht." * "Ich sehe, du willst ein Projekt starten. Lass uns die Eckdaten erfassen." * **Technik:** - * **Intent Router:** Erkennt Absichten (Fakt vs. Gefühl vs. Entscheidung vs. Interview). - * **Strategic Retrieval:** Lädt gezielt Werte oder Erfahrungen nach. + * **Hybrid Router v5:** Erkennt Absichten (Frage vs. Befehl) und unterscheidet Objekte (`types.yaml`) von Handlungen (`decision_engine.yaml`). + * **Traffic Control:** Priorisiert Chat-Anfragen ("Realtime") vor Hintergrund-Jobs ("Background"). * **One-Shot Extraction:** Generiert Entwürfe für neue Notizen. * **Active Intelligence:** Schlägt Links während des Schreibens vor. -* **Status:** 🟢 Live (WP05–WP07, WP10). +* **Status:** 🟢 Live (WP05–WP07, WP10, WP15). --- @@ -57,8 +57,9 @@ Der Datenfluss in Mindnet ist zyklisch ("Data Flywheel"): 1. **Input:** Du schreibst Notizen in Obsidian **ODER** lässt sie von Mindnet im Chat entwerfen. 2. **Intelligence (Live):** Während du schreibst, analysiert Mindnet den Text und schlägt Verknüpfungen vor (Sliding Window Analyse). -3. **Ingest:** Ein asynchrones Python-Skript importiert, zerlegt (Chunking) und vernetzt (Edges) die Daten in Qdrant. -4. **Intent Recognition:** Der Router analysiert deine Frage: Willst du Fakten, Code, Empathie oder etwas dokumentieren? +3. **Ingest:** Ein asynchrones Python-Skript importiert und zerlegt die Daten. + * **Neu (Smart Edges):** Ein LLM analysiert jeden Textabschnitt und entscheidet, welche Kanten relevant sind. +4. **Intent Recognition:** Der Router analysiert deine Eingabe: Ist es eine Frage (RAG) oder ein Befehl (Interview)? 5. **Retrieval / Action:** * Bei Fragen: Das System sucht Inhalte passend zum Intent. * Bei Interviews: Das System wählt das passende Schema (z.B. Projekt-Vorlage). @@ -69,7 +70,8 @@ Der Datenfluss in Mindnet ist zyklisch ("Data Flywheel"): * **Backend:** Python 3.10+, FastAPI (Async). * **Datenbank:** Qdrant (Vektor & Graph, 768 Dim). * **KI:** Ollama (Phi-3 Mini für Chat, Nomic für Embeddings) – 100% lokal. -* **Frontend:** Streamlit Web-UI (v2.4). +* **Traffic Control:** Semaphore-Logik zur Lastverteilung zwischen Chat und Import. +* **Frontend:** Streamlit Web-UI (v2.5) mit Healing Parser. --- @@ -79,13 +81,13 @@ Wo findest du was? | Wenn du... | ...lies dieses Dokument | | :--- | :--- | -| **...wissen willst, wie man Notizen schreibt.** | `mindnet_knowledge_design_manual_v2.4.md` | -| **...das System installieren oder betreiben musst.** | `mindnet_admin_guide_v2.4.md` | -| **...am Python-Code entwickeln willst.** | `mindnet_developer_guide_v2.4.md` | -| **...die Pipeline (Import -> RAG) verstehen willst.** | `mindnet_pipeline_playbook_v2.4.md` | -| **...die genaue JSON-Struktur oder APIs suchst.** | `mindnet_technical_architecture.md` | -| **...verstehen willst, was fachlich passiert.** | `mindnet_functional_architecture.md` | -| **...den aktuellen Projektstatus suchst.** | `mindnet_appendices_v2.4.md` | +| **...wissen willst, wie man Notizen schreibt.** | `mindnet_knowledge_design_manual_v2.6.md` | +| **...das System installieren oder betreiben musst.** | `mindnet_admin_guide_v2.6.md` | +| **...am Python-Code entwickeln willst.** | `mindnet_developer_guide_v2.6.md` | +| **...die Pipeline (Import -> RAG) verstehen willst.** | `mindnet_pipeline_playbook_v2.6.md` | +| **...die genaue JSON-Struktur oder APIs suchst.** | `mindnet_technical_architecture_v2.6.md` | +| **...verstehen willst, was fachlich passiert.** | `mindnet_functional_architecture_v2.6.md` | +| **...den aktuellen Projektstatus suchst.** | `mindnet_appendices_v2.6.md` | --- @@ -99,5 +101,5 @@ Wo findest du was? ## 6. Aktueller Fokus -Wir haben den **Interview-Assistenten (WP07)** und die **Backend Intelligence (WP11)** erfolgreich integriert. -Das System kann nun aktiv helfen, Wissen zu strukturieren und zu vernetzen. Der Fokus verschiebt sich nun in Richtung **Self-Tuning (WP08)**, um aus dem gesammelten Feedback automatisch zu lernen. \ No newline at end of file +Wir haben die **Smart Edge Allocation (WP15)** und die **System-Stabilisierung (Traffic Control)** erfolgreich abgeschlossen. +Das System kann nun aktiv helfen, Wissen zu strukturieren, ohne dabei unter Last zusammenzubrechen. Der Fokus verschiebt sich nun in Richtung **Conversational Memory (WP17)** und **MCP Integration (WP13)**. \ No newline at end of file diff --git a/docs/admin_guide.md b/docs/admin_guide.md index ef87828..4f781f1 100644 --- a/docs/admin_guide.md +++ b/docs/admin_guide.md @@ -1,8 +1,8 @@ # Mindnet v2.4 – Admin Guide -**Datei:** `docs/mindnet_admin_guide_v2.4.md` -**Stand:** 2025-12-11 -**Status:** **FINAL** (Inkl. Async Architecture & Nomic Model) -**Quellen:** `Handbuch.md`, `mindnet_developer_guide_v2.4.md`. +**Datei:** `docs/mindnet_admin_guide_v2.6.md` +**Stand:** 2025-12-12 +**Status:** **FINAL** (Inkl. Traffic Control & Smart Edge Config) +**Quellen:** `Handbuch.md`, `mindnet_developer_guide_v2.6.md`. > Dieses Handbuch richtet sich an **Administratoren**. Es beschreibt Installation, Konfiguration, Backup-Strategien, Monitoring und den sicheren Betrieb der Mindnet-Instanz (API + UI + DB). @@ -55,7 +55,7 @@ Wir nutzen Qdrant als Vektor-Datenbank. Persistenz ist wichtig. ### 2.4 Ollama Setup (LLM & Embeddings) Mindnet benötigt einen lokalen LLM-Server für Chat UND Embeddings. -**WICHTIG (Update v2.3.10):** Es muss zwingend `nomic-embed-text` installiert sein, sonst startet der Import nicht. +**WICHTIG (Update v2.4):** Es muss zwingend `nomic-embed-text` installiert sein, sonst startet der Import nicht. # 1. Installieren (Linux Script) curl -fsSL https://ollama.com/install.sh | sh @@ -68,7 +68,7 @@ Mindnet benötigt einen lokalen LLM-Server für Chat UND Embeddings. curl http://localhost:11434/api/generate -d '{"model": "phi3:mini", "prompt":"Hi"}' ### 2.5 Konfiguration (ENV) -Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM` und `MINDNET_EMBEDDING_MODEL`. +Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM` und das neue `MINDNET_LLM_BACKGROUND_LIMIT`. # Server Config UVICORN_HOST=0.0.0.0 @@ -87,11 +87,15 @@ Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM` # AI Modelle (Ollama) MINDNET_OLLAMA_URL="http://127.0.0.1:11434" MINDNET_LLM_MODEL="phi3:mini" - MINDNET_EMBEDDING_MODEL="nomic-embed-text" # NEU + MINDNET_EMBEDDING_MODEL="nomic-embed-text" - # Timeouts (Erhöht für Async/Nomic) + # Timeouts (Erhöht für Async/Nomic/Smart Edges) MINDNET_LLM_TIMEOUT=300.0 - MINDNET_API_TIMEOUT=60.0 + MINDNET_API_TIMEOUT=300.0 # Wichtig: Frontend muss warten können + + # Traffic Control (Neu in v2.6) + # Begrenzt parallele LLM-Calls im Import, um Chat stabil zu halten. + MINDNET_LLM_BACKGROUND_LIMIT=2 # Configs MINDNET_PROMPTS_PATH="./config/prompts.yaml" @@ -155,12 +159,15 @@ Mindnet benötigt zwei Services pro Umgebung: API (Uvicorn) und UI (Streamlit). ## 3. Betrieb im Alltag ### 3.1 Regelmäßige Importe -Der Vault-Zustand sollte regelmäßig (z.B. stündlich per Cronjob) nach Qdrant synchronisiert werden. Das Skript nutzt nun **AsyncIO** und eine Semaphore, um Ollama nicht zu überlasten. +Der Vault-Zustand sollte regelmäßig (z.B. stündlich per Cronjob) nach Qdrant synchronisiert werden. Das Skript nutzt nun **AsyncIO**, **Smart Edges** und **Traffic Control**. **Cronjob-Beispiel (stündlich):** 0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1 +**Hinweis zu Smart Edges:** +Der Import dauert nun länger (ca. 2-3 Min pro Datei), da das LLM intensiv genutzt wird. Dank `MINDNET_LLM_BACKGROUND_LIMIT=2` bleibt der Server dabei aber für den Chat responsive. + ### 3.2 Health-Checks Prüfe regelmäßig, ob alle Komponenten laufen. @@ -176,7 +183,7 @@ Prüfe regelmäßig, ob alle Komponenten laufen. --- -## 4. Troubleshooting (Update v2.4) +## 4. Troubleshooting (Update v2.6) ### "Vector dimension error: expected dim: 768, got 384" * **Ursache:** Du versuchst, in eine alte Qdrant-Collection (mit 384 Dim aus v2.2) neue Embeddings (mit 768 Dim von Nomic) zu schreiben. @@ -184,15 +191,17 @@ Prüfe regelmäßig, ob alle Komponenten laufen. 1. `python -m scripts.reset_qdrant --mode wipe --prefix mindnet --yes` (Löscht DB). 2. `python -m scripts.import_markdown ...` (Baut neu auf). -### "500 Internal Server Error" beim Speichern -* **Ursache:** Oft Timeout bei Ollama, wenn `nomic-embed-text` noch nicht im RAM geladen ist ("Cold Start"). +### "500 Internal Server Error" beim Speichern/Chatten +* **Ursache:** Oft Timeout bei Ollama, wenn `nomic-embed-text` noch nicht im RAM geladen ist ("Cold Start") oder der Import die GPU blockiert. * **Lösung:** - 1. Sicherstellen, dass Modell existiert: `ollama list`. - 2. API neustarten (re-initialisiert Async Clients). + 1. `MINDNET_LLM_TIMEOUT` in `.env` auf `300.0` setzen. + 2. `MINDNET_LLM_BACKGROUND_LIMIT` auf `1` reduzieren (falls Hardware schwach). -### "NameError: name 'os' is not defined" -* **Ursache:** Fehlender Import in Skripten nach Updates. -* **Lösung:** `git pull` (Fix wurde in v2.3.10 deployed). +### Import ist sehr langsam +* **Ursache:** Smart Edges sind aktiv (`types.yaml`) und analysieren jeden Chunk. +* **Lösung:** + * Akzeptieren (für bessere Qualität). + * Oder für Massen-Initial-Import in `types.yaml` temporär `enable_smart_edge_allocation: false` setzen. --- @@ -215,7 +224,7 @@ Wenn die Datenbank korrupt ist oder Modelle gewechselt werden: # 1. DB komplett leeren (Wipe) python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes - # 2. Alles neu importieren + # 2. Alles neu importieren (Force re-calculates Hashes) python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force --- diff --git a/docs/appendix.md b/docs/appendix.md index 2244d5d..e437a88 100644 --- a/docs/appendix.md +++ b/docs/appendix.md @@ -1,7 +1,7 @@ # Mindnet v2.4 – Appendices & Referenzen -**Datei:** `docs/mindnet_appendices_v2.4.md` -**Stand:** 2025-12-11 -**Status:** **FINAL** (Integrierter Stand WP01–WP11) +**Datei:** `docs/mindnet_appendices_v2.6.md` +**Stand:** 2025-12-12 +**Status:** **FINAL** (Integrierter Stand WP01–WP15) **Quellen:** `TYPE_REGISTRY_MANUAL.md`, `chunking_strategy.md`, `mindnet_technical_architecture.md`, `Handbuch.md`. > Dieses Dokument bündelt Tabellen, Schemata und technische Referenzen, die in den Prozess-Dokumenten (Playbook, Guides) den Lesefluss stören würden. @@ -10,22 +10,27 @@ ## Anhang A: Typ-Registry Referenz (Default-Werte) -Diese Tabelle zeigt die Standard-Konfiguration der `types.yaml` (Stand v2.4). +Diese Tabelle zeigt die Standard-Konfiguration der `types.yaml` (Stand v2.6 / Config v1.6). -| Typ (`type`) | Chunk Profile | Retriever Weight | Edge Defaults (Auto-Kanten) | Beschreibung | +| Typ (`type`) | Chunk Profile | Retriever Weight | Smart Edges? | Beschreibung | | :--- | :--- | :--- | :--- | :--- | -| **concept** | `medium` | 0.60 | `references`, `related_to` | Abstrakte Begriffe, Theorien. | -| **project** | `long` | 0.97 | `references`, `depends_on` | Aktive Vorhaben. Hohe Priorität. | -| **decision** | `long` | 1.00 | `caused_by`, `references` | Entscheidungen (ADRs). Höchste Prio. | -| **experience** | `medium` | 0.90 | `derived_from`, `inspired_by` | Persönliche Learnings. | -| **journal** | `short` | 0.80 | `related_to` | Zeitgebundene Logs. Fein granular. | -| **person** | `short` | 0.50 | `related_to` | Personen-Profile. | -| **source** | `long` | 0.50 | *(keine)* | Externe Quellen (Bücher, PDFs). | -| **event** | `short` | 0.60 | `related_to` | Meetings, Konferenzen. | -| **value** | `medium` | 1.00 | `related_to` | Persönliche Werte/Prinzipien. | -| **goal** | `medium` | 0.95 | `depends_on` | Strategische Ziele. | -| **belief** | `medium` | 0.90 | `related_to` | Glaubenssätze. | -| **default** | `medium` | 1.00 | `references` | Fallback, wenn Typ unbekannt. | +| **concept** | `sliding_smart_edges` | 0.60 | **Ja** | Abstrakte Begriffe, Theorien. | +| **project** | `sliding_smart_edges` | 0.97 | **Ja** | Aktive Vorhaben. Hohe Priorität. | +| **decision** | `structured_smart_edges` | 1.00 | **Ja** | Entscheidungen (ADRs). Höchste Prio. | +| **experience** | `sliding_smart_edges` | 0.90 | **Ja** | Persönliche Learnings. Intensiv analysiert. | +| **journal** | `sliding_standard` | 0.80 | Nein | Zeitgebundene Logs. Performance-optimiert. | +| **person** | `sliding_standard` | 0.50 | Nein | Personen-Profile. | +| **source** | `sliding_standard` | 0.50 | Nein | Externe Quellen (Bücher, PDFs). | +| **event** | `sliding_standard` | 0.60 | Nein | Meetings, Konferenzen. | +| **value** | `structured_smart_edges` | 1.00 | **Ja** | Persönliche Werte/Prinzipien. | +| **principle** | `structured_smart_edges` | 1.00 | **Ja** | Handlungsleitlinien. | +| **profile** | `structured_smart_edges` | 0.80 | **Ja** | Eigene Identitäts-Beschreibungen. | +| **goal** | `sliding_standard` | 0.95 | Nein | Strategische Ziele. | +| **belief** | `sliding_short` | 0.90 | Nein | Glaubenssätze. | +| **risk** | `sliding_short` | 0.90 | Nein | Risiken & Gefahren. | +| **task** | `sliding_short` | 0.70 | Nein | Aufgaben. | +| **glossary** | `sliding_short` | 0.50 | Nein | Begriffsdefinitionen. | +| **default** | `sliding_standard` | 1.00 | Nein | Fallback, wenn Typ unbekannt. | --- @@ -43,6 +48,7 @@ Referenz aller implementierten Kantenarten (`kind`). | `similar_to` | Inline | Ja | Inhaltliche Ähnlichkeit. "Ist wie X". | | `caused_by` | Inline | Nein | Kausalität. "X ist der Grund für Y". | | `solves` | Inline | Nein | Lösung. "Tool X löst Problem Y". | +| `blocks` | Inline | Nein | **NEU:** Blockade. "Risiko X blockiert Projekt Y". | | `derived_from` | Matrix / Default | Nein | Herkunft. "Erkenntnis stammt aus Prinzip X". | | `based_on` | Matrix | Nein | Fundament. "Erfahrung basiert auf Wert Y". | | `uses` | Matrix | Nein | Nutzung. "Projekt nutzt Konzept Z". | @@ -60,7 +66,7 @@ Diese sind die Felder, die effektiv in Qdrant gespeichert werden. "title": "string (text)", // Titel aus Frontmatter "type": "string (keyword)", // Typ (z.B. 'project') "retriever_weight": "float", // Numerische Wichtigkeit (0.0-1.0) - "chunk_profile": "string", // Genutztes Profil (z.B. 'long') + "chunk_profile": "string", // Genutztes Profil (z.B. 'sliding_smart_edges') "edge_defaults": ["string"], // Liste der aktiven Default-Kanten "tags": ["string"], // Liste von Tags "created": "string (iso-date)", // Erstellungsdatum @@ -78,6 +84,7 @@ Diese sind die Felder, die effektiv in Qdrant gespeichert werden. "window": "string (text)", // Text + Overlap (für Embedding) "ord": "integer", // Laufende Nummer (1..N) "retriever_weight": "float", // Kopie aus Note (für Query-Speed) + "chunk_profile": "string", // Vererbt von Note "neighbors_prev": ["string"], // ID des Vorgängers "neighbors_next": ["string"] // ID des Nachfolgers } @@ -92,7 +99,8 @@ Diese sind die Felder, die effektiv in Qdrant gespeichert werden. "scope": "string (keyword)", // Immer 'chunk' "rule_id": "string (keyword)", // Traceability: 'inline:rel', 'explicit:wikilink' "confidence": "float", // 0.0 - 1.0 - "note_id": "string (keyword)" // Owner Note ID + "note_id": "string (keyword)", // Owner Note ID + "provenance": "keyword" // 'explicit', 'rule', 'smart' (NEU) } --- @@ -115,7 +123,8 @@ Diese Variablen steuern das Verhalten der Skripte und Container. | `MINDNET_EMBEDDING_MODEL` | `nomic-embed-text` | **NEU:** Name des Vektor-Modells. | | `MINDNET_OLLAMA_URL` | `http://127.0.0.1:11434`| URL zum LLM-Server (Neu in v2.2). | | `MINDNET_LLM_TIMEOUT` | `300.0` | Timeout für Ollama (Erhöht für CPU-Inference). | -| `MINDNET_API_TIMEOUT` | `60.0` | **NEU:** Frontend Timeout (Streamlit). | +| `MINDNET_API_TIMEOUT` | `300.0` | **NEU:** Frontend Timeout (Erhöht für Smart Edges). | +| `MINDNET_LLM_BACKGROUND_LIMIT`| `2` | **NEU:** Max. parallele Import-Tasks (Traffic Control). | | `MINDNET_VAULT_ROOT` | `./vault` | **NEU:** Pfad für Write-Back Operationen. | | `MINDNET_HASH_COMPARE` | `Body` | Vergleichsmodus für Import (`Body`, `Frontmatter`, `Full`). | | `MINDNET_HASH_SOURCE` | `parsed` | Quelle für Hash (`parsed`, `raw`, `file`). | @@ -129,16 +138,19 @@ Diese Variablen steuern das Verhalten der Skripte und Container. * **Decision Engine:** Komponente, die den Intent prüft und Strategien wählt (WP06). * **Draft Editor:** Web-Komponente zur Bearbeitung generierter Notizen (WP10a). * **Explanation Layer:** Komponente, die Scores und Graphen als Begründung liefert. +* **Healing Parser:** UI-Funktion, die defektes YAML von LLMs repariert (v2.5). * **Hybrid Router:** Kombination aus Keyword-Matching und LLM-Klassifizierung für Intents. * **Matrix Logic:** Regelwerk, das Kanten-Typen basierend auf Quell- und Ziel-Typ bestimmt. * **Nomic:** Das neue, hochpräzise Embedding-Modell (768 Dim). * **One-Shot Extractor:** LLM-Strategie zur sofortigen Generierung von Drafts ohne Rückfragen (WP07). * **RAG (Retrieval Augmented Generation):** Kombination aus Suche und Text-Generierung. * **Resurrection Pattern:** UI-Technik, um Eingaben bei Tab-Wechseln zu erhalten. +* **Smart Edge Allocation:** LLM-basierte Prüfung, ob ein Link zu einem Chunk passt (WP15). +* **Traffic Control:** Priorisierung von Chat-Anfragen ("Realtime") vor Import-Last ("Background") im LLM-Service (v2.6). --- -## Anhang F: Workpackage Status (v2.4.0) +## Anhang F: Workpackage Status (v2.6.0) Aktueller Implementierungsstand der Module. @@ -151,9 +163,12 @@ Aktueller Implementierungsstand der Module. | **WP04b**| Explanation Layer | 🟢 Live | API liefert Reasons & Breakdown. | | **WP04c**| Feedback Loop | 🟢 Live | Logging (JSONL) & Traceability aktiv. | | **WP05** | Persönlichkeit / Chat | 🟢 Live | RAG-Integration mit Context Enrichment. | -| **WP06** | Decision Engine | 🟢 Live | Intent-Router & Strategic Retrieval. | +| **WP06** | Decision Engine | 🟢 Live | Hybrid Router v5, Strategic Retrieval. | | **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** | | **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. | | **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). | -| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** | -| **WP11** | Backend Intelligence | 🟢 Live | **Async Core, Nomic, Matrix.** | \ No newline at end of file +| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI & Healing Parser.** | +| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic, Matrix.** | +| **WP15** | Smart Edge Allocation | 🟢 Live | **LLM-Filter & Traffic Control.** | +| **WP16** | Auto-Discovery | 🟡 Geplant | UX & Retrieval Tuning. | +| **WP17** | Conversational Memory | 🟡 Geplant | Dialog-Gedächtnis. | \ No newline at end of file diff --git a/docs/dev_workflow.md b/docs/dev_workflow.md index 806f19e..ae8b833 100644 --- a/docs/dev_workflow.md +++ b/docs/dev_workflow.md @@ -1,6 +1,6 @@ # Mindnet v2.4 – Entwickler-Workflow **Datei:** `docs/DEV_WORKFLOW.md` -**Stand:** 2025-12-11 (Aktualisiert: Inkl. Async Intelligence & Nomic) +**Stand:** 2025-12-12 (Aktualisiert: v2.6 mit Traffic Control) Dieses Handbuch beschreibt den Entwicklungszyklus zwischen **Windows PC** (IDE), **Raspberry Pi** (Gitea) und **Beelink** (Runtime/Server). @@ -35,14 +35,14 @@ Hier erstellst du die neue Funktion in einer sicheren Umgebung. 2. **Branch erstellen:** * Klicke wieder unten links auf `main`. * Wähle `+ Create new branch...`. - * Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp11-async-fix`). + * Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp15-traffic-control`). * Drücke **Enter**. 3. **Sicherheits-Check:** * Steht unten links jetzt dein Feature-Branch? **Nur dann darfst du Code ändern!** 4. **Coden:** - * Nimm deine Änderungen vor (z.B. neue Schemas in `decision_engine.yaml` oder Async-Logik in `ingestion.py`). + * Nimm deine Änderungen vor (z.B. neue Schemas in `types.yaml` oder Async-Logik in `ingestion.py`). 5. **Sichern & Hochladen:** * **Source Control** Icon (Gabel-Symbol) -> Nachricht eingeben -> **Commit**. @@ -64,15 +64,27 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft. ```bash git fetch # Tipp: 'git branch -r' zeigt alle verfügbaren Branches an - git checkout feature/wp11-async-fix + git checkout feature/wp15-traffic-control git pull ``` -4. **Umgebung vorbereiten (WICHTIG für v2.4):** +4. **Umgebung vorbereiten (WICHTIG für v2.6):** + Prüfe deine `.env` Datei. Sie benötigt jetzt Einträge für die Traffic Control. + ```bash + nano .env + ``` + **Ergänze/Prüfe:** + ```ini + # Traffic Control (Neu in v2.6) + MINDNET_LLM_BACKGROUND_LIMIT=2 # Max. parallele Import-Tasks + MINDNET_API_TIMEOUT=300.0 # Frontend wartet länger + MINDNET_EMBEDDING_MODEL="nomic-embed-text" + ``` + + **Dependencies aktualisieren:** ```bash source .venv/bin/activate - pip install -r requirements.txt # HTTPX usw. - # Sicherstellen, dass das neue Embedding-Modell da ist: + pip install -r requirements.txt ollama pull nomic-embed-text ``` @@ -102,18 +114,15 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft. uvicorn app.main:app --host 0.0.0.0 --port 8002 --env-file .env ``` -6. **Validieren (Smoke Tests):** +6. **Validieren (Smoke Tests v2.6):** - * **Browser:** Öffne `http://:8502` um die UI zu testen (Intent Badge prüfen!). - * **CLI:** Führe Testskripte in einem **zweiten Terminal** aus: - - **Test A: Intelligence / Aliases (Neu in WP11)** - ```bash - python debug_analysis.py - # Erwartung: "✅ ALIAS GEFUNDEN" - ``` + * **Test A: Last-Test (Traffic Control):** + 1. Starte einen Import im Terminal: `python3 -m scripts.import_markdown ...` + 2. Öffne **gleichzeitig** `http://:8502` im Browser. + 3. Stelle eine Chat-Frage ("Was ist Mindnet?"). + 4. **Erwartung:** Der Chat antwortet sofort (Realtime Lane), während der Import im Hintergrund weiterläuft (Background Lane). - **Test B: API Check** + * **Test B: API Check** ```bash curl -X POST "http://localhost:8002/ingest/analyze" -d '{"text": "mindnet", "type": "journal"}' ``` @@ -147,6 +156,9 @@ Jetzt bringen wir die Änderung in das Live-System (Port 8001 / 8501). pip install -r requirements.txt ollama pull nomic-embed-text + # .env prüfen (Traffic Control) + # Ggf. MINDNET_LLM_BACKGROUND_LIMIT=2 hinzufügen + # Falls sich die Vektor-Dimension geändert hat (v2.4 Upgrade): # python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes # python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force @@ -168,7 +180,7 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch. cd ~/mindnet_dev git checkout main git pull - git branch -d feature/wp11-async-fix + git branch -d feature/wp15-traffic-control ``` 3. **VS Code:** * Auf `main` wechseln. @@ -190,15 +202,15 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch. ## 4. Troubleshooting -**"Vector dimension error: expected 768, got 384"** -* **Ursache:** Du hast `nomic-embed-text` (768) aktiviert, aber die DB ist noch alt (384). -* **Lösung:** `scripts.reset_qdrant` ausführen und neu importieren. +**"Read timed out" im Frontend** +* **Ursache:** Backend braucht für Smart Edges länger als 60s. +* **Lösung:** `MINDNET_API_TIMEOUT=300.0` in `.env` setzen und Services neustarten. -**"Read timed out (300s)" / 500 Error beim Interview** -* **Ursache:** Das LLM (Ollama) braucht für den One-Shot Draft länger als das Timeout erlaubt. +**Import ist extrem langsam** +* **Ursache:** Smart Edges analysieren jeden Chunk mit LLM. * **Lösung:** - 1. Erhöhe in `.env` den Wert: `MINDNET_LLM_TIMEOUT=300.0`. - 2. Starte die Server neu. + * Akzeptieren (Qualität vor Speed). + * Oder temporär in `config/types.yaml`: `enable_smart_edge_allocation: false`. **"UnicodeDecodeError in .env"** * **Ursache:** Umlaute oder Sonderzeichen in der `.env` Datei. diff --git a/docs/developer_guide.md b/docs/developer_guide.md index ba6abf2..f593d74 100644 --- a/docs/developer_guide.md +++ b/docs/developer_guide.md @@ -1,7 +1,7 @@ # Mindnet v2.4 – Developer Guide -**Datei:** `docs/mindnet_developer_guide_v2.4.md` -**Stand:** 2025-12-11 -**Status:** **FINAL** (Inkl. Async Core, Nomic & Frontend State) +**Datei:** `docs/mindnet_developer_guide_v2.6.md` +**Stand:** 2025-12-12 +**Status:** **FINAL** (Inkl. Async Core, Nomic, Traffic Control & Frontend State) **Quellen:** `mindnet_technical_architecture.md`, `Handbuch.md`, `DEV_WORKFLOW.md`. > **Zielgruppe:** Entwickler:innen. @@ -9,7 +9,7 @@ --- - [Mindnet v2.4 – Developer Guide](#mindnet-v24--developer-guide) - - [1. Projektstruktur (Post-WP10)](#1-projektstruktur-post-wp10) + - [1. Projektstruktur (Post-WP15)](#1-projektstruktur-post-wp15) - [2. Lokales Setup (Development)](#2-lokales-setup-development) - [2.1 Voraussetzungen](#21-voraussetzungen) - [2.2 Installation](#22-installation) @@ -21,6 +21,7 @@ - [3.3 Der Retriever (`app.core.retriever`)](#33-der-retriever-appcoreretriever) - [3.4 Das Frontend (`app.frontend.ui`)](#34-das-frontend-appfrontendui) - [3.5 Embedding Service (`app.services.embeddings_client`)](#35-embedding-service-appservicesembeddings_client) + - [3.6 Traffic Control (`app.services.llm_service`)](#36-traffic-control-appservicesllm_service) - [4. Tests \& Debugging](#4-tests--debugging) - [4.1 Unit Tests (Pytest)](#41-unit-tests-pytest) - [4.2 Integration / Pipeline Tests](#42-integration--pipeline-tests) @@ -34,15 +35,15 @@ --- -## 1. Projektstruktur (Post-WP10) +## 1. Projektstruktur (Post-WP15) Der Code ist modular in `app` (Logik), `scripts` (CLI) und `config` (Steuerung) getrennt. mindnet/ ├── app/ │ ├── core/ # Kernlogik - │ │ ├── ingestion.py # NEU: Async Ingestion Service (WP11) - │ │ ├── chunker.py # Text-Zerlegung + │ │ ├── ingestion.py # NEU: Async Ingestion Service mit Change Detection + │ │ ├── chunker.py # Smart Chunker Orchestrator │ │ ├── derive_edges.py # Edge-Erzeugung (WP03 Logik) │ │ ├── retriever.py # Scoring & Hybrid Search │ │ ├── qdrant.py # DB-Verbindung @@ -52,21 +53,22 @@ Der Code ist modular in `app` (Logik), `scripts` (CLI) und `config` (Steuerung) │ ├── routers/ # FastAPI Endpoints │ │ ├── query.py # Suche │ │ ├── ingest.py # NEU: Save/Analyze (WP11) - │ │ ├── chat.py # Hybrid Router & Interview Logic (WP06/WP07) + │ │ ├── chat.py # Hybrid Router v5 & Interview Logic │ │ ├── feedback.py # Feedback (WP04c) │ │ └── ... │ ├── services/ # Interne & Externe Dienste - │ │ ├── llm_service.py # Ollama Client (Mit Timeout & Raw-Mode) - │ │ ├── embeddings_client.py# NEU: Async Embeddings (HTTPX) + │ │ ├── llm_service.py # Ollama Client mit Traffic Control + │ │ ├── semantic_analyzer.py# NEU: LLM-Filter für Edges (WP15) + │ │ ├── embeddings_client.py# Async Embeddings (HTTPX) │ │ ├── feedback_service.py # Logging (JSONL Writer) │ │ └── discovery.py # NEU: Intelligence Logic (WP11) │ ├── frontend/ # NEU (WP10) - │ │ └── ui.py # Streamlit Application inkl. Draft-Editor + │ │ └── ui.py # Streamlit Application inkl. Healing Parser │ └── main.py # Entrypoint der API ├── config/ # YAML-Konfigurationen (Single Source of Truth) - │ ├── types.yaml # Import-Regeln - │ ├── prompts.yaml # LLM Prompts & Interview Templates (WP06/07) - │ ├── decision_engine.yaml # Router-Strategien & Schemas (WP06/07) + │ ├── types.yaml # Import-Regeln & Smart-Edge Config + │ ├── prompts.yaml # LLM Prompts & Interview Templates + │ ├── decision_engine.yaml # Router-Strategien (Actions only) │ └── retriever.yaml # Scoring-Regeln & Kantengewichte ├── data/ │ └── logs/ # Lokale Logs (search_history.jsonl, feedback.jsonl) @@ -121,12 +123,13 @@ Erstelle eine `.env` Datei im Root-Verzeichnis. MINDNET_LLM_MODEL="phi3:mini" MINDNET_EMBEDDING_MODEL="nomic-embed-text" # NEU MINDNET_OLLAMA_URL="http://127.0.0.1:11434" - MINDNET_LLM_TIMEOUT=300.0 + MINDNET_LLM_TIMEOUT=300.0 # Timeout für CPU-Inference Cold-Starts MINDNET_DECISION_CONFIG="./config/decision_engine.yaml" + MINDNET_LLM_BACKGROUND_LIMIT=2 # NEU: Limit für parallele Import-Tasks # Frontend Settings (WP10) MINDNET_API_URL="http://localhost:8002" - MINDNET_API_TIMEOUT=60.0 + MINDNET_API_TIMEOUT=300.0 # Erhöht wegen Smart Edge Berechnung # Import-Strategie MINDNET_HASH_COMPARE="Body" @@ -154,15 +157,15 @@ Wir entwickeln mit zwei Services. Du kannst sie manuell in zwei Terminals starte ### 3.1 Der Importer (`scripts.import_markdown`) Dies ist das komplexeste Modul. * **Einstieg:** `scripts/import_markdown.py` -> `main_async()`. -* **Async & Semaphore:** Das Skript nutzt nun `asyncio` und eine Semaphore (Limit: 5), um parallele Embeddings zu erzeugen, ohne Ollama zu überlasten. +* **Smart Edges:** Nutzt `app.core.chunker` und `app.services.semantic_analyzer` zur Kanten-Filterung. * **Idempotenz:** Der Importer muss mehrfach laufen können, ohne Duplikate zu erzeugen. Wir nutzen deterministische IDs (UUIDv5). -* **Debugging:** Nutze `--dry-run` oder `scripts/payload_dryrun.py`. +* **Robustheit:** In `ingestion.py` sind Mechanismen wie Change Detection und Robust File I/O (fsync) implementiert. ### 3.2 Der Hybrid Router (`app.routers.chat`) Hier liegt die Logik für Intent Detection (WP06) und Interview-Modus (WP07). -* **Logic:** `_classify_intent` prüft zuerst Keywords (Fast Path) und fällt auf `llm_service.generate_raw_response` zurück (Slow Path), wenn konfiguriert. -* **One-Shot:** Wenn Intent `INTERVIEW` erkannt wird, wird **kein Retrieval** ausgeführt. Stattdessen wird ein Draft generiert. -* **Erweiterung:** Um neue Intents hinzuzufügen, editiere nur die YAML, nicht den Python-Code (Late Binding). +* **Question Detection:** Prüft zuerst, ob der Input eine Frage ist. Falls ja -> RAG. +* **Keyword Match:** Prüft Keywords in `decision_engine.yaml` und `types.yaml`. +* **Priority:** Ruft `llm_service` mit `priority="realtime"` auf. ### 3.3 Der Retriever (`app.core.retriever`) Hier passiert das Scoring. @@ -172,14 +175,20 @@ Hier passiert das Scoring. ### 3.4 Das Frontend (`app.frontend.ui`) Eine Streamlit-App (WP10). * **Resurrection Pattern:** Das UI nutzt ein spezielles State-Management, um Eingaben bei Tab-Wechseln (Chat <-> Editor) zu erhalten. Widgets synchronisieren sich mit `st.session_state`. -* **Draft Editor:** Enthält einen YAML-Sanitizer (`normalize_meta_and_body`), der sicherstellt, dass LLM-Halluzinationen im Frontmatter nicht das File zerstören. -* **Logik:** Ruft `/chat` und `/feedback` und `/ingest/analyze` Endpoints der API auf. +* **Healing Parser:** Die Funktion `parse_markdown_draft` repariert defekte YAML-Frontmatter (fehlendes `---`) automatisch. +* **Logik:** Ruft `/chat`, `/feedback` und `/ingest/analyze` Endpoints der API auf. ### 3.5 Embedding Service (`app.services.embeddings_client`) **Neu in v2.4:** -* Nutzt `httpx.AsyncClient` für non-blocking Calls an Ollama. +* Nutzt `httpx.AsyncClient` für non-blocking Calls an Ollama (Primary Mode). +* Besitzt einen **Fallback** auf `requests` (Synchron), falls Legacy-Skripte ihn nutzen. * Unterstützt dediziertes Embedding-Modell (`nomic-embed-text`) getrennt vom Chat-Modell. -* Enthält Legacy-Funktion `embed_text` für synchrone Skripte. + +### 3.6 Traffic Control (`app.services.llm_service`) +**Neu in v2.6 (Version 2.8.0):** +* Stellt sicher, dass Batch-Prozesse (Import) den Live-Chat nicht ausbremsen. +* **Methode:** `generate_raw_response(..., priority="background")` aktiviert eine Semaphore. +* **Limit:** Konfigurierbar über `MINDNET_LLM_BACKGROUND_LIMIT` (Default: 2) in der `.env`. --- @@ -232,9 +241,10 @@ Mindnet lernt nicht durch Training (Fine-Tuning), sondern durch **Konfiguration* Definiere die "Physik" des Typs (Import-Regeln und Basis-Wichtigkeit). risk: - chunk_profile: short # Risiken sind oft kurze Statements - retriever_weight: 0.90 # Sehr wichtig, fast so hoch wie Decisions - edge_defaults: ["blocks"] # Automatische Kante zu verlinkten Projekten + chunk_profile: sliding_short # Risiken sind oft kurze Statements + retriever_weight: 0.90 # Sehr wichtig + edge_defaults: ["blocks"] # Automatische Kante zu verlinkten Projekten + detection_keywords: ["gefahr", "risiko"] **2. Strategie-Ebene (`config/decision_engine.yaml`)** Damit dieser Typ aktiv geladen wird, musst du ihn einer Strategie zuordnen. @@ -262,14 +272,17 @@ Konfiguriere `edge_weights`, wenn Kausalität wichtiger ist als Ähnlichkeit. Wenn Mindnet neue Fragen stellen soll: -**1. Schema erweitern (`config/decision_engine.yaml`)** -Füge das Feld in die Liste ein. +**1. Schema erweitern (`config/types.yaml`)** +Füge das Feld in die Liste ein (Neu: Schemas liegen jetzt hier). project: - fields: ["Titel", "Ziel", "Budget"] # <--- Budget neu + schema: + - "Titel" + - "Ziel" + - "Budget (Neu)" **2. Keine Code-Änderung nötig** -Der `One-Shot Extractor` (Prompt Template) liest diese Liste dynamisch und weist das LLM an, das Budget zu extrahieren oder `[TODO]` zu setzen. +Der `One-Shot Extractor` (Prompt Template) liest diese Liste dynamisch. ### Fazit * **Vault:** Liefert das Wissen. diff --git a/docs/mindnet_functional_architecture.md b/docs/mindnet_functional_architecture.md index 32b5dd2..daf17b9 100644 --- a/docs/mindnet_functional_architecture.md +++ b/docs/mindnet_functional_architecture.md @@ -1,9 +1,9 @@ # Mindnet v2.4 – Fachliche Architektur -**Datei:** `docs/mindnet_functional_architecture_v2.4.md` -**Stand:** 2025-12-11 -**Status:** **FINAL** (Integrierter Stand WP01–WP11: Async Intelligence) +**Datei:** `docs/mindnet_functional_architecture_v2.6.md` +**Stand:** 2025-12-12 +**Status:** **FINAL** (Integrierter Stand WP01–WP15: 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). ---
@@ -19,7 +19,8 @@ - [2.1 Struktur-Kanten (Das Skelett)](#21-struktur-kanten-das-skelett) - [2.2 Inhalts-Kanten (explizit)](#22-inhalts-kanten-explizit) - [2.3 Typ-basierte Default-Kanten (Regelbasiert)](#23-typ-basierte-default-kanten-regelbasiert) - - [2.4 Matrix-Logik (Kontextsensitive Kanten) – Neu in v2.4](#24-matrix-logik-kontextsensitive-kanten--neu-in-v24) + - [2.4 Smart Edge Allocation (LLM-gefiltert) – Neu in v2.6](#24-smart-edge-allocation-llm-gefiltert--neu-in-v26) + - [2.5 Matrix-Logik (Kontextsensitive Kanten)](#25-matrix-logik-kontextsensitive-kanten) - [3) Edge-Payload – Felder \& Semantik](#3-edge-payload--felder--semantik) - [4) Typ-Registry (`config/types.yaml`)](#4-typ-registry-configtypesyaml) - [4.1 Zweck](#41-zweck) @@ -27,13 +28,15 @@ - [5) Der Retriever (Funktionaler Layer)](#5-der-retriever-funktionaler-layer) - [5.1 Scoring-Modell](#51-scoring-modell) - [5.2 Erklärbarkeit (Explainability) – WP04b](#52-erklärbarkeit-explainability--wp04b) - - [6) Context Intelligence \& Intent Router (WP06–WP11)](#6-context-intelligence--intent-router-wp06wp11) + - [5.3 Graph-Expansion](#53-graph-expansion) + - [6) Context Intelligence \& Intent Router (WP06–WP15)](#6-context-intelligence--intent-router-wp06wp15) - [6.1 Das Problem: Statische vs. Dynamische Antworten](#61-das-problem-statische-vs-dynamische-antworten) - - [6.2 Der Intent-Router (Keyword \& Semantik)](#62-der-intent-router-keyword--semantik) - - [6.3 Strategic Retrieval (Injektion von Werten)](#63-strategic-retrieval-injektion-von-werten) - - [6.4 Reasoning (Das Gewissen)](#64-reasoning-das-gewissen) - - [6.5 Der Interview-Modus (One-Shot Extraction)](#65-der-interview-modus-one-shot-extraction) - - [6.6 Active Intelligence (Link Suggestions) – Neu in v2.4](#66-active-intelligence-link-suggestions--neu-in-v24) + - [6.2 Der Hybrid Router v5 (Action vs. Question)](#62-der-hybrid-router-v5-action-vs-question) + - [6.3 Traffic Control (Realtime vs. Background)](#63-traffic-control-realtime-vs-background) + - [6.4 Strategic Retrieval (Injektion von Werten)](#64-strategic-retrieval-injektion-von-werten) + - [6.5 Reasoning (Das Gewissen)](#65-reasoning-das-gewissen) + - [6.6 Der Interview-Modus (One-Shot Extraction)](#66-der-interview-modus-one-shot-extraction) + - [6.7 Active Intelligence (Link Suggestions)](#67-active-intelligence-link-suggestions) - [7) Future Concepts: The Empathic Digital Twin (Ausblick)](#7-future-concepts-the-empathic-digital-twin-ausblick) - [7.1 Antizipation durch Erfahrung](#71-antizipation-durch-erfahrung) - [7.2 Empathie \& "Ich"-Modus](#72-empathie--ich-modus) @@ -49,7 +52,7 @@ - [13) Lösch-/Update-Garantien (Idempotenz)](#13-lösch-update-garantien-idempotenz) - [14) Beispiel – Von Markdown zu Kanten](#14-beispiel--von-markdown-zu-kanten) - [15) Referenzen (Projektdateien \& Leitlinien)](#15-referenzen-projektdateien--leitlinien) - - [16) Workpackage Status (v2.4.0)](#16-workpackage-status-v240) + - [16) Workpackage Status (v2.6.0)](#16-workpackage-status-v260)
--- @@ -63,7 +66,7 @@ Die drei zentralen Artefakt-Sammlungen lauten: - `mindnet_chunks` – semantische Teilstücke einer Note (Fenster/„Chunks“) - `mindnet_edges` – gerichtete Beziehungen zwischen Knoten (Chunks/Notes) -Die Import-Pipeline (seit v2.3.10 asynchron) erzeugt diese Artefakte **deterministisch** und **idempotent** (erneute Läufe überschreiben konsistent statt zu duplizieren). Die Import-Schritte sind: *parse → chunk → embed → edge → upsert*. +Die Import-Pipeline (seit v2.6 mit **Traffic Control** und **Smart Edges**) erzeugt diese Artefakte **deterministisch** und **idempotent** (erneute Läufe überschreiben konsistent statt zu duplizieren). Die Import-Schritte sind: *parse → chunk → embed → smart-edge-allocation → upsert*. --- @@ -80,9 +83,9 @@ Die Import-Pipeline (seit v2.3.10 asynchron) erzeugt diese Artefakte **determini - Jeder Chunk gehört **genau einer** Note. - Chunks bilden eine Sequenz (1…N) – das ermöglicht *next/prev*. - **Update v2.4:** Chunks werden jetzt durch das Modell `nomic-embed-text` in **768-dimensionale Vektoren** umgewandelt. Dies erlaubt eine deutlich höhere semantische Auflösung als frühere Modelle (384 Dim). -- **Neu in v2.2:** Alle Kanten entstehen ausschließlich zwischen Chunks (Scope="chunk"), nie zwischen Notes direkt. Notes dienen nur noch als Metadatencontainer. +- **Update v2.6 (Smart Chunking):** Ein Chunk ist nicht mehr nur ein dummer Textblock, der alle Links der Mutter-Notiz erbt. Durch die **Smart Edge Allocation (WP15)** "weiß" jeder Chunk genau, welche Kanten inhaltlich zu ihm gehören (siehe 2.4). -> **Wichtig:** Chunking-Profile (short/medium/long) kommen aus `types.yaml` (per Note-Typ), können aber lokal überschrieben werden. Die effektiven Werte werden bei der Payload-Erzeugung bestimmt. +> **Wichtig:** Chunking-Profile (short/medium/long/sliding_smart_edges) kommen aus `types.yaml` (per Note-Typ), können aber lokal überschrieben werden. Die effektiven Werte werden bei der Payload-Erzeugung bestimmt. --- @@ -97,7 +100,7 @@ Edges kodieren Beziehungen. Sie sind **gerichtet** und werden in `mindnet_edges` Diese Kanten entstehen immer, unabhängig von Inhalten. ### 2.2 Inhalts-Kanten (explizit) -Hier unterscheidet v2.2 präzise zwischen verschiedenen Quellen der Evidenz: +Hier unterscheidet v2.6 präzise zwischen verschiedenen Quellen der Evidenz: 1. **Explizite Inline-Relationen (Höchste Priorität):** Im Fließtext notierte, semantisch qualifizierte Relationen. @@ -131,7 +134,19 @@ Regel: **Für jede gefundene explizite Referenz** (s. o.) werden **zusätzliche* Beispiel: Ein *project* mit `edge_defaults=["depends_on"]` erzeugt zu *jedem* explizit referenzierten Ziel **zusätzlich** eine `depends_on`-Kante. Diese Kanten tragen *provenance=rule* und eine **rule_id** der Form `edge_defaults:{note_type}:{relation}` sowie eine geringere Confidence (~0.7). -### 2.4 Matrix-Logik (Kontextsensitive Kanten) – Neu in v2.4 +### 2.4 Smart Edge Allocation (LLM-gefiltert) – Neu in v2.6 +Mit **WP15** wurde ein intelligenter Filter eingeführt, um das Problem des "Broadcasting" zu lösen. Früher erbte jeder Chunk einer Notiz *alle* Links der Notiz, was zu unpräzisem Retrieval führte. + +**Der neue Prozess:** +1. **Scan:** Das System sammelt alle expliziten Links der gesamten Notiz. +2. **Chunking:** Der Text wird in Abschnitte zerlegt. +3. **Analyse (LLM):** Ein lokales LLM (Semantic Analyzer) liest jeden Chunk einzeln. +4. **Entscheidung:** Das LLM entscheidet für jeden Link aus Schritt 1: *"Ist dieser Link im Kontext dieses spezifischen Textabschnitts relevant?"* +5. **Zuweisung:** Nur wenn das LLM zustimmt, wird die Kante am Chunk erstellt. + +*Steuerung:* Dies ist pro Typ in der `types.yaml` konfigurierbar (`enable_smart_edge_allocation: true`). + +### 2.5 Matrix-Logik (Kontextsensitive Kanten) Mit WP-11 wurde eine Intelligenz eingeführt, die Kanten-Typen nicht nur anhand des Quell-Typs, sondern auch anhand des Ziel-Typs bestimmt ("Matrix"). **Beispiel für `Source Type: experience`:** @@ -154,7 +169,7 @@ Jede Kante hat mindestens: Erweiterte/abgeleitete Felder (WP03 Superset): -- `provenance` – `"explicit"` (Wikilink/Inline/Callout) oder `"rule"` (Typ-Defaults) +- `provenance` – `"explicit"` (Wikilink/Inline/Callout), `"rule"` (Typ-Defaults) oder neu `"smart"` (vom LLM validiert). - `rule_id` – maschinenlesbare Regelquelle (z.B. `inline:rel`, `edge_defaults:project:depends_on`) - `confidence` – 0.0–1.0; Heuristik zur Gewichtung im Scoring. @@ -168,20 +183,24 @@ Erweiterte/abgeleitete Felder (WP03 Superset): - Steuert **Chunking-Profile** (`short|medium|long`) **pro Typ** - Liefert **retriever_weight** (Note-/Chunk-Gewichtung im Ranking) **pro Typ** - Definiert **edge_defaults** je Typ (s. o.) +- **Neu in v2.6:** Steuert `enable_smart_edge_allocation` (LLM-Filter) und `detection_keywords` für den Hybrid Router. Der Importer lädt die Registry aus `MINDNET_TYPES_FILE` oder nutzt Fallbacks. **Frontmatter-Overrides** für Profile werden in v2.2 weitgehend ignoriert zugunsten einer zentralen Governance in der YAML-Datei. ### 4.2 Beispiel (auslieferungsnah) - version: 1.0 + version: 2.6.0 types: concept: chunk_profile: medium edge_defaults: ["references", "related_to"] retriever_weight: 0.60 - project: - chunk_profile: long - edge_defaults: ["references", "depends_on"] - retriever_weight: 0.97 + experience: + chunk_profile: sliding_smart_edges + enable_smart_edge_allocation: true # WP15: LLM prüft Kanten + detection_keywords: ["passiert", "erlebt", "situation"] # WP06: Router-Trigger + schema: # WP07: Interview-Struktur + - "Situation (Was ist passiert?)" + - "Meine Reaktion (Was habe ich getan?)" **Auflösung im Importer** - `effective_chunk_profile(note_type, registry)` → `"short|medium|long"|None` @@ -220,9 +239,12 @@ Der Retriever ist keine Blackbox mehr. Er liefert auf Wunsch (`explain=True`) ei Die API gibt diese Analysen als menschenlesbare Sätze (`reasons`) und als Datenstruktur (`score_breakdown`) zurück. +### 5.3 Graph-Expansion +Der Hybrid-Modus lädt dynamisch die Nachbarschaft der Top-K Vektor-Treffer ("Seeds") über `graph_adapter.expand`. Dies baut einen temporären `NetworkX`-artigen Graphen im Speicher (Klasse `Subgraph`), auf dem Boni berechnet werden. + --- -## 6) Context Intelligence & Intent Router (WP06–WP11) +## 6) Context Intelligence & Intent Router (WP06–WP15) Seit WP06 agiert Mindnet nicht mehr statisch, sondern passt seine Suchstrategie dem **Intent** (der Absicht) des Nutzers an. Dies ist die Transformation vom reinen Wissens-Abrufer zum strategischen Partner. @@ -230,40 +252,51 @@ Seit WP06 agiert Mindnet nicht mehr statisch, sondern passt seine Suchstrategie * **Früher (Pre-WP06):** Jede Frage ("Was ist X?" oder "Soll ich X?") wurde gleich behandelt -> Fakten-Retrieval. * **Heute (WP06):** Das System erkennt, *was* der User will (Rat, Fakten oder Datenerfassung) und wechselt den Modus. -### 6.2 Der Intent-Router (Keyword & Semantik) -Der Router prüft vor jeder Antwort die Absicht über konfigurierbare Strategien (`config/decision_engine.yaml`): +### 6.2 Der Hybrid Router v5 (Action vs. Question) +Der Router wurde in v2.6 (WP15) weiterentwickelt, um Fehlalarme zu vermeiden. -1. **FACT:** Reine Wissensfrage ("Was ist Qdrant?"). → Standard RAG. -2. **DECISION:** Frage nach Rat oder Strategie ("Soll ich Qdrant nutzen?"). → Aktiviert die Decision Engine. -3. **EMPATHY:** Emotionale Zustände ("Ich bin gestresst"). → Aktiviert den empathischen Modus. -4. **INTERVIEW (WP07):** Wunsch, Wissen zu erfassen ("Neues Projekt anlegen"). → Aktiviert den Draft-Generator. -5. **CODING:** Technische Anfragen. +1. **Frage-Erkennung:** + * Das System prüft zuerst: Enthält der Satz ein `?` oder typische W-Wörter (Wer, Wie, Was)? + * Wenn **JA** -> Gehe in den **RAG Modus** (Intent `FACT` oder `DECISION`). Interviews werden hier blockiert. + +2. **Befehls-Erkennung (Fast Path):** + * Wenn **NEIN** (keine Frage), prüft das System auf Keywords in `types.yaml` ("Projekt", "Erfahrung"). + * Treffer -> **INTERVIEW Modus** (Erfassen). Das Schema wird aus `types.yaml` geladen. -### 6.3 Strategic Retrieval (Injektion von Werten) +3. **LLM Fallback (Slow Path):** + * Wenn beides fehlschlägt, entscheidet das LLM über den Intent. + +### 6.3 Traffic Control (Realtime vs. Background) +Um den Live-Betrieb (Chat) nicht durch den ressourcenintensiven Smart-Import (LLM-Analyse) zu gefährden, implementiert v2.6 ein **Traffic Control System** im `LLMService`. + +* **Realtime Lane (Chat):** Anfragen aus dem Chat erhalten `priority="realtime"`. Sie umgehen alle Warteschlangen und werden sofort bearbeitet. +* **Background Lane (Import):** Anfragen zur Kantenanalyse erhalten `priority="background"`. Sie werden durch eine **Semaphore** gedrosselt (Standard: max 2 parallel), um die Hardware nicht zu überlasten. + +### 6.4 Strategic Retrieval (Injektion von Werten) Im Modus `DECISION` führt das System eine **zweite Suchstufe** aus. Es sucht nicht nur nach semantisch passenden Texten zur Frage, sondern erzwingt das Laden von strategischen Notizen wie: * **Values (`type: value`):** Moralische Werte (z.B. "Privacy First"). * **Principles (`type: principle`):** Handlungsanweisungen. * **Goals (`type: goal`):** Strategische Ziele. -### 6.4 Reasoning (Das Gewissen) +### 6.5 Reasoning (Das Gewissen) Das LLM erhält im Prompt die explizite Anweisung: *"Wäge die Fakten (aus der Suche) gegen die injizierten Werte ab."* Dadurch entstehen Antworten, die nicht nur technisch korrekt sind, sondern subjektiv passend ("Tool X passt nicht zu deinem Ziel Z"). -### 6.5 Der Interview-Modus (One-Shot Extraction) +### 6.6 Der Interview-Modus (One-Shot Extraction) Wenn der User Wissen erfassen will ("Ich möchte ein neues Projekt anlegen"), wechselt Mindnet in den **Interview-Modus**. * **Late Binding Schema:** Das System lädt ein konfiguriertes Schema für den Ziel-Typ (z.B. `project`: Pflichtfelder sind Titel, Ziel, Status). * **One-Shot Extraction:** Statt eines langen Dialogs extrahiert das LLM **sofort** alle verfügbaren Infos aus dem Prompt und generiert einen validen Markdown-Draft mit Frontmatter. -* **Draft-Status:** Fehlende Pflichtfelder werden mit `[TODO]` markiert. +* **Healing Parser (v2.5):** Falls das LLM die YAML-Syntax beschädigt (z.B. fehlendes Ende), repariert das Frontend den Entwurf automatisch. * **UI-Integration:** Das Frontend rendert statt einer Chat-Antwort einen **interaktiven Editor** (WP10), in dem der Entwurf finalisiert werden kann. -### 6.6 Active Intelligence (Link Suggestions) – Neu in v2.4 +### 6.7 Active Intelligence (Link Suggestions) Im **Draft Editor** (Frontend) unterstützt das System den Autor aktiv. * **Analyse:** Ein "Sliding Window" scannt den Text im Hintergrund (auch lange Entwürfe). * **Erkennung:** Es findet Begriffe ("Mindnet") und semantische Konzepte ("Autofahrt in Italien"). * **Matching:** Es prüft gegen den Index (Aliases und Vektoren). * **Vorschlag:** Es bietet fertige Markdown-Links an (z.B. `[[rel:related_to ...]]`), die per Klick eingefügt werden. -* **Logik:** Dabei kommt die in 2.4 beschriebene **Matrix-Logik** zum Einsatz, um den korrekten Kanten-Typ vorzuschlagen. +* **Logik:** Dabei kommt die in 2.5 beschriebene **Matrix-Logik** zum Einsatz, um den korrekten Kanten-Typ vorzuschlagen. --- @@ -301,8 +334,9 @@ Mindnet lernt nicht durch klassisches Training (Fine-Tuning), sondern durch **Ko chunk_profile: short # Risiken sind oft kurze Statements retriever_weight: 0.90 # Hohe Priorität im Ranking edge_defaults: ["blocks"] # Automatische Kante zu verlinkten Projekten + detection_keywords: ["gefahr", "risiko"] # Für den Hybrid Router ``` - *Effekt:* Der Retriever spült diese Notizen bei relevanten Anfragen nach oben. + *Effekt:* Der Router erkennt das Wort "Risiko" und bietet ein Interview an. Der Retriever spült diese Notizen bei relevanten Anfragen nach oben. 2. **Semantik erklären (`config/prompts.yaml` / `decision_engine.yaml`)** Bringe dem LLM bei, wie es mit diesem Typ umgehen soll. Füge `risk` zur `DECISION`-Strategie hinzu (`inject_types`), damit es bei Entscheidungen geladen wird. @@ -359,12 +393,12 @@ Die Interaktion erfolgt primär über das **Web-Interface (WP10)**. ## 10) Confidence & Provenance – wozu? Der Retriever kann Edges gewichten: -- **provenance**: *explicit* > *rule* +- **provenance**: *explicit* > *smart* (Neu) > *rule* - **confidence**: numerische Feinsteuerung - **retriever_weight (Note/Chunk)**: skaliert die Relevanz des gesamten Knotens Eine typische Gewichtung (konfigurierbar in `retriever.yaml`) ist: -`explicit: 1.0`, `rule: 0.8`. Damit bevorzugt der Graph **kuratiertes Wissen** (explizit notierte Links) vor „erweiterten“ Default-Ableitungen. +`explicit: 1.0`, `smart: 0.9`, `rule: 0.8`. Damit bevorzugt der Graph **kuratiertes Wissen** (explizit notierte Links) vor „erweiterten“ Default-Ableitungen. --- @@ -413,11 +447,11 @@ Frontmatter-Eigenschaften (Properties) bleiben **minimiert**: > [!edge] related_to: [[Vector DB Basics]] -**Ergebnis (fachlich)** -1. `depends_on(Chunk→Qdrant)` mit `rule_id=inline:rel`, `confidence≈0.95`. -2. `references(Chunk→Embeddings 101)` mit `rule_id=explicit:wikilink`, `confidence=1.0`. -3. `related_to(Chunk→Vector DB Basics)` via Callout; `rule_id=callout:edge`, `confidence≈0.90`. -4. **Typ-Defaults:** Falls die Note vom Typ `project` ist, entstehen zusätzlich `depends_on`-Kanten zu den Zielen aus (2) und (3). +**Ergebnis (fachlich - Smart Edges)** +Das LLM analysiert jeden Chunk. +1. Chunk 1 ("Wir nutzen..."): Enthält `depends_on(Chunk→Qdrant)`. Das LLM bestätigt: Relevant. -> Kante wird erstellt. +2. Chunk 2 ("Siehe auch..."): Enthält `references(Chunk→Embeddings)`. Das LLM bestätigt. +3. **Wichtig:** Ein Chunk 3 ("Die Benutzeroberfläche ist blau..."), der keine Erwähnung von Qdrant hat, bekommt **keine** `depends_on` Kante zu Qdrant, auch wenn die Note global verlinkt ist. Das ist der Gewinn von WP15. --- @@ -425,6 +459,7 @@ Frontmatter-Eigenschaften (Properties) bleiben **minimiert**: - Import-Pipeline & Registry-Auflösung: `scripts/import_markdown.py`. - Kantenbildung (V2-Logic): `app/core/derive_edges.py`. +- Smart Chunking & Traffic Control: `app/core/chunker.py` & `app/services/llm_service.py`. - Typ-Registry: `config/types.yaml` & `TYPE_REGISTRY_MANUAL.md`. - Retriever-Scoring & Explanation: `app/core/retriever.py`. - Persönlichkeit & Chat: `config/prompts.yaml` & `app/routers/chat.py`. @@ -435,7 +470,7 @@ Frontmatter-Eigenschaften (Properties) bleiben **minimiert**: --- -## 16) Workpackage Status (v2.4.0) +## 16) Workpackage Status (v2.6.0) Aktueller Implementierungsstand der Module. @@ -448,9 +483,12 @@ Aktueller Implementierungsstand der Module. | **WP04b**| Explanation Layer | 🟢 Live | API liefert Reasons & Breakdown. | | **WP04c**| Feedback Loop | 🟢 Live | Logging (JSONL) & Traceability aktiv. | | **WP05** | Persönlichkeit / Chat | 🟢 Live | RAG-Integration mit Context Enrichment. | -| **WP06** | Decision Engine | 🟢 Live | Intent-Router & Strategic Retrieval. | +| **WP06** | Decision Engine | 🟢 Live | Hybrid Router, Strategic Retrieval. | | **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** | | **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. | | **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). | -| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** | -| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** | \ No newline at end of file +| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI & Healing Parser.** | +| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** | +| **WP15** | Smart Edge Allocation | 🟢 Live | **LLM-Filter & Traffic Control aktiv.** | +| **WP16** | Auto-Discovery | 🟡 Geplant | UX & Retrieval Tuning. | +| **WP17** | Conversational Memory | 🟡 Geplant | Dialog-Gedächtnis. | \ No newline at end of file diff --git a/docs/mindnet_technical_architecture.md b/docs/mindnet_technical_architecture.md index 875bc98..893ef5f 100644 --- a/docs/mindnet_technical_architecture.md +++ b/docs/mindnet_technical_architecture.md @@ -1,21 +1,21 @@ -# Mindnet v2.4 – Technische Architektur -**Datei:** `docs/mindnet_technical_architecture_v2.4.md` -**Stand:** 2025-12-11 -**Status:** **FINAL** (Integrierter Stand WP01–WP11: Async Intelligence) -**Quellen:** `Programmplan_V2.2.md`, `Handbuch.md`, `chunking_strategy.md`, `wp04_retriever_scoring.md`. +# Mindnet v2.6 – Technische Architektur +**Datei:** `docs/mindnet_technical_architecture_v2.6.md` +**Stand:** 2025-12-12 +**Status:** **FINAL** (Integrierter Stand WP01–WP15: Smart Edges & Traffic Control) +**Quellen:** `Programmplan_V2.6.md`, `Handbuch.md`, `chunking_strategy.md`, `wp04_retriever_scoring.md`. > **Ziel dieses Dokuments:** -> Vollständige Beschreibung der technischen Architektur inkl. Graph-Datenbank, Retrieval-Logik, der **RAG-Komponenten (Decision Engine & Hybrid Router)**, des **Interview-Modus** und dem **Frontend (Streamlit)**. +> Vollständige Beschreibung der technischen Architektur inkl. Graph-Datenbank, Retrieval-Logik, der **RAG-Komponenten (Decision Engine & Hybrid Router)**, des **Traffic Control Systems** und dem **Frontend (Streamlit)**. ---
📖 Inhaltsverzeichnis (Klicken zum Öffnen) -- [Mindnet v2.4 – Technische Architektur](#mindnet-v24--technische-architektur) +- [Mindnet v2.6 – Technische Architektur](#mindnet-v26--technische-architektur) - [](#) - [1. Systemüberblick](#1-systemüberblick) - [1.1 Architektur-Zielbild](#11-architektur-zielbild) - - [1.2 Verzeichnisstruktur \& Komponenten (Post-WP10)](#12-verzeichnisstruktur--komponenten-post-wp10) + - [1.2 Verzeichnisstruktur \& Komponenten (Post-WP15)](#12-verzeichnisstruktur--komponenten-post-wp15) - [2. Datenmodell \& Collections (Qdrant)](#2-datenmodell--collections-qdrant) - [2.1 Notes Collection (`_notes`)](#21-notes-collection-prefix_notes) - [2.2 Chunks Collection (`_chunks`)](#22-chunks-collection-prefix_chunks) @@ -27,22 +27,23 @@ - [3.4 Prompts (`config/prompts.yaml`)](#34-prompts-configpromptsyaml) - [3.5 Environment (`.env`)](#35-environment-env) - [4. Import-Pipeline (Markdown → Qdrant)](#4-import-pipeline-markdown--qdrant) - - [4.1 Verarbeitungsschritte (Async)](#41-verarbeitungsschritte-async) + - [4.1 Verarbeitungsschritte (Async + Smart)](#41-verarbeitungsschritte-async--smart) - [5. Retriever-Architektur \& Scoring](#5-retriever-architektur--scoring) - [5.1 Betriebsmodi](#51-betriebsmodi) - [5.2 Scoring-Formel (WP04a)](#52-scoring-formel-wp04a) - [5.3 Explanation Layer (WP04b)](#53-explanation-layer-wp04b) - [5.4 Graph-Expansion](#54-graph-expansion) - [6. RAG \& Chat Architektur (WP06 Hybrid Router + WP07 Interview)](#6-rag--chat-architektur-wp06-hybrid-router--wp07-interview) - - [6.1 Architektur-Pattern: Intent Router](#61-architektur-pattern-intent-router) - - [6.2 Schritt 1: Intent Detection (Hybrid)](#62-schritt-1-intent-detection-hybrid) - - [6.3 Schritt 2: Strategy Resolution (Late Binding)](#63-schritt-2-strategy-resolution-late-binding) - - [6.4 Schritt 3: Retrieval vs. Extraction](#64-schritt-3-retrieval-vs-extraction) - - [6.5 Schritt 4: Generation \& Response](#65-schritt-4-generation--response) + - [6.1 Architektur-Pattern: Intent Router v5](#61-architektur-pattern-intent-router-v5) + - [6.2 Traffic Control (Priorisierung)](#62-traffic-control-priorisierung) + - [6.3 Schritt 1: Intent Detection (Question vs. Action)](#63-schritt-1-intent-detection-question-vs-action) + - [6.4 Schritt 2: Strategy Resolution](#64-schritt-2-strategy-resolution) + - [6.5 Schritt 3: Retrieval vs. Extraction](#65-schritt-3-retrieval-vs-extraction) + - [6.6 Schritt 4: Generation \& Response](#66-schritt-4-generation--response) - [7. Frontend Architektur (WP10)](#7-frontend-architektur-wp10) - [7.1 Kommunikation](#71-kommunikation) - [7.2 Features \& UI-Logik](#72-features--ui-logik) - - [7.3 Draft-Editor \& Sanitizer (Neu in WP10a)](#73-draft-editor--sanitizer-neu-in-wp10a) + - [7.3 Draft-Editor \& Healing Parser (Neu in WP10a/v2.5)](#73-draft-editor--healing-parser-neu-in-wp10av25) - [7.4 State Management (Resurrection Pattern)](#74-state-management-resurrection-pattern) - [7.5 Deployment Ports](#75-deployment-ports) - [8. Feedback \& Logging Architektur (WP04c)](#8-feedback--logging-architektur-wp04c) @@ -62,57 +63,63 @@ Mindnet ist ein **lokales RAG-System (Retrieval Augmented Generation)** mit Web-Interface. 1. **Source:** Markdown-Notizen in einem Vault (Obsidian-kompatibel). 2. **Pipeline:** Ein Python-Importer transformiert diese in **Notes**, **Chunks** und **Edges**. + * **Neu in v2.6:** Intelligente Kanten-Zuweisung durch lokale LLMs (**Smart Edge Allocation**). 3. **Storage:** * **Qdrant:** Vektor-Datenbank für Graph und Semantik (Collections: notes, chunks, edges). * **Local Files (JSONL):** Append-Only Logs für Feedback und Search-History (Data Flywheel). 4. **Backend:** Eine FastAPI-Anwendung stellt Endpunkte für **Semantische** und **Hybride Suche** sowie **Feedback** bereit. - * **Update v2.3.10:** Der Core arbeitet nun vollständig **asynchron (AsyncIO)**, um Blockaden bei Embedding-Requests zu vermeiden. -5. **Frontend:** Streamlit-App (`ui.py`) für Interaktion und Visualisierung inkl. **Draft Editor** und **Intelligence-Features**. + * **Update v2.6:** Der Core arbeitet nun vollständig **asynchron (AsyncIO)** mit **Traffic Control** (Semaphore zur Lastverteilung). +5. **Frontend:** Streamlit-App (`ui.py`) für Interaktion und Visualisierung inkl. **Draft Editor**, **Active Intelligence** und **Healing Parser**. 6. **Inference:** Lokales LLM (Ollama: Phi-3 Mini) für RAG-Chat und Antwortgenerierung. Embedding via `nomic-embed-text`. Das System arbeitet **deterministisch** (stabile IDs) und ist **konfigurationsgetrieben** (`types.yaml`, `retriever.yaml`, `decision_engine.yaml`, `prompts.yaml`). -### 1.2 Verzeichnisstruktur & Komponenten (Post-WP10) +### 1.2 Verzeichnisstruktur & Komponenten (Post-WP15) /mindnet/ ├── app/ │ ├── main.py # FastAPI Einstiegspunkt │ ├── core/ - │ │ ├── ingestion.py # NEU: Async Ingestion Service (WP11) + │ │ ├── ingestion.py # NEU: Async Ingestion mit Change Detection │ │ ├── qdrant.py # Client-Factory & Connection │ │ ├── qdrant_points.py # Low-Level Point Operations (Upsert/Delete) │ │ ├── note_payload.py # Bau der Note-Objekte │ │ ├── chunk_payload.py # Bau der Chunk-Objekte - │ │ ├── chunker.py # Text-Zerlegung (Profiling) + │ │ ├── chunker.py # Smart Chunker Orchestrator (WP15) │ │ ├── edges.py # Edge-Datenstrukturen │ │ ├── derive_edges.py # Logik der Kantenableitung (WP03) │ │ ├── graph_adapter.py # Subgraph & Reverse-Lookup (WP04b) │ │ └── retriever.py # Scoring, Expansion & Explanation (WP04a/b) │ ├── models/ # Pydantic DTOs + │ │ └── dto.py # Zentrale DTO-Definition │ ├── routers/ │ │ ├── query.py # Such-Endpunkt │ │ ├── ingest.py # NEU: API für Save & Analyze (WP11) - │ │ ├── chat.py # Hybrid Router & Interview Logic (WP06/07) + │ │ ├── chat.py # Hybrid Router v5 & Interview Logic (WP06/07) │ │ ├── feedback.py # Feedback-Endpunkt (WP04c) │ │ └── ... │ ├── services/ - │ │ ├── llm_service.py # Ollama Chat Client - │ │ ├── embeddings_client.py# NEU: Async Embedding Client (HTTPX) - │ │ └── feedback_service.py # JSONL Logging (WP04c) + │ │ ├── llm_service.py # Ollama Chat Client mit Traffic Control (v2.8.0) + │ │ ├── semantic_analyzer.py# NEU: LLM-Filter für Edges (WP15) + │ │ ├── embeddings_client.py# NEU: Async Embeddings (HTTPX) + │ │ ├── feedback_service.py # Logging (JSONL Writer) + │ │ └── discovery.py # NEU: Intelligence Logic (WP11) │ ├── frontend/ # NEU (WP10) - │ └── ui.py # Streamlit Application inkl. Sanitizer - ├── config/ - │ ├── types.yaml # Typ-Definitionen (Import-Zeit) - │ ├── retriever.yaml # Scoring-Gewichte (Laufzeit) - │ ├── decision_engine.yaml # Strategien & Schemas (WP06/WP07) - │ └── prompts.yaml # LLM System-Prompts & Templates (WP06) + │ │ └── ui.py # Streamlit Application inkl. Healing Parser + │ └── main.py # Entrypoint der API + ├── config/ # YAML-Konfigurationen (Single Source of Truth) + │ ├── types.yaml # Import-Regeln & Smart-Edge Config + │ ├── prompts.yaml # LLM Prompts & Interview Templates (WP06/07) + │ ├── decision_engine.yaml # Router-Strategien (Actions only) + │ └── retriever.yaml # Scoring-Regeln & Kantengewichte ├── data/ - │ └── logs/ # Lokale JSONL-Logs (WP04c) - ├── scripts/ + │ └── logs/ # Lokale Logs (search_history.jsonl, feedback.jsonl) + ├── scripts/ # CLI-Tools (Import, Diagnose, Reset) │ ├── import_markdown.py # Haupt-Importer CLI (Async) │ ├── payload_dryrun.py # Diagnose: JSON-Generierung ohne DB │ └── edges_full_check.py # Diagnose: Graph-Integrität - └── tests/ # Pytest Suite + ├── tests/ # Pytest Suite & Smoke Scripts + └── vault/ # Dein lokaler Markdown-Content (Git-ignored) --- @@ -140,7 +147,7 @@ Repräsentiert die Metadaten einer Datei. ### 2.2 Chunks Collection (`_chunks`) Die atomaren Sucheinheiten. * **Zweck:** Vektorsuche (Embeddings), Granulares Ergebnis. -* **Update v2.3.10:** Vektor-Dimension ist jetzt **768** (für `nomic-embed-text`). +* **Update v2.4:** Vektor-Dimension ist jetzt **768** (für `nomic-embed-text`). * **Schema (Payload):** | Feld | Datentyp | Beschreibung | @@ -171,6 +178,7 @@ Gerichtete Kanten. Massiv erweitert in WP03 für Provenienz-Tracking. | `scope` | Keyword | Geltungsbereich. | Immer `"chunk"` (v2.2). | | `rule_id` | Keyword | Herkunftsregel. | `explicit:wikilink`, `inline:rel` | | `confidence` | Float | Vertrauenswürdigkeit (0.0-1.0). | 1.0, 0.95, 0.7 | + | `provenance` | Keyword | Quelle der Kante (Neu in v2.6). | `explicit`, `rule`, `smart` | --- @@ -181,13 +189,20 @@ Die Logik ist ausgelagert in YAML-Dateien. ### 3.1 Typ-Registry (`config/types.yaml`) Steuert den Import-Prozess. * **Priorität:** Frontmatter > Pfad > Default. +* **Neu in v2.6:** + * `enable_smart_edge_allocation`: (Bool) Aktiviert den LLM-Filter für diesen Typ. + * `detection_keywords`: (List) Keywords für den Hybrid Router ("Objekterkennung"). * **Inhalt (Beispiel):** types: concept: - chunk_profile: medium + chunk_profile: sliding_standard edge_defaults: ["references", "related_to"] retriever_weight: 0.60 + experience: + chunk_profile: sliding_smart_edges + enable_smart_edge_allocation: true + detection_keywords: ["erfahrung", "erlebt"] ### 3.2 Retriever-Config (`config/retriever.yaml`) Steuert das Scoring zur Laufzeit (WP04a). @@ -199,14 +214,14 @@ Steuert das Scoring zur Laufzeit (WP04a). centrality_weight: 0.5 ### 3.3 Decision Engine (`config/decision_engine.yaml`) -**Neu in WP06/07:** Steuert den Intent-Router und die Interview-Schemas. +**Neu in WP06/07:** Steuert den Intent-Router. * Definiert Strategien (`DECISION`, `INTERVIEW`, etc.). -* Definiert `schemas` für den Interview-Modus (Pflichtfelder pro Typ). +* **Update v2.6:** Enthält nur noch **Handlungs-Keywords** (Verben wie "neu", "erstellen"). Objektnamen ("Projekt") werden nun über `types.yaml` aufgelöst. * Definiert LLM-Router-Settings (`llm_fallback_enabled`). ### 3.4 Prompts (`config/prompts.yaml`) Steuert die LLM-Persönlichkeit und Templates. -* Enthält Templates für alle Strategien inkl. `interview_template` mit One-Shot Logik. +* Enthält Templates für alle Strategien inkl. `interview_template` und neu `router_prompt`. ### 3.5 Environment (`.env`) Erweiterung für LLM-Steuerung und Embedding-Modell: @@ -215,32 +230,33 @@ Erweiterung für LLM-Steuerung und Embedding-Modell: MINDNET_EMBEDDING_MODEL=nomic-embed-text # NEU in v2.3.10 MINDNET_OLLAMA_URL=http://127.0.0.1:11434 MINDNET_LLM_TIMEOUT=300.0 # Neu: Erhöht für CPU-Inference Cold-Starts - MINDNET_API_TIMEOUT=60.0 # Neu: Timeout für Frontend-API Calls + MINDNET_API_TIMEOUT=300.0 # Neu: Timeout für Frontend-API (Smart Edges brauchen Zeit) MINDNET_DECISION_CONFIG="config/decision_engine.yaml" MINDNET_VAULT_ROOT="./vault" # Neu: Pfad für Write-Back + MINDNET_LLM_BACKGROUND_LIMIT=2 # Neu: Limit für parallele Hintergrund-Jobs (Traffic Control) --- ## 4. Import-Pipeline (Markdown → Qdrant) Das Skript `scripts/import_markdown.py` orchestriert den Prozess. -**Neu in v2.3.10:** Der Import nutzt `asyncio` und eine **Semaphore**, um Ollama nicht zu überlasten. +**Neu in v2.6:** Der Import nutzt `asyncio` und **Traffic Control**, um Ollama nicht zu überlasten. -### 4.1 Verarbeitungsschritte (Async) +### 4.1 Verarbeitungsschritte (Async + Smart) -1. **Discovery & Parsing:** - * Einlesen der `.md` Dateien. Hash-Vergleich (Body/Frontmatter) zur Erkennung von Änderungen. -2. **Typauflösung:** - * Bestimmung des `type` via `types.yaml`. -3. **Chunking:** - * Zerlegung via `chunker.py` basierend auf `chunk_profile`. -4. **Embedding (Async):** - * Der `EmbeddingsClient` (`app/services/embeddings_client.py`) sendet Text-Chunks asynchron an Ollama. - * Modell: `nomic-embed-text` (768d). - * Semaphore: Max. 5 gleichzeitige Files, um OOM (Out-of-Memory) zu verhindern. +1. **Discovery & Parsing:** Hash-Vergleich zur Erkennung von Änderungen. +2. **Typauflösung:** Bestimmung des `type` via `types.yaml`. +3. **Config Check:** Laden des `chunk_profile` und `enable_smart_edge_allocation`. +4. **Chunking & Smart Edges (WP15):** + * Zerlegung des Textes via `chunker.py`. + * Wenn Smart Edges aktiv: Der `SemanticAnalyzer` sendet Chunks an das LLM. + * **Traffic Control:** Der Request nutzt `priority="background"`. Die Semaphore (Limit: 2) drosselt die Parallelität. 5. **Kantenableitung (Edge Derivation):** * `derive_edges.py` erzeugt Inline-, Callout- und Default-Edges. -6. **Upsert:** +6. **Embedding (Async):** + * Der `EmbeddingsClient` (`app/services/embeddings_client.py`) sendet Text-Chunks asynchron an Ollama. + * Modell: `nomic-embed-text` (768d). +7. **Upsert:** * Schreiben in Qdrant. Nutzung von `--purge-before-upsert`. * **Strict Mode:** Der Prozess bricht ab, wenn Embeddings leer sind oder Dimension `0` haben. @@ -269,49 +285,55 @@ $$ * **Gewichte ($W$):** Stammen aus `retriever.yaml`. ### 5.3 Explanation Layer (WP04b) -Der Retriever kann Ergebnisse erklären (`explain=True`). -* **Logik:** - * Berechnung des `ScoreBreakdown` (Anteile von Semantik, Graph, Typ). - * Analyse des lokalen Subgraphen mittels `graph_adapter.py`. - * **Incoming Edges (Authority):** Wer zeigt auf diesen Treffer? (z.B. "Referenziert von...") - * **Outgoing Edges (Hub):** Worauf zeigt dieser Treffer? (z.B. "Verweist auf...") -* **Output:** `QueryHit` enthält ein `explanation` Objekt mit menschenlesbaren `reasons` und `related_edges`. +Der Retriever ist keine Blackbox mehr. Er liefert auf Wunsch (`explain=True`) eine strukturierte Begründung (`Explanation`-Objekt). + +**Die "Warum"-Logik:** +1. **Semantik:** Prüfung der Cosine-Similarity ("Sehr hohe textuelle Übereinstimmung"). +2. **Typ:** Prüfung des `retriever_weight` ("Bevorzugt, da Entscheidung"). +3. **Graph (Kontext):** + * **Hub (Outgoing):** Worauf verweist dieser Treffer? ("Verweist auf Qdrant"). + * **Authority (Incoming):** Wer verweist auf diesen Treffer? ("Wird referenziert von Projekt Alpha"). + +Die API gibt diese Analysen als menschenlesbare Sätze (`reasons`) und als Datenstruktur (`score_breakdown`) zurück. ### 5.4 Graph-Expansion Der Hybrid-Modus lädt dynamisch die Nachbarschaft der Top-K Vektor-Treffer ("Seeds") über `graph_adapter.expand`. Dies baut einen temporären `NetworkX`-artigen Graphen im Speicher (Klasse `Subgraph`), auf dem Boni berechnet werden. --- -## 6. RAG \& Chat Architektur (WP06 Hybrid Router + WP07 Interview) +## 6. RAG & Chat Architektur (WP06 Hybrid Router + WP07 Interview) -Der Flow für eine Chat-Anfrage (`/chat`) wurde in WP06 auf eine **Configuration-Driven Architecture** umgestellt. Der `ChatRouter` (`app/routers/chat.py`) fungiert als zentraler Dispatcher. +Der Flow für eine Chat-Anfrage (`/chat`) wurde in WP06 auf eine **Configuration-Driven Architecture** umgestellt und in WP15 (v2.6) verfeinert. -### 6.1 Architektur-Pattern: Intent Router +### 6.1 Architektur-Pattern: Intent Router v5 Die Behandlung einer Anfrage ist nicht mehr hartkodiert, sondern wird dynamisch zur Laufzeit entschieden. * **Input:** User Message. -* **Config:** `config/decision_engine.yaml` (Strategien & Keywords). +* **Config:** `decision_engine.yaml` (Strategien) + `types.yaml` (Objekte). * **Komponenten:** - * **Fast Path:** Keyword Matching (CPU-schonend). - * **Slow Path:** LLM-basierter Semantic Router (für subtile Intents). + * **Traffic Control:** `LLMService` priorisiert Chat-Anfragen. + * **Question Detection:** Unterscheidung Frage vs. Befehl. -### 6.2 Schritt 1: Intent Detection (Hybrid) +### 6.2 Traffic Control (Priorisierung) +Der `LLMService` implementiert zwei Spuren basierend auf `app/services/llm_service.py` (v2.8.0): +* **Realtime (Chat):** `priority="realtime"`. Umgeht die Semaphore. Antwortet sofort. +* **Background (Import/Analyse):** `priority="background"`. Wartet in der Semaphore (`asyncio.Semaphore`), die lazy mit `MINDNET_LLM_BACKGROUND_LIMIT` initialisiert wird. + +### 6.3 Schritt 1: Intent Detection (Question vs. Action) Der Router ermittelt die Absicht (`Intent`) des Nutzers. -1. **Keyword Scan (Fast Path):** - * Iteration über alle Strategien in `decision_engine.yaml`. - * Prüfung auf `trigger_keywords`. - * **Best Match:** Bei mehreren Treffern gewinnt das längste/spezifischste Keyword (Robustheit gegen Shadowing). -2. **LLM Fallback (Slow Path):** - * Nur aktiv, wenn `llm_fallback_enabled: true`. - * Greift, wenn keine Keywords gefunden wurden. - * Sendet die Query an das LLM mit einem Klassifizierungs-Prompt (`llm_router_prompt`). - * Ergebnis: `EMPATHY`, `DECISION`, `INTERVIEW`, `CODING` oder `FACT`. +1. **Frage-Check:** Wenn der Input ein `?` enthält oder mit W-Wörtern beginnt, wird der **RAG-Modus** erzwungen (oder LLM-Entscheidung). Interviews werden hier unterdrückt. +2. **Keyword Scan (Fast Path):** + * Prüfung auf `trigger_keywords` (Handlung) in `decision_engine.yaml`. + * Prüfung auf `detection_keywords` (Objekt) in `types.yaml`. + * Treffer -> **INTERVIEW Modus** (Erfassen). +3. **LLM Fallback (Slow Path):** + * Greift, wenn keine Keywords passen. Sendet Query an LLM Router. -### 6.3 Schritt 2: Strategy Resolution (Late Binding) +### 6.4 Schritt 2: Strategy Resolution Basierend auf dem Intent lädt der Router die Parameter: * **Bei RAG (FACT/DECISION):** `inject_types` für Strategic Retrieval. * **Bei INTERVIEW (WP07):** `schemas` (Pflichtfelder) basierend auf der erkannten Ziel-Entität (`_detect_target_type`). -### 6.4 Schritt 3: Retrieval vs. Extraction +### 6.5 Schritt 3: Retrieval vs. Extraction Der Router verzweigt hier: **A) RAG Modus (FACT, DECISION, EMPATHY):** @@ -324,9 +346,9 @@ Der Router verzweigt hier: 2. **Schema Injection:** Das Schema für den erkannten Typ (z.B. "Project") wird geladen. 3. **Prompt Assembly:** Der `interview_template` Prompt wird mit der Schema-Definition ("Ziel", "Status") gefüllt. -### 6.5 Schritt 4: Generation & Response +### 6.6 Schritt 4: Generation & Response * **Templating:** Das LLM erhält den Prompt basierend auf dem gewählten Template. -* **Execution:** Der `LLMService` führt den Call aus. Ein konfigurierbarer Timeout (`MINDNET_LLM_TIMEOUT`) fängt Cold-Start-Verzögerungen auf CPU-Hardware ab. +* **Execution:** Der `LLMService` führt den Call mit `priority="realtime"` aus. * **Response:** Rückgabe enthält Antworttext (im Interview-Modus: Markdown Codeblock), Quellenliste und den erkannten `intent`. --- @@ -337,8 +359,12 @@ Das Frontend ist eine **Streamlit-Anwendung** (`app/frontend/ui.py`), die als se ### 7.1 Kommunikation * **Backend-URL:** Konfiguriert via `MINDNET_API_URL` (Default: `http://localhost:8002`). -* **Endpoints:** Nutzt `/chat` für Interaktion, `/feedback` für Bewertungen und `/ingest/analyze` für Intelligence. -* **Resilienz:** Das Frontend implementiert eigene Timeouts (`MINDNET_API_TIMEOUT`, Default 300s). +* **Endpoints:** + * `/chat` (Interaktion) + * `/feedback` (Bewertungen) + * `/ingest/analyze` (Active Intelligence / Link-Vorschläge) + * `/ingest/save` (Persistence / Speichern im Vault) +* **Resilienz:** Das Frontend nutzt `MINDNET_API_TIMEOUT` (300s), um auf langsame Backend-Prozesse (Smart Edges) zu warten. ### 7.2 Features & UI-Logik * **State Management:** Session-State speichert Chat-Verlauf und `query_id`. @@ -347,16 +373,16 @@ Das Frontend ist eine **Streamlit-Anwendung** (`app/frontend/ui.py`), die als se * **Source Expanders:** Zeigt verwendete Chunks inkl. Score und "Why"-Explanation. * **Sidebar:** Zeigt Suchhistorie (Log-basiert) und Konfiguration. -### 7.3 Draft-Editor & Sanitizer (Neu in WP10a) +### 7.3 Draft-Editor & Healing Parser (Neu in WP10a/v2.5) Wenn der Intent `INTERVIEW` ist, rendert die UI statt einer Textblase den **Draft-Editor**. 1. **Parsing:** Die Funktion `parse_markdown_draft` extrahiert den Codeblock aus der LLM-Antwort. + * **Healing:** Erkennt und repariert defekte YAML-Frontmatter (z.B. fehlendes schließendes `---`), falls das LLM unter Last Fehler macht. 2. **Sanitization (`normalize_meta_and_body`):** * Prüft den YAML-Frontmatter auf unerlaubte Felder (Halluzinationen des LLMs). * Verschiebt ungültige Felder (z.B. "Situation") in den Body der Notiz. - * Stellt sicher, dass das Markdown valide bleibt. 3. **Editor Widget:** `st.text_area` erlaubt das Bearbeiten des Inhalts vor dem Speichern. -4. **Action:** Buttons zum Download oder Kopieren des fertigen Markdowns. +4. **Save Action:** Speichert über `/ingest/save` atomar in Vault und DB. Erzeugt intelligente Dateinamen via `slugify`. ### 7.4 State Management (Resurrection Pattern) Um Datenverlust bei Tab-Wechseln (Chat <-> Editor) zu verhindern, nutzt `ui.py` ein Persistenz-Muster: @@ -420,5 +446,5 @@ Validierung erfolgt über `tests/ensure_indexes_and_show.py`. 2. **Unresolved Targets:** * Kanten zu Notizen, die noch nicht existieren, werden mit `target_id="Titel"` angelegt. * Heilung durch `scripts/resolve_unresolved_references.py` möglich. -3. **Vektor-Konfiguration für Edges:** - * `mindnet_edges` hat aktuell keine Vektoren (`vectors = null`). Eine semantische Suche *auf Kanten* ist noch nicht möglich. \ No newline at end of file +3. **Hardware-Last bei Smart Import:** + * Der "Smart Edge" Import ist rechenintensiv. Trotz Traffic Control kann die Antwortzeit im Chat leicht steigen, wenn die GPU am VRAM-Limit arbeitet. \ No newline at end of file diff --git a/docs/pipeline_playbook.md b/docs/pipeline_playbook.md index ac1170a..492b9df 100644 --- a/docs/pipeline_playbook.md +++ b/docs/pipeline_playbook.md @@ -1,7 +1,7 @@ # mindnet v2.4 – Pipeline Playbook -**Datei:** `docs/mindnet_pipeline_playbook_v2.4.md` -**Stand:** 2025-12-11 -**Status:** **FINAL** (Inkl. Async Ingestion & Active Intelligence) +**Datei:** `docs/mindnet_pipeline_playbook_v2.6.md` +**Stand:** 2025-12-12 +**Status:** **FINAL** (Inkl. Smart Edges & Traffic Control) **Quellen:** `mindnet_v2_implementation_playbook.md`, `Handbuch.md`, `chunking_strategy.md`, `docs_mindnet_retriever.md`, `mindnet_admin_guide_v2.4.md`. --- @@ -12,7 +12,7 @@ - [](#) - [1. Zweck \& Einordnung](#1-zweck--einordnung) - [2. Die Import-Pipeline (Runbook)](#2-die-import-pipeline-runbook) - - [2.1 Der 12-Schritte-Prozess (Async)](#21-der-12-schritte-prozess-async) + - [2.1 Der 13-Schritte-Prozess (Async + Smart)](#21-der-13-schritte-prozess-async--smart) - [2.2 Standard-Betrieb (Inkrementell)](#22-standard-betrieb-inkrementell) - [2.3 Deployment \& Restart (Systemd)](#23-deployment--restart-systemd) - [2.4 Full Rebuild (Clean Slate)](#24-full-rebuild-clean-slate) @@ -21,12 +21,13 @@ - [3.2 Payload-Felder](#32-payload-felder) - [4. Edge-Erzeugung (Die V2-Logik)](#4-edge-erzeugung-die-v2-logik) - [4.1 Prioritäten \& Provenance](#41-prioritäten--provenance) - - [4.2 Typ-Defaults](#42-typ-defaults) + - [4.2 Smart Edge Allocation (WP15)](#42-smart-edge-allocation-wp15) + - [4.3 Typ-Defaults](#43-typ-defaults) - [5. Retriever, Chat \& Generation (RAG Pipeline)](#5-retriever-chat--generation-rag-pipeline) - [5.1 Retrieval (Hybrid)](#51-retrieval-hybrid) - [5.2 Intent Router (WP06/07)](#52-intent-router-wp0607) - [5.3 Context Enrichment](#53-context-enrichment) - - [5.4 Generation (LLM)](#54-generation-llm) + - [5.4 Generation (LLM) mit Traffic Control](#54-generation-llm-mit-traffic-control) - [5.5 Active Intelligence Pipeline (Neu in v2.4)](#55-active-intelligence-pipeline-neu-in-v24) - [6. Feedback \& Lernen (WP04c)](#6-feedback--lernen-wp04c) - [7. Quality Gates \& Tests](#7-quality-gates--tests) @@ -34,7 +35,7 @@ - [7.2 Smoke-Test (E2E)](#72-smoke-test-e2e) - [8. Ausblick \& Roadmap (Technische Skizzen)](#8-ausblick--roadmap-technische-skizzen) - [8.1 WP-08: Self-Tuning (Skizze)](#81-wp-08-self-tuning-skizze) - - [16. Workpackage Status (v2.4.0)](#16-workpackage-status-v240) + - [16. Workpackage Status (v2.6.0)](#16-workpackage-status-v260)
--- @@ -45,7 +46,7 @@ Dieses Playbook ist das zentrale operative Handbuch für die **mindnet-Pipeline* **Zielgruppe:** Dev/Ops, Tech-Leads. **Scope:** -* **Ist-Stand (WP01–WP11):** Async Import, Chunking, Edge-Erzeugung, Hybrider Retriever, RAG-Chat (Hybrid Router), Feedback Loop, Frontend, Draft Editor, Active Intelligence. +* **Ist-Stand (WP01–WP15):** Async Import, Smart Chunking, Edge-Erzeugung, Hybrider Retriever, RAG-Chat (Hybrid Router), Feedback Loop, Frontend, Draft Editor, Active Intelligence. * **Roadmap (Ausblick):** Technische Skizze für Self-Tuning (WP08). --- @@ -54,23 +55,32 @@ Dieses Playbook ist das zentrale operative Handbuch für die **mindnet-Pipeline* Der Import ist der kritischste Prozess ("Data Ingestion"). Er muss **deterministisch** und **idempotent** sein. Wir nutzen `scripts/import_markdown.py` als zentralen Entrypoint. -### 2.1 Der 12-Schritte-Prozess (Async) -Seit v2.3.10 läuft der Import **asynchron**, um Netzwerk-Blockaden bei der Embedding-Generierung zu vermeiden. +### 2.1 Der 13-Schritte-Prozess (Async + Smart) +Seit v2.6 läuft der Import vollständig asynchron, nutzt intelligente Kantenvalidierung (Smart Edges) und drosselt sich selbst ("Traffic Control"). 1. **Markdown lesen:** Rekursives Scannen des Vaults. 2. **Frontmatter extrahieren:** Validierung von Pflichtfeldern (`id`, `type`, `title`). 3. **Typauflösung:** Bestimmung des `type` via `types.yaml` (Prio: Frontmatter > Pfad > Default). 4. **Note-Payload generieren:** Erstellen des JSON-Objekts für `mindnet_notes`. -5. **Chunking anwenden:** Zerlegung des Textes basierend auf dem `chunk_profile` des Typs. -6. **Inline-Kanten finden:** Parsing von `[[rel:...]]` im Fließtext. -7. **Callout-Kanten finden:** Parsing von `> [!edge]` Blöcken. -8. **Default-Edges erzeugen:** Anwendung der `edge_defaults` aus der Typ-Registry. -9. **Strukturkanten erzeugen:** `belongs_to` (Chunk->Note), `next`/`prev` (Sequenz). -10. **Embedding & Upsert (Async):** - * Das System nutzt eine **Semaphore** (Limit: 5 Files concurrent), um Ollama nicht zu überlasten. +5. **Chunking anwenden:** Zerlegung des Textes basierend auf dem `chunk_profile` des Typs (z.B. `sliding_smart_edges`). +6. **Smart Edge Allocation (Neu in WP15):** + * Wenn in `types.yaml` aktiviert (`enable_smart_edge_allocation`): + * Der `SemanticAnalyzer` sendet jeden Chunk an das LLM. + * **Resilienz & Traffic Control:** + * **Priority:** Der Request nutzt `priority="background"`. + * **Semaphore:** Eine globale Semaphore (Limit: 2, konfigurierbar) verhindert System-Überlastung. + * **Retry & Backoff:** Bei Fehlern (Timeouts) wird bis zu 5-mal mit exponentieller Wartezeit wiederholt. + * **JSON Repair:** Der Analyzer erkennt fehlerhaftes JSON (z.B. Dict statt Liste) und repariert es automatisch. + * **Safety Fallback:** Schlagen alle Versuche fehl, werden die Kanten *allen* Chunks zugewiesen, um Datenverlust zu vermeiden. + * Ergebnis: Das LLM filtert irrelevante Links aus dem Chunk ("Broadcasting" verhindern). +7. **Inline-Kanten finden:** Parsing von `[[rel:...]]` im Fließtext. +8. **Callout-Kanten finden:** Parsing von `> [!edge]` Blöcken. +9. **Default-Edges erzeugen:** Anwendung der `edge_defaults` aus der Typ-Registry. +10. **Strukturkanten erzeugen:** `belongs_to`, `next`, `prev` (Sequenz). +11. **Embedding & Upsert (Async):** * Generierung der Vektoren via `nomic-embed-text` (768 Dim). -11. **Strict Mode:** Der Prozess bricht sofort ab, wenn ein Embedding leer ist oder die Dimension `0` hat. -12. **Diagnose:** Automatischer Check der Integrität nach dem Lauf. +12. **Strict Mode:** Der Prozess bricht sofort ab, wenn ein Embedding leer ist oder die Dimension `0` hat. +13. **Diagnose:** Automatischer Check der Integrität nach dem Lauf. ### 2.2 Standard-Betrieb (Inkrementell) Für regelmäßige Updates (z.B. Cronjob). Erkennt Änderungen via Hash. @@ -102,7 +112,7 @@ Nach einem Import oder Code-Update müssen die API-Prozesse neu gestartet werden sudo systemctl status mindnet-prod ### 2.4 Full Rebuild (Clean Slate) -Notwendig bei Änderungen an `types.yaml` (z.B. neue Chunk-Größen) oder beim Wechsel des Embedding-Modells (z.B. Update auf `nomic-embed-text`). +Notwendig bei Änderungen an `types.yaml` (z.B. Smart Edges an/aus) oder beim Wechsel des Embedding-Modells. **WICHTIG:** Vorher das Modell pullen, sonst schlägt der Import fehl! @@ -123,10 +133,10 @@ Das Chunking ist profilbasiert und typgesteuert. ### 3.1 Chunk-Profile In `types.yaml` definiert. Standard-Profile (in `chunk_config.py` implementiert): -* `short`: Max 128 Tokens (z.B. für Logs, Chats). -* `medium`: Max 256 Tokens (z.B. für Konzepte). -* `long`: Max 512 Tokens (z.B. für Essays, Projekte). -* `by_heading`: Trennt strikt an Überschriften. +* `sliding_short`: Max 128 Tokens (z.B. für Logs, Chats). +* `sliding_standard`: Max 512 Tokens (Standard für Massendaten). +* `sliding_smart_edges`: Sliding Window, optimiert für LLM-Analyse (Fließtext). +* `structured_smart_edges`: Trennt strikt an Überschriften (für strukturierte Daten). ### 3.2 Payload-Felder Jeder Chunk erhält zwei Text-Felder: @@ -137,7 +147,7 @@ Jeder Chunk erhält zwei Text-Felder: ## 4. Edge-Erzeugung (Die V2-Logik) -In v2.2 entstehen Kanten nach strenger Priorität. +In v2.6 entstehen Kanten nach strenger Priorität. ### 4.1 Prioritäten & Provenance Der Importer setzt `provenance`, `rule_id` und `confidence` automatisch: @@ -147,10 +157,17 @@ Der Importer setzt `provenance`, `rule_id` und `confidence` automatisch: | **1** | Inline | `[[rel:depends_on X]]` | `inline:rel` | ~0.95 | | **2** | Callout | `> [!edge] related_to: [[X]]` | `callout:edge` | ~0.90 | | **3** | Wikilink | `[[X]]` | `explicit:wikilink` | 1.00 | -| **4** | Default | *(via types.yaml)* | `edge_defaults:...` | ~0.70 | -| **5** | Struktur | *(automatisch)* | `structure:...` | 1.00 | +| **4** | Smart | *(via LLM Filter)* | `smart:llm_filter` | 0.90 | +| **5** | Default | *(via types.yaml)* | `edge_defaults:...` | ~0.70 | +| **6** | Struktur | *(automatisch)* | `structure:...` | 1.00 | -### 4.2 Typ-Defaults +### 4.2 Smart Edge Allocation (WP15) +Dieser Mechanismus löst das Problem, dass Chunks sonst alle Links der Note erben. +* **Prozess:** Der `SemanticAnalyzer` prüft jeden Chunk: "Ist Link X im Kontext von Chunk Y relevant?" +* **Ergebnis:** Kanten werden präzise an den Chunk gebunden, nicht global an die Note. +* **Steuerung:** Das Feature wird in `types.yaml` per Typ aktiviert (`enable_smart_edge_allocation: true`). + +### 4.3 Typ-Defaults Wenn in `types.yaml` für einen Typ `edge_defaults` definiert sind, werden diese **additiv** zu expliziten Links erzeugt. * *Beispiel:* Note Typ `project` verlinkt `[[Tool A]]`. * *Ergebnis:* Kante `references` (explizit) UND Kante `depends_on` (Default). @@ -165,25 +182,23 @@ Der Datenfluss endet nicht beim Finden. Er geht weiter bis zur Antwort. Der `/chat` Endpunkt nutzt **Hybrid Retrieval** (Semantic + Graph), um auch logisch verbundene, aber textlich unterschiedliche Notizen zu finden (z.B. Decisions zu einem Projekt). ### 5.2 Intent Router (WP06/07) -Der Request durchläuft den **Hybrid Router**: -1. **Fast Path:** Prüfung auf `trigger_keywords` aus `decision_engine.yaml`. -2. **Slow Path:** Falls kein Keyword matched und `llm_fallback_enabled=true`, klassifiziert das LLM den Intent. - * `FACT`: Wissen abfragen. - * `DECISION`: Rat suchen. - * `EMPATHY`: Trost suchen. - * `INTERVIEW`: Wissen eingeben (Neu in WP07). -3. **Result:** Auswahl der Strategie und der `inject_types` oder `schemas`. +Der Request durchläuft den **Hybrid Router v5**: +1. **Question Detection:** Ist es eine Frage (`?`, W-Wörter)? -> RAG Modus. Interviews werden hier blockiert. +2. **Keyword Scan:** Enthält es Keywords aus `types.yaml` (Objekt, z.B. "Projekt") oder `decision_engine.yaml` (Action, z.B. "erstellen")? -> INTERVIEW Modus. +3. **LLM Fallback:** Wenn unklar, entscheidet das LLM. ### 5.3 Context Enrichment Der Router (`chat.py`) reichert die gefundenen Chunks mit Metadaten an: * **Typ-Injection:** `[DECISION]`, `[PROJECT]`. * **Reasoning-Infos:** `(Score: 0.75)`. -### 5.4 Generation (LLM) +### 5.4 Generation (LLM) mit Traffic Control * **Engine:** Ollama (lokal). * **Modell:** `phi3:mini` (Standard). * **Prompting:** Template wird basierend auf Intent gewählt (`decision_template`, `interview_template` etc.). -* **One-Shot (WP07):** Im Interview-Modus generiert das LLM direkt einen Markdown-Block ohne Rückfragen. +* **Traffic Control:** Der `LLMService` unterscheidet: + * **Chat-Requests** (`priority="realtime"`) -> Sofortige Ausführung. + * **Import-Requests** (`priority="background"`) -> Gedrosselt durch Semaphore (Standard: 2). ### 5.5 Active Intelligence Pipeline (Neu in v2.4) Ein paralleler Datenfluss im Frontend ("Draft Editor") zur Unterstützung des Autors. @@ -252,7 +267,7 @@ Wie entwickeln wir die Pipeline weiter? --- -## 16. Workpackage Status (v2.4.0) +## 16. Workpackage Status (v2.6.0) Aktueller Implementierungsstand der Module. @@ -270,4 +285,7 @@ Aktueller Implementierungsstand der Module. | **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. | | **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). | | **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** | -| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** | \ No newline at end of file +| **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. | \ No newline at end of file diff --git a/docs/user_guide.md b/docs/user_guide.md index 8f77940..cceb621 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -1,7 +1,7 @@ # Mindnet v2.4 – User Guide -**Datei:** `docs/mindnet_user_guide_v2.4.md` -**Stand:** 2025-12-11 -**Status:** **FINAL** (Inkl. RAG, Web-Interface & Interview-Assistent & Intelligence) +**Datei:** `docs/mindnet_user_guide_v2.6.md` +**Stand:** 2025-12-12 +**Status:** **FINAL** (Inkl. Smart Edges, Hybrid Router v5 & Healing UI) **Quellen:** `knowledge_design.md`, `wp04_retriever_scoring.md`, `Programmplan_V2.2.md`, `Handbuch.md`. > **Willkommen bei Mindnet.** @@ -18,6 +18,7 @@ Wenn du nach "Projekt Alpha" suchst, findet Mindnet nicht nur das Dokument. Es f * **Abhängigkeiten:** "Technologie X wird benötigt". * **Entscheidungen:** "Warum nutzen wir X?". * **Ähnliches:** "Projekt Beta war ähnlich". +* **Neu in v2.6 (Smart Edges):** Mindnet zeigt dir präzise Verknüpfungen an, die für den spezifischen Textabschnitt relevant sind, statt pauschal alle Links der Notiz anzuzeigen. ### 1.2 Der Zwilling (Die Personas) Mindnet passt seinen Charakter dynamisch an deine Frage an: @@ -52,28 +53,22 @@ Seit Version 2.3.1 bedienst du Mindnet über eine grafische Oberfläche im Brows ## 3. Den Chat steuern (Intents) -Du steuerst die Persönlichkeit von Mindnet durch deine Wortwahl. Das System nutzt einen **Hybrid Router**, der sowohl auf Schlüsselwörter als auch auf die Bedeutung achtet. +Du steuerst die Persönlichkeit von Mindnet durch deine Wortwahl. Das System nutzt einen **Hybrid Router v5**, der intelligent zwischen Frage und Befehl unterscheidet. -### 3.1 Modus: Entscheidung ("Der Berater") -Wenn du vor einer Wahl stehst, hilft Mindnet dir, konform zu deinen Prinzipien zu bleiben. +### 3.1 Frage-Modus (Wissen abrufen) +Sobald du ein Fragezeichen `?` benutzt oder Wörter wie "Wer", "Wie", "Was", "Soll ich" verwendest, sucht Mindnet nach Antworten (**RAG**). -* **Auslöser (Keywords):** "Soll ich...", "Was ist deine Meinung?", "Strategie für...", "Vor- und Nachteile". -* **Was passiert:** Mindnet lädt deine **Werte** (`type: value`) und **Ziele** (`type: goal`) in den Kontext und prüft die Fakten dagegen. -* **Beispiel-Dialog:** - * *Du:* "Soll ich Tool X nutzen?" - * *Mindnet:* "Nein. Tool X speichert Daten in den USA. Das verstößt gegen dein Prinzip 'Privacy First' und dein Ziel 'Digitale Autarkie'." +* **Entscheidung ("Soll ich?"):** Mindnet lädt deine **Werte** (`type: value`) und **Ziele** (`type: goal`) in den Kontext und prüft die Fakten dagegen. + * *Beispiel:* "Soll ich Tool X nutzen?" -> "Nein, Tool X speichert Daten in den USA. Das verstößt gegen dein Prinzip 'Privacy First' und dein Ziel 'Digitale Autarkie'." +* **Empathie ("Ich fühle..."):** Mindnet lädt deine **Erfahrungen** (`type: experience`) und **Glaubenssätze** (`type: belief`). Es antwortet verständnisvoll und zitiert deine eigenen Lektionen. + * *Beispiel:* "Ich bin frustriert." -> "Das erinnert mich an Projekt Y, da ging es uns ähnlich..." -### 3.2 Modus: Empathie ("Der Spiegel") -Wenn du frustriert bist oder reflektieren willst, wechselt Mindnet in den "Ich"-Modus. - -* **Auslöser (Keywords & Semantik):** "Ich fühle mich...", "Traurig", "Gestresst", "Alles ist sinnlos", "Ich bin überfordert". -* **Was passiert:** Mindnet lädt deine **Erfahrungen** (`type: experience`) und **Glaubenssätze** (`type: belief`). Es antwortet verständnisvoll und zitiert deine eigenen Lektionen. - -### 3.3 Modus: Interview ("Der Analyst") -Wenn du Wissen festhalten willst, statt zu suchen. +### 3.2 Befehls-Modus (Wissen erfassen / Interview) +Wenn du keine Frage stellst, sondern eine Absicht äußerst, wechselt Mindnet in den **Interview-Modus**. * **Auslöser:** "Neues Projekt", "Notiz erstellen", "Ich will etwas festhalten", "Neue Entscheidung dokumentieren". -* **Was passiert:** Siehe Kapitel 6.3. +* **Was passiert:** Mindnet sucht nicht im Archiv, sondern öffnet den **Draft-Editor**. +* **Beispiel:** "Neue Erfahrung: Streit am Recyclinghof." -> Das System erstellt sofort eine strukturierte Notiz mit den Feldern "Situation", "Reaktion" und "Learning". --- @@ -127,9 +122,10 @@ Mindnet kann dir helfen, Markdown-Notizen zu schreiben. 2. **Generierung:** Mindnet erkennt den Wunsch (`INTERVIEW`), analysiert den Satz und erstellt **sofort** einen Entwurf. 3. **Editor:** Die UI wechselt von der Chat-Blase zu einem **Draft-Editor**. * Du siehst das generierte Frontmatter (`type: project`, `status: draft`). + * **Healing UI (v2.5):** Falls das LLM Syntax-Fehler gemacht hat (z.B. fehlendes `---`), hat der Editor das bereits automatisch repariert. * Du siehst den Body-Text mit Platzhaltern (`[TODO]`), wo Infos fehlten (z.B. Stakeholder). 4. **Finalisierung:** Ergänze die fehlenden Infos direkt im Editor und klicke auf **Download** oder **Kopieren**. -5. **Speichern:** Speichere die Datei in deinen Obsidian Vault. Beim nächsten Import ist sie im System. +5. **Speichern:** Klicke auf "💾 Speichern". Die Notiz landet sofort im Vault und im Index. ### 6.4 Der Intelligence-Workflow (Neu in v2.4) Wenn du Texte im **manuellen Editor** schreibst, unterstützt dich Mindnet aktiv bei der Vernetzung: diff --git a/scripts/import_markdown.py b/scripts/import_markdown.py index da86fc0..d5ce195 100644 --- a/scripts/import_markdown.py +++ b/scripts/import_markdown.py @@ -11,6 +11,13 @@ import logging from pathlib import Path from dotenv import load_dotenv +import logging +# Setzt das Level global auf INFO, damit Sie den Fortschritt sehen +logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s') + +# Wenn Sie TIEFE Einblicke wollen, setzen Sie den SemanticAnalyzer spezifisch auf DEBUG: +logging.getLogger("app.services.semantic_analyzer").setLevel(logging.DEBUG) + # Importiere den neuen Async Service # Stellen wir sicher, dass der Pfad stimmt (Pythonpath) import sys diff --git a/tests/test_dialog_full_flow.py b/tests/test_dialog_full_flow.py new file mode 100644 index 0000000..4b0d163 --- /dev/null +++ b/tests/test_dialog_full_flow.py @@ -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() \ No newline at end of file diff --git a/tests/test_final_wp15_validation.py b/tests/test_final_wp15_validation.py new file mode 100644 index 0000000..def57c0 --- /dev/null +++ b/tests/test_final_wp15_validation.py @@ -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() \ No newline at end of file diff --git a/tests/test_smart_chunking_integration.py b/tests/test_smart_chunking_integration.py new file mode 100644 index 0000000..06a489d --- /dev/null +++ b/tests/test_smart_chunking_integration.py @@ -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() \ No newline at end of file diff --git a/tests/test_wp15_final.py b/tests/test_wp15_final.py new file mode 100644 index 0000000..93df4f1 --- /dev/null +++ b/tests/test_wp15_final.py @@ -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() \ No newline at end of file