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