Merge pull request 'WP15' (#9) from WP15 into main
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 3s
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 3s
Reviewed-on: #9 ### MERGE: WP-15 Smart Edge Allocation & Traffic Control (v2.6.0) **Zusammenfassung:** Dieses Merge Request schließt das Workpackage WP-15 ab. Die Kernziele – präzisere Kanten-Zuweisung und System-Stabilität unter LLM-Last – wurden erreicht. Die Implementierung der LLM-basierten Smart Edges ist damit produktionsreif. **Kern-Features:** 1. **Smart Edge Allocation:** Kanten werden nun LLM-geprüft und nur an die spezifischen Chunks gebunden, in denen der verlinkte Inhalt relevant ist. Das reduziert "Beifang" im Retrieval und erhöht die Präzision. * Steuerung: Über `types.yaml` (`enable_smart_edge_allocation`). 2. **Traffic Control & Resilienz:** Der `LLMService` implementiert Prioritäts-Warteschlangen (`priority="realtime"` vs. `"background"`). * **Ergebnis:** Chat-Anfragen haben Vorfahrt und das System stürzt während rechenintensiver Importe nicht mehr mit Timeouts ab. Die Parallelität des Imports ist über `MINDNET_LLM_BACKGROUND_LIMIT` konfigurierbar. 3. **UI Robustheit:** Der "Healing Parser" im Frontend (`ui.py`) repariert automatisch kaputte YAML-Frontmatter in LLM-generierten Drafts. **Betroffene Komponenten:** * `app/services/llm_service.py` (Traffic Control / Semaphore) * `app/core/chunker.py` / `app/services/semantic_analyzer.py` (Smart Edge Logik / Retry) * `app/routers/chat.py` (Hybrid Router v5 / Question Detection) * `app/frontend/ui.py` (Healing Parser / API-Timeouts) * `config/types.yaml` (Neue Profile & Smart Edge Flag) **Aktionen nach Merge:** * **Admins:** `MINDNET_LLM_BACKGROUND_LIMIT` in der Produktions-.env prüfen und ggf. anpassen. * **Initialisierung:** Bei Änderung der Chunking-Profile ist ein Full Rebuild notwendig.
This commit is contained in:
commit
d2e0b48aa5
|
|
@ -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 | 🟡 |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
||||
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))
|
||||
fm, text_without_fm = extract_frontmatter_from_text(md_text)
|
||||
|
||||
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]
|
||||
h1_match = re.search(r'^#\s+(.*)', text_without_fm, re.MULTILINE)
|
||||
if h1_match:
|
||||
h1_title = h1_match.group(1).strip()
|
||||
|
||||
if t.type in ("fence", "code_block"):
|
||||
# Codeblock hat eigenen content im selben Token
|
||||
content = t.content or ""
|
||||
push(kind, content, None)
|
||||
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:
|
||||
# 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
|
||||
buffer.append(line)
|
||||
|
||||
return blocks
|
||||
if buffer:
|
||||
content = "\n".join(buffer).strip()
|
||||
if content:
|
||||
blocks.append(RawBlock("paragraph", content, None, section_path, current_h2))
|
||||
|
||||
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
|
||||
return blocks, h1_title
|
||||
|
||||
blocks = parse_blocks(md_text)
|
||||
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 = []
|
||||
|
||||
chunks: List[Chunk] = []
|
||||
buf: List[Tuple[str, str, str]] = [] # (text, section_title, section_path)
|
||||
char_pos = 0
|
||||
|
||||
def flush_buffer(force=False):
|
||||
nonlocal buf, chunks, char_pos
|
||||
if not buf:
|
||||
return
|
||||
text = "\n\n".join([b[0] for b in buf]).strip()
|
||||
if not text:
|
||||
buf = []
|
||||
return
|
||||
|
||||
# Wenn zu groß, satzbasiert weich umbrechen
|
||||
toks = estimate_tokens(text)
|
||||
if toks > max_tokens:
|
||||
sentences = split_sentences(text)
|
||||
cur = []
|
||||
cur_tokens = 0
|
||||
for s in sentences:
|
||||
st = estimate_tokens(s)
|
||||
if cur_tokens + st > target and cur:
|
||||
_emit("\n".join(cur))
|
||||
# Overlap: letzte Sätze wiederverwenden
|
||||
ov_text = " ".join(cur)[-overlap*4:] # 4 chars/token Heuristik
|
||||
cur = [ov_text, s] if ov_text else [s]
|
||||
cur_tokens = estimate_tokens(" ".join(cur))
|
||||
else:
|
||||
cur.append(s)
|
||||
cur_tokens += st
|
||||
if cur:
|
||||
_emit("\n".join(cur))
|
||||
else:
|
||||
_emit(text)
|
||||
buf = []
|
||||
|
||||
def _emit(text_block: str):
|
||||
nonlocal chunks, char_pos
|
||||
def _create_chunk(txt, win, sec, path):
|
||||
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
|
||||
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=[]
|
||||
))
|
||||
char_pos = end + 1
|
||||
|
||||
# Blocks in Puffer sammeln; bei Überschreiten Zielbereich flushen
|
||||
cur_sec_title = None
|
||||
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 = []
|
||||
|
||||
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
|
||||
|
|
@ -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,21 +17,13 @@ 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 []
|
||||
|
||||
from app.core.qdrant import QdrantConfig, get_client, ensure_collections, ensure_payload_indexes
|
||||
|
|
@ -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:
|
||||
|
|
@ -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,31 +277,25 @@ 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}"}
|
||||
|
||||
# 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,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import os
|
|||
import json
|
||||
import re
|
||||
import yaml
|
||||
import unicodedata
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
|
@ -23,7 +24,7 @@ timeout_setting = os.getenv("MINDNET_API_TIMEOUT") or os.getenv("MINDNET_LLM_TIM
|
|||
API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0
|
||||
|
||||
# --- PAGE SETUP ---
|
||||
st.set_page_config(page_title="mindnet v2.3.10", page_icon="🧠", layout="wide")
|
||||
st.set_page_config(page_title="mindnet v2.5", page_icon="🧠", layout="wide")
|
||||
|
||||
# --- CSS STYLING ---
|
||||
st.markdown("""
|
||||
|
|
@ -53,15 +54,6 @@ st.markdown("""
|
|||
background-color: white;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
|
||||
}
|
||||
|
||||
.suggestion-card {
|
||||
border-left: 3px solid #1a73e8;
|
||||
background-color: #ffffff;
|
||||
padding: 10px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
|
|
@ -71,8 +63,18 @@ if "user_id" not in st.session_state: st.session_state.user_id = str(uuid.uuid4(
|
|||
|
||||
# --- HELPER FUNCTIONS ---
|
||||
|
||||
def slugify(value):
|
||||
if not value: return ""
|
||||
value = str(value).lower()
|
||||
replacements = {'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 'ß': 'ss', '&': 'und', '+': 'und'}
|
||||
for k, v in replacements.items():
|
||||
value = value.replace(k, v)
|
||||
|
||||
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
|
||||
value = re.sub(r'[^\w\s-]', '', value).strip()
|
||||
return re.sub(r'[-\s]+', '-', value)
|
||||
|
||||
def normalize_meta_and_body(meta, body):
|
||||
"""Sanitizer: Stellt sicher, dass nur erlaubte Felder im Frontmatter bleiben."""
|
||||
ALLOWED_KEYS = {"title", "type", "status", "tags", "id", "created", "updated", "aliases", "lang"}
|
||||
clean_meta = {}
|
||||
extra_content = []
|
||||
|
|
@ -99,7 +101,11 @@ def normalize_meta_and_body(meta, body):
|
|||
extra_content.append(f"## {header}\n{val}\n")
|
||||
|
||||
if all_tags:
|
||||
clean_meta["tags"] = list(set(all_tags))
|
||||
clean_tags = []
|
||||
for t in all_tags:
|
||||
t_clean = str(t).replace("#", "").strip()
|
||||
if t_clean: clean_tags.append(t_clean)
|
||||
clean_meta["tags"] = list(set(clean_tags))
|
||||
|
||||
if extra_content:
|
||||
new_section = "\n".join(extra_content)
|
||||
|
|
@ -110,37 +116,67 @@ def normalize_meta_and_body(meta, body):
|
|||
return clean_meta, final_body
|
||||
|
||||
def parse_markdown_draft(full_text):
|
||||
"""Robustes Parsing + Sanitization."""
|
||||
clean_text = full_text
|
||||
"""
|
||||
HEALING PARSER: Repariert kaputten LLM Output (z.B. fehlendes schließendes '---').
|
||||
"""
|
||||
clean_text = full_text.strip()
|
||||
|
||||
pattern_block = r"```(?:markdown|md)?\s*(.*?)\s*```"
|
||||
match_block = re.search(pattern_block, full_text, re.DOTALL | re.IGNORECASE)
|
||||
# 1. Code-Block Wrapper entfernen
|
||||
pattern_block = r"```(?:markdown|md|yaml)?\s*(.*?)\s*```"
|
||||
match_block = re.search(pattern_block, clean_text, re.DOTALL | re.IGNORECASE)
|
||||
if match_block:
|
||||
clean_text = match_block.group(1).strip()
|
||||
|
||||
parts = re.split(r"^---+\s*$", clean_text, maxsplit=2, flags=re.MULTILINE)
|
||||
|
||||
meta = {}
|
||||
body = clean_text
|
||||
yaml_str = ""
|
||||
|
||||
# 2. Versuch A: Standard Split (Idealfall)
|
||||
parts = re.split(r"^---+\s*$", clean_text, maxsplit=2, flags=re.MULTILINE)
|
||||
|
||||
if len(parts) >= 3:
|
||||
yaml_str = parts[1]
|
||||
body_candidate = parts[2]
|
||||
body = parts[2]
|
||||
|
||||
# 3. Versuch B: Healing (Wenn LLM das schließende --- vergessen hat)
|
||||
elif clean_text.startswith("---"):
|
||||
# Wir suchen die erste Überschrift '#', da Frontmatter davor sein muss
|
||||
# Pattern: Suche --- am Anfang, dann nimm alles bis zum ersten # am Zeilenanfang
|
||||
fallback_match = re.search(r"^---\s*(.*?)(?=\n#)", clean_text, re.DOTALL | re.MULTILINE)
|
||||
if fallback_match:
|
||||
yaml_str = fallback_match.group(1)
|
||||
# Der Body ist alles NACH dem YAML String (inklusive dem #)
|
||||
body = clean_text.replace(f"---{yaml_str}", "", 1).strip()
|
||||
|
||||
# 4. YAML Parsing
|
||||
if yaml_str:
|
||||
yaml_str_clean = yaml_str.replace("#", "") # Tags cleanen
|
||||
try:
|
||||
parsed = yaml.safe_load(yaml_str)
|
||||
parsed = yaml.safe_load(yaml_str_clean)
|
||||
if isinstance(parsed, dict):
|
||||
meta = parsed
|
||||
body = body_candidate.strip()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"YAML Parsing Warning: {e}")
|
||||
|
||||
# Fallback: Titel aus H1
|
||||
if not meta.get("title"):
|
||||
h1_match = re.search(r"^#\s+(.*)$", body, re.MULTILINE)
|
||||
if h1_match:
|
||||
meta["title"] = h1_match.group(1).strip()
|
||||
|
||||
# Correction: type/status swap
|
||||
if meta.get("type") == "draft":
|
||||
meta["status"] = "draft"
|
||||
meta["type"] = "experience"
|
||||
|
||||
return normalize_meta_and_body(meta, body)
|
||||
|
||||
def build_markdown_doc(meta, body):
|
||||
"""Baut das finale Dokument zusammen."""
|
||||
if "id" not in meta or meta["id"] == "generated_on_save":
|
||||
safe_title = re.sub(r'[^a-zA-Z0-9]', '-', meta.get('title', 'note')).lower()[:30]
|
||||
meta["id"] = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}-{uuid.uuid4().hex[:4]}"
|
||||
raw_title = meta.get('title', 'note')
|
||||
clean_slug = slugify(raw_title)[:50] or "note"
|
||||
meta["id"] = f"{datetime.now().strftime('%Y%m%d')}-{clean_slug}"
|
||||
|
||||
meta["updated"] = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
|
|
@ -189,7 +225,6 @@ def send_chat_message(message: str, top_k: int, explain: bool):
|
|||
return {"error": str(e)}
|
||||
|
||||
def analyze_draft_text(text: str, n_type: str):
|
||||
"""Ruft den neuen Intelligence-Service (WP-11) auf."""
|
||||
try:
|
||||
response = requests.post(
|
||||
INGEST_ANALYZE_ENDPOINT,
|
||||
|
|
@ -202,12 +237,11 @@ def analyze_draft_text(text: str, n_type: str):
|
|||
return {"error": str(e)}
|
||||
|
||||
def save_draft_to_vault(markdown_content: str, filename: str = None):
|
||||
"""Ruft den neuen Persistence-Service (WP-11) auf."""
|
||||
try:
|
||||
response = requests.post(
|
||||
INGEST_SAVE_ENDPOINT,
|
||||
json={"markdown_content": markdown_content, "filename": filename},
|
||||
timeout=60
|
||||
timeout=API_TIMEOUT
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
|
@ -225,7 +259,7 @@ def submit_feedback(query_id, node_id, score, comment=None):
|
|||
def render_sidebar():
|
||||
with st.sidebar:
|
||||
st.title("🧠 mindnet")
|
||||
st.caption("v2.3.10 | Mode Switch Fix")
|
||||
st.caption("v2.5 | Healing Parser")
|
||||
mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0)
|
||||
st.divider()
|
||||
st.subheader("⚙️ Settings")
|
||||
|
|
@ -240,7 +274,6 @@ def render_sidebar():
|
|||
return mode, top_k, explain
|
||||
|
||||
def render_draft_editor(msg):
|
||||
# Ensure ID Stability
|
||||
if "query_id" not in msg or not msg["query_id"]:
|
||||
msg["query_id"] = str(uuid.uuid4())
|
||||
|
||||
|
|
@ -253,7 +286,7 @@ def render_draft_editor(msg):
|
|||
widget_body_key = f"{key_base}_widget_body"
|
||||
data_body_key = f"{key_base}_data_body"
|
||||
|
||||
# --- 1. INIT STATE (Nur beim allerersten Laden der Message) ---
|
||||
# --- 1. INIT STATE ---
|
||||
if f"{key_base}_init" not in st.session_state:
|
||||
meta, body = parse_markdown_draft(msg["content"])
|
||||
if "type" not in meta: meta["type"] = "default"
|
||||
|
|
@ -261,26 +294,34 @@ def render_draft_editor(msg):
|
|||
tags = meta.get("tags", [])
|
||||
meta["tags_str"] = ", ".join(tags) if isinstance(tags, list) else str(tags)
|
||||
|
||||
# Persistent Data (Source of Truth)
|
||||
# Persistent Data
|
||||
st.session_state[data_meta_key] = meta
|
||||
st.session_state[data_sugg_key] = []
|
||||
st.session_state[data_body_key] = body.strip()
|
||||
|
||||
# Init Widgets Keys
|
||||
st.session_state[f"{key_base}_wdg_title"] = meta["title"]
|
||||
st.session_state[f"{key_base}_wdg_type"] = meta["type"]
|
||||
st.session_state[f"{key_base}_wdg_tags"] = meta["tags_str"]
|
||||
|
||||
st.session_state[f"{key_base}_init"] = True
|
||||
|
||||
# --- 2. RESURRECTION FIX (WICHTIG!) ---
|
||||
# Wenn wir vom Manuellen Editor zurückkommen, wurde der widget_key von Streamlit gelöscht.
|
||||
# Wir müssen ihn aus dem persistenten data_body_key wiederherstellen.
|
||||
# --- 2. RESURRECTION ---
|
||||
if widget_body_key not in st.session_state and data_body_key in st.session_state:
|
||||
st.session_state[widget_body_key] = st.session_state[data_body_key]
|
||||
|
||||
# --- CALLBACKS ---
|
||||
def _sync_meta():
|
||||
meta = st.session_state[data_meta_key]
|
||||
meta["title"] = st.session_state.get(f"{key_base}_wdg_title", "")
|
||||
meta["type"] = st.session_state.get(f"{key_base}_wdg_type", "default")
|
||||
meta["tags_str"] = st.session_state.get(f"{key_base}_wdg_tags", "")
|
||||
st.session_state[data_meta_key] = meta
|
||||
|
||||
def _sync_body():
|
||||
# Sync Widget -> Data (Source of Truth)
|
||||
st.session_state[data_body_key] = st.session_state[widget_body_key]
|
||||
|
||||
def _insert_text(text_to_insert):
|
||||
# Insert in Widget Key und Sync Data
|
||||
current = st.session_state.get(widget_body_key, "")
|
||||
new_text = f"{current}\n\n{text_to_insert}"
|
||||
st.session_state[widget_body_key] = new_text
|
||||
|
|
@ -296,33 +337,23 @@ def render_draft_editor(msg):
|
|||
st.markdown(f'<div class="draft-box">', unsafe_allow_html=True)
|
||||
st.markdown("### 📝 Entwurf bearbeiten")
|
||||
|
||||
# Metadata Form
|
||||
meta_ref = st.session_state[data_meta_key]
|
||||
c1, c2 = st.columns([2, 1])
|
||||
with c1:
|
||||
# Auch hier Keys für Widgets nutzen, um Resets zu vermeiden
|
||||
title_key = f"{key_base}_wdg_title"
|
||||
if title_key not in st.session_state: st.session_state[title_key] = meta_ref["title"]
|
||||
meta_ref["title"] = st.text_input("Titel", key=title_key)
|
||||
st.text_input("Titel", key=f"{key_base}_wdg_title", on_change=_sync_meta)
|
||||
|
||||
with c2:
|
||||
known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle"]
|
||||
curr = meta_ref["type"]
|
||||
if curr not in known_types: known_types.append(curr)
|
||||
type_key = f"{key_base}_wdg_type"
|
||||
if type_key not in st.session_state: st.session_state[type_key] = meta_ref["type"]
|
||||
meta_ref["type"] = st.selectbox("Typ", known_types, index=known_types.index(curr) if curr in known_types else 0, key=type_key)
|
||||
known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle", "risk", "belief"]
|
||||
curr_type = st.session_state.get(f"{key_base}_wdg_type", meta_ref["type"])
|
||||
if curr_type not in known_types: known_types.append(curr_type)
|
||||
st.selectbox("Typ", known_types, key=f"{key_base}_wdg_type", on_change=_sync_meta)
|
||||
|
||||
tags_key = f"{key_base}_wdg_tags"
|
||||
if tags_key not in st.session_state: st.session_state[tags_key] = meta_ref.get("tags_str", "")
|
||||
meta_ref["tags_str"] = st.text_input("Tags", key=tags_key)
|
||||
st.text_input("Tags", key=f"{key_base}_wdg_tags", on_change=_sync_meta)
|
||||
|
||||
# Tabs
|
||||
tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"])
|
||||
|
||||
# --- TAB 1: EDITOR ---
|
||||
with tab_edit:
|
||||
# Hier kein 'value' Argument mehr, da wir den Key oben (Resurrection) initialisiert haben.
|
||||
st.text_area(
|
||||
"Body",
|
||||
key=widget_body_key,
|
||||
|
|
@ -338,11 +369,11 @@ def render_draft_editor(msg):
|
|||
if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"):
|
||||
st.session_state[data_sugg_key] = []
|
||||
|
||||
# Lese vom Widget (aktuell) oder Data (Fallback)
|
||||
text_to_analyze = st.session_state.get(widget_body_key, st.session_state.get(data_body_key, ""))
|
||||
current_doc_type = st.session_state.get(f"{key_base}_wdg_type", "concept")
|
||||
|
||||
with st.spinner("Analysiere..."):
|
||||
analysis = analyze_draft_text(text_to_analyze, meta_ref["type"])
|
||||
analysis = analyze_draft_text(text_to_analyze, current_doc_type)
|
||||
|
||||
if "error" in analysis:
|
||||
st.error(f"Fehler: {analysis['error']}")
|
||||
|
|
@ -354,7 +385,6 @@ def render_draft_editor(msg):
|
|||
else:
|
||||
st.success(f"{len(suggestions)} Vorschläge gefunden.")
|
||||
|
||||
# Render List
|
||||
suggestions = st.session_state[data_sugg_key]
|
||||
if suggestions:
|
||||
current_text_state = st.session_state.get(widget_body_key, "")
|
||||
|
|
@ -380,16 +410,24 @@ def render_draft_editor(msg):
|
|||
st.button("➕ Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,))
|
||||
|
||||
# --- TAB 3: SAVE ---
|
||||
final_tags = [t.strip() for t in meta_ref["tags_str"].split(",") if t.strip()]
|
||||
final_tags_str = st.session_state.get(f"{key_base}_wdg_tags", "")
|
||||
final_tags = [t.strip() for t in final_tags_str.split(",") if t.strip()]
|
||||
|
||||
final_meta = {
|
||||
"id": "generated_on_save",
|
||||
"type": meta_ref["type"],
|
||||
"title": meta_ref["title"],
|
||||
"type": st.session_state.get(f"{key_base}_wdg_type", "default"),
|
||||
"title": st.session_state.get(f"{key_base}_wdg_title", "").strip(),
|
||||
"status": "draft",
|
||||
"tags": final_tags
|
||||
}
|
||||
# Final Doc aus Data
|
||||
|
||||
final_body = st.session_state.get(widget_body_key, st.session_state[data_body_key])
|
||||
|
||||
if not final_meta["title"]:
|
||||
h1_match = re.search(r"^#\s+(.*)$", final_body, re.MULTILINE)
|
||||
if h1_match:
|
||||
final_meta["title"] = h1_match.group(1).strip()
|
||||
|
||||
final_doc = build_markdown_doc(final_meta, final_body)
|
||||
|
||||
with tab_view:
|
||||
|
|
@ -403,7 +441,13 @@ def render_draft_editor(msg):
|
|||
with b1:
|
||||
if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"):
|
||||
with st.spinner("Speichere im Vault..."):
|
||||
safe_title = re.sub(r'[^a-zA-Z0-9]', '-', meta_ref["title"]).lower()[:30] or "draft"
|
||||
|
||||
raw_title = final_meta.get("title", "")
|
||||
if not raw_title:
|
||||
clean_body = re.sub(r"[#*_\[\]()]", "", final_body).strip()
|
||||
raw_title = clean_body[:40] if clean_body else "draft"
|
||||
|
||||
safe_title = slugify(raw_title)[:60] or "draft"
|
||||
fname = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md"
|
||||
|
||||
result = save_draft_to_vault(final_doc, filename=fname)
|
||||
|
|
@ -423,7 +467,6 @@ def render_chat_interface(top_k, explain):
|
|||
for idx, msg in enumerate(st.session_state.messages):
|
||||
with st.chat_message(msg["role"]):
|
||||
if msg["role"] == "assistant":
|
||||
# Header
|
||||
intent = msg.get("intent", "UNKNOWN")
|
||||
src = msg.get("intent_source", "?")
|
||||
icon = {"EMPATHY":"❤️", "DECISION":"⚖️", "CODING":"💻", "FACT":"📚", "INTERVIEW":"📝"}.get(intent, "🧠")
|
||||
|
|
@ -432,13 +475,11 @@ def render_chat_interface(top_k, explain):
|
|||
with st.expander("🐞 Debug Raw Payload", expanded=False):
|
||||
st.json(msg)
|
||||
|
||||
# Logic
|
||||
if intent == "INTERVIEW":
|
||||
render_draft_editor(msg)
|
||||
else:
|
||||
st.markdown(msg["content"])
|
||||
|
||||
# Sources
|
||||
if "sources" in msg and msg["sources"]:
|
||||
for hit in msg["sources"]:
|
||||
with st.expander(f"📄 {hit.get('note_id', '?')} ({hit.get('total_score', 0):.2f})"):
|
||||
|
|
|
|||
|
|
@ -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,39 +77,39 @@ 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 "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).
|
||||
|
||||
# 2. SLOW PATH: LLM Router
|
||||
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})"
|
||||
|
||||
# 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
|
||||
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()
|
||||
found_intents = []
|
||||
|
||||
# Zuerst INTERVIEW prüfen
|
||||
if "INTERVIEW" in llm_output_upper or "CREATE" in llm_output_upper:
|
||||
return "INTERVIEW", "LLM Router"
|
||||
|
||||
for strat_key in strategies.keys():
|
||||
if strat_key in llm_output_upper:
|
||||
found_intents.append(strat_key)
|
||||
return strat_key, "LLM Router"
|
||||
|
||||
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)"
|
||||
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", "")
|
||||
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 = active_schema # Fallback falls nur Liste definiert
|
||||
hint_str = ""
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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
|
||||
|
||||
attempt = 0
|
||||
|
||||
while True:
|
||||
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."
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return data.get("response", "").strip()
|
||||
else:
|
||||
response.raise_for_status()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM Raw Gen Error: {e}")
|
||||
return "Interner LLM Fehler."
|
||||
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):
|
||||
if self.client:
|
||||
await self.client.aclose()
|
||||
138
app/services/semantic_analyzer.py
Normal file
138
app/services/semantic_analyzer.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""
|
||||
app/services/semantic_analyzer.py — Edge Validation & Filtering
|
||||
Version: 2.0 (Update: Background Priority for Batch Jobs)
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Importe
|
||||
from app.services.llm_service import LLMService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SemanticAnalyzer:
|
||||
def __init__(self):
|
||||
self.llm = LLMService()
|
||||
|
||||
async def assign_edges_to_chunk(self, chunk_text: str, all_edges: List[str], note_type: str) -> List[str]:
|
||||
"""
|
||||
Sendet einen Chunk und eine Liste potenzieller Kanten an das LLM.
|
||||
Das LLM filtert heraus, welche Kanten für diesen Chunk relevant sind.
|
||||
|
||||
Features:
|
||||
- Retry Strategy: Wartet bei Überlastung (max_retries=5).
|
||||
- Priority Queue: Läuft als "background" Task, um den Chat nicht zu blockieren.
|
||||
- Observability: Loggt Input-Größe, Raw-Response und Parsing-Details.
|
||||
"""
|
||||
if not all_edges:
|
||||
return []
|
||||
|
||||
# 1. Prompt laden
|
||||
prompt_template = self.llm.prompts.get("edge_allocation_template")
|
||||
|
||||
if not prompt_template:
|
||||
logger.warning("⚠️ [SemanticAnalyzer] Prompt 'edge_allocation_template' fehlt. Nutze Fallback.")
|
||||
prompt_template = (
|
||||
"TASK: Wähle aus den Kandidaten die relevanten Kanten für den Text.\n"
|
||||
"TEXT: {chunk_text}\n"
|
||||
"KANDIDATEN: {edge_list}\n"
|
||||
"OUTPUT: JSON Liste von Strings [\"kind:target\"]."
|
||||
)
|
||||
|
||||
# 2. Kandidaten-Liste formatieren
|
||||
edges_str = "\n".join([f"- {e}" for e in all_edges])
|
||||
|
||||
# LOG: Request Info
|
||||
logger.debug(f"🔍 [SemanticAnalyzer] Request: {len(chunk_text)} chars Text, {len(all_edges)} Candidates.")
|
||||
|
||||
# 3. Prompt füllen
|
||||
final_prompt = prompt_template.format(
|
||||
chunk_text=chunk_text[:3500],
|
||||
edge_list=edges_str
|
||||
)
|
||||
|
||||
try:
|
||||
# 4. LLM Call mit Traffic Control (NEU: priority="background")
|
||||
# Wir nutzen die "Slow Lane", damit der User im Chat nicht warten muss.
|
||||
response_json = await self.llm.generate_raw_response(
|
||||
prompt=final_prompt,
|
||||
force_json=True,
|
||||
max_retries=5,
|
||||
base_delay=5.0,
|
||||
priority="background" # <--- WICHTIG: Drosselung aktivieren
|
||||
)
|
||||
|
||||
# LOG: Raw Response Preview
|
||||
logger.debug(f"📥 [SemanticAnalyzer] Raw Response (Preview): {response_json[:200]}...")
|
||||
|
||||
# 5. Parsing & Cleaning
|
||||
clean_json = response_json.replace("```json", "").replace("```", "").strip()
|
||||
|
||||
if not clean_json:
|
||||
logger.warning("⚠️ [SemanticAnalyzer] Leere Antwort vom LLM erhalten. Trigger Fallback.")
|
||||
return []
|
||||
|
||||
try:
|
||||
data = json.loads(clean_json)
|
||||
except json.JSONDecodeError as json_err:
|
||||
logger.error(f"❌ [SemanticAnalyzer] JSON Decode Error.")
|
||||
logger.error(f" Grund: {json_err}")
|
||||
logger.error(f" Empfangener String: {clean_json[:500]}")
|
||||
logger.info(" -> Workaround: Fallback auf 'Alle Kanten' (durch Chunker).")
|
||||
return []
|
||||
|
||||
valid_edges = []
|
||||
|
||||
# 6. Robuste Validierung (List vs Dict)
|
||||
if isinstance(data, list):
|
||||
# Standardfall: ["kind:target", ...]
|
||||
valid_edges = [str(e) for e in data if isinstance(e, str) and ":" in e]
|
||||
|
||||
elif isinstance(data, dict):
|
||||
# Abweichende Formate behandeln
|
||||
logger.info(f"ℹ️ [SemanticAnalyzer] LLM lieferte Dict statt Liste. Versuche Reparatur. Keys: {list(data.keys())}")
|
||||
|
||||
for key, val in data.items():
|
||||
# Fall A: {"edges": ["kind:target"]}
|
||||
if key.lower() in ["edges", "results", "kanten", "matches"] and isinstance(val, list):
|
||||
valid_edges.extend([str(e) for e in val if isinstance(e, str) and ":" in e])
|
||||
|
||||
# Fall B: {"kind": "target"}
|
||||
elif isinstance(val, str):
|
||||
valid_edges.append(f"{key}:{val}")
|
||||
|
||||
# Fall C: {"kind": ["target1", "target2"]}
|
||||
elif isinstance(val, list):
|
||||
for target in val:
|
||||
if isinstance(target, str):
|
||||
valid_edges.append(f"{key}:{target}")
|
||||
|
||||
# Safety: Filtere nur Kanten, die halbwegs valide aussehen
|
||||
final_result = [e for e in valid_edges if ":" in e]
|
||||
|
||||
# LOG: Ergebnis
|
||||
if final_result:
|
||||
logger.info(f"✅ [SemanticAnalyzer] Success. {len(final_result)} Kanten zugewiesen.")
|
||||
else:
|
||||
logger.debug(" [SemanticAnalyzer] Keine spezifischen Kanten erkannt (Empty Result).")
|
||||
|
||||
return final_result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"💥 [SemanticAnalyzer] Kritischer Fehler: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
async def close(self):
|
||||
if self.llm:
|
||||
await self.llm.close()
|
||||
|
||||
# Singleton Helper
|
||||
_analyzer_instance = None
|
||||
def get_semantic_analyzer():
|
||||
global _analyzer_instance
|
||||
if _analyzer_instance is None:
|
||||
_analyzer_instance = SemanticAnalyzer()
|
||||
return _analyzer_instance
|
||||
|
|
@ -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"]
|
||||
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."
|
||||
|
|
@ -97,44 +97,66 @@ technical_template: |
|
|||
# ---------------------------------------------------------
|
||||
# 5. INTERVIEW: Der "One-Shot Extractor" (Performance Mode)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
interview_template: |
|
||||
TASK:
|
||||
Erstelle einen Markdown-Entwurf für eine Notiz vom Typ '{target_type}'.
|
||||
Du bist ein professioneller Ghostwriter. Verwandle den "USER INPUT" in eine strukturierte Notiz vom Typ '{target_type}'.
|
||||
|
||||
SCHEMA (Inhaltliche Pflichtfelder für den Body):
|
||||
STRUKTUR (Nutze EXAKT diese Überschriften):
|
||||
{schema_fields}
|
||||
|
||||
USER INPUT:
|
||||
"{query}"
|
||||
|
||||
ANWEISUNG:
|
||||
1. Extrahiere Informationen aus dem Input.
|
||||
2. Generiere validen Markdown.
|
||||
ANWEISUNG ZUM INHALT:
|
||||
1. Analysiere den Input genau.
|
||||
2. Schreibe die Inhalte unter die passenden Überschriften aus der STRUKTUR-Liste oben.
|
||||
3. STIL: Schreibe flüssig, professionell und in der Ich-Perspektive. Korrigiere Grammatikfehler, aber behalte den persönlichen Ton bei.
|
||||
4. Wenn Informationen für einen Abschnitt fehlen, schreibe nur: "[TODO: Ergänzen]". Erfinde nichts dazu.
|
||||
|
||||
OUTPUT REGELN (STRIKT BEACHTEN):
|
||||
A. FRONTMATTER (YAML):
|
||||
- Darf NUR folgende Felder enthalten: [type, status, title, tags].
|
||||
- Schreibe KEINE inhaltlichen Sätze (wie 'Situation', 'Ziel') in das YAML!
|
||||
- Setze 'status: draft'.
|
||||
|
||||
B. BODY (Markdown):
|
||||
- Nutze für jedes Schema-Feld eine Markdown-Überschrift (## Feldname).
|
||||
- Schreibe den Inhalt DARUNTER.
|
||||
- Nutze "[TODO: Ergänzen]", wenn Infos fehlen.
|
||||
|
||||
HINWEIS ZUM TYP:
|
||||
{schema_hint}
|
||||
|
||||
OUTPUT FORMAT BEISPIEL:
|
||||
```markdown
|
||||
OUTPUT FORMAT (YAML + MARKDOWN):
|
||||
---
|
||||
type: {target_type}
|
||||
status: draft
|
||||
title: ...
|
||||
tags: [...]
|
||||
title: (Erstelle einen treffenden, kurzen Titel für den Inhalt)
|
||||
tags: [Tag1, Tag2]
|
||||
---
|
||||
# Titel der Notiz
|
||||
|
||||
## Erstes Schema Feld
|
||||
Der Inhalt hier...
|
||||
# (Wiederhole den Titel hier)
|
||||
|
||||
## (Erster Begriff aus STRUKTUR)
|
||||
(Text...)
|
||||
|
||||
## (Zweiter Begriff aus STRUKTUR)
|
||||
(Text...)
|
||||
|
||||
(usw.)
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 6. EDGE_ALLOCATION: Kantenfilter (Intent: OFFLINE_FILTER)
|
||||
# ---------------------------------------------------------
|
||||
edge_allocation_template: |
|
||||
TASK:
|
||||
Du bist ein strikter Selektor. Du erhältst eine Liste von "Kandidaten-Kanten" (Strings).
|
||||
Wähle jene aus, die inhaltlich im "Textabschnitt" vorkommen oder relevant sind.
|
||||
|
||||
TEXTABSCHNITT:
|
||||
"""
|
||||
{chunk_text}
|
||||
"""
|
||||
|
||||
KANDIDATEN (Auswahl-Pool):
|
||||
{edge_list}
|
||||
|
||||
REGELN:
|
||||
1. Die Kanten haben das Format "typ:ziel". Der "typ" ist variabel und kann ALLES sein (z.B. uses, blocks, inspired_by, loves, etc.).
|
||||
2. Gib NUR die Strings aus der Kandidaten-Liste zurück, die zum Text passen.
|
||||
3. Erfinde KEINE neuen Kanten. Nutze exakt die Schreibweise aus der Liste.
|
||||
4. Antworte als flache JSON-Liste.
|
||||
|
||||
BEISPIEL (Zur Demonstration der Logik):
|
||||
Input Text: "Das Projekt Alpha scheitert, weil Budget fehlt."
|
||||
Input Kandidaten: ["blocks:Projekt Alpha", "inspired_by:Buch der Weisen", "needs:Budget"]
|
||||
Output: ["blocks:Projekt Alpha", "needs:Budget"]
|
||||
|
||||
DEIN OUTPUT (JSON):
|
||||
|
|
@ -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"]
|
||||
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"]
|
||||
|
|
@ -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.
|
||||
* 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.
|
||||
|
|
@ -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.
|
||||
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)**.
|
||||
|
|
@ -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
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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.** |
|
||||
| **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. |
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Mindnet v2.4 – Entwickler-Workflow
|
||||
**Datei:** `docs/DEV_WORKFLOW.md`
|
||||
**Stand:** 2025-12-11 (Aktualisiert: Inkl. Async Intelligence & Nomic)
|
||||
**Stand:** 2025-12-12 (Aktualisiert: v2.6 mit Traffic Control)
|
||||
|
||||
Dieses Handbuch beschreibt den Entwicklungszyklus zwischen **Windows PC** (IDE), **Raspberry Pi** (Gitea) und **Beelink** (Runtime/Server).
|
||||
|
||||
|
|
@ -35,14 +35,14 @@ Hier erstellst du die neue Funktion in einer sicheren Umgebung.
|
|||
2. **Branch erstellen:**
|
||||
* Klicke wieder unten links auf `main`.
|
||||
* Wähle `+ Create new branch...`.
|
||||
* Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp11-async-fix`).
|
||||
* Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp15-traffic-control`).
|
||||
* Drücke **Enter**.
|
||||
|
||||
3. **Sicherheits-Check:**
|
||||
* Steht unten links jetzt dein Feature-Branch? **Nur dann darfst du Code ändern!**
|
||||
|
||||
4. **Coden:**
|
||||
* Nimm deine Änderungen vor (z.B. neue Schemas in `decision_engine.yaml` oder Async-Logik in `ingestion.py`).
|
||||
* Nimm deine Änderungen vor (z.B. neue Schemas in `types.yaml` oder Async-Logik in `ingestion.py`).
|
||||
|
||||
5. **Sichern & Hochladen:**
|
||||
* **Source Control** Icon (Gabel-Symbol) -> Nachricht eingeben -> **Commit**.
|
||||
|
|
@ -64,15 +64,27 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft.
|
|||
```bash
|
||||
git fetch
|
||||
# Tipp: 'git branch -r' zeigt alle verfügbaren Branches an
|
||||
git checkout feature/wp11-async-fix
|
||||
git checkout feature/wp15-traffic-control
|
||||
git pull
|
||||
```
|
||||
|
||||
4. **Umgebung vorbereiten (WICHTIG für v2.4):**
|
||||
4. **Umgebung vorbereiten (WICHTIG für v2.6):**
|
||||
Prüfe deine `.env` Datei. Sie benötigt jetzt Einträge für die Traffic Control.
|
||||
```bash
|
||||
nano .env
|
||||
```
|
||||
**Ergänze/Prüfe:**
|
||||
```ini
|
||||
# Traffic Control (Neu in v2.6)
|
||||
MINDNET_LLM_BACKGROUND_LIMIT=2 # Max. parallele Import-Tasks
|
||||
MINDNET_API_TIMEOUT=300.0 # Frontend wartet länger
|
||||
MINDNET_EMBEDDING_MODEL="nomic-embed-text"
|
||||
```
|
||||
|
||||
**Dependencies aktualisieren:**
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt # HTTPX usw.
|
||||
# Sicherstellen, dass das neue Embedding-Modell da ist:
|
||||
pip install -r requirements.txt
|
||||
ollama pull nomic-embed-text
|
||||
```
|
||||
|
||||
|
|
@ -102,18 +114,15 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft.
|
|||
uvicorn app.main:app --host 0.0.0.0 --port 8002 --env-file .env
|
||||
```
|
||||
|
||||
6. **Validieren (Smoke Tests):**
|
||||
6. **Validieren (Smoke Tests v2.6):**
|
||||
|
||||
* **Browser:** Öffne `http://<IP>:8502` um die UI zu testen (Intent Badge prüfen!).
|
||||
* **CLI:** Führe Testskripte in einem **zweiten Terminal** aus:
|
||||
* **Test A: Last-Test (Traffic Control):**
|
||||
1. Starte einen Import im Terminal: `python3 -m scripts.import_markdown ...`
|
||||
2. Öffne **gleichzeitig** `http://<IP>:8502` im Browser.
|
||||
3. Stelle eine Chat-Frage ("Was ist Mindnet?").
|
||||
4. **Erwartung:** Der Chat antwortet sofort (Realtime Lane), während der Import im Hintergrund weiterläuft (Background Lane).
|
||||
|
||||
**Test A: Intelligence / Aliases (Neu in WP11)**
|
||||
```bash
|
||||
python debug_analysis.py
|
||||
# Erwartung: "✅ ALIAS GEFUNDEN"
|
||||
```
|
||||
|
||||
**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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
---
|
||||
<details>
|
||||
|
|
@ -19,7 +19,8 @@
|
|||
- [2.1 Struktur-Kanten (Das Skelett)](#21-struktur-kanten-das-skelett)
|
||||
- [2.2 Inhalts-Kanten (explizit)](#22-inhalts-kanten-explizit)
|
||||
- [2.3 Typ-basierte Default-Kanten (Regelbasiert)](#23-typ-basierte-default-kanten-regelbasiert)
|
||||
- [2.4 Matrix-Logik (Kontextsensitive Kanten) – Neu in v2.4](#24-matrix-logik-kontextsensitive-kanten--neu-in-v24)
|
||||
- [2.4 Smart Edge Allocation (LLM-gefiltert) – Neu in v2.6](#24-smart-edge-allocation-llm-gefiltert--neu-in-v26)
|
||||
- [2.5 Matrix-Logik (Kontextsensitive Kanten)](#25-matrix-logik-kontextsensitive-kanten)
|
||||
- [3) Edge-Payload – Felder \& Semantik](#3-edge-payload--felder--semantik)
|
||||
- [4) Typ-Registry (`config/types.yaml`)](#4-typ-registry-configtypesyaml)
|
||||
- [4.1 Zweck](#41-zweck)
|
||||
|
|
@ -27,13 +28,15 @@
|
|||
- [5) Der Retriever (Funktionaler Layer)](#5-der-retriever-funktionaler-layer)
|
||||
- [5.1 Scoring-Modell](#51-scoring-modell)
|
||||
- [5.2 Erklärbarkeit (Explainability) – WP04b](#52-erklärbarkeit-explainability--wp04b)
|
||||
- [6) Context Intelligence \& Intent Router (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)
|
||||
|
||||
</details>
|
||||
---
|
||||
|
|
@ -63,7 +66,7 @@ Die drei zentralen Artefakt-Sammlungen lauten:
|
|||
- `mindnet_chunks` – semantische Teilstücke einer Note (Fenster/„Chunks“)
|
||||
- `mindnet_edges` – gerichtete Beziehungen zwischen Knoten (Chunks/Notes)
|
||||
|
||||
Die Import-Pipeline (seit v2.3.10 asynchron) erzeugt diese Artefakte **deterministisch** und **idempotent** (erneute Läufe überschreiben konsistent statt zu duplizieren). Die Import-Schritte sind: *parse → chunk → embed → edge → upsert*.
|
||||
Die Import-Pipeline (seit v2.6 mit **Traffic Control** und **Smart Edges**) erzeugt diese Artefakte **deterministisch** und **idempotent** (erneute Läufe überschreiben konsistent statt zu duplizieren). Die Import-Schritte sind: *parse → chunk → embed → smart-edge-allocation → upsert*.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -80,9 +83,9 @@ Die Import-Pipeline (seit v2.3.10 asynchron) erzeugt diese Artefakte **determini
|
|||
- Jeder Chunk gehört **genau einer** Note.
|
||||
- Chunks bilden eine Sequenz (1…N) – das ermöglicht *next/prev*.
|
||||
- **Update v2.4:** Chunks werden jetzt durch das Modell `nomic-embed-text` in **768-dimensionale Vektoren** umgewandelt. Dies erlaubt eine deutlich höhere semantische Auflösung als frühere Modelle (384 Dim).
|
||||
- **Neu in v2.2:** Alle Kanten entstehen ausschließlich zwischen Chunks (Scope="chunk"), nie zwischen Notes direkt. Notes dienen nur noch als Metadatencontainer.
|
||||
- **Update v2.6 (Smart Chunking):** Ein Chunk ist nicht mehr nur ein dummer Textblock, der alle Links der Mutter-Notiz erbt. Durch die **Smart Edge Allocation (WP15)** "weiß" jeder Chunk genau, welche Kanten inhaltlich zu ihm gehören (siehe 2.4).
|
||||
|
||||
> **Wichtig:** Chunking-Profile (short/medium/long) kommen aus `types.yaml` (per Note-Typ), können aber lokal überschrieben werden. Die effektiven Werte werden bei der Payload-Erzeugung bestimmt.
|
||||
> **Wichtig:** Chunking-Profile (short/medium/long/sliding_smart_edges) kommen aus `types.yaml` (per Note-Typ), können aber lokal überschrieben werden. Die effektiven Werte werden bei der Payload-Erzeugung bestimmt.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -97,7 +100,7 @@ Edges kodieren Beziehungen. Sie sind **gerichtet** und werden in `mindnet_edges`
|
|||
Diese Kanten entstehen immer, unabhängig von Inhalten.
|
||||
|
||||
### 2.2 Inhalts-Kanten (explizit)
|
||||
Hier unterscheidet v2.2 präzise zwischen verschiedenen Quellen der Evidenz:
|
||||
Hier unterscheidet v2.6 präzise zwischen verschiedenen Quellen der Evidenz:
|
||||
|
||||
1. **Explizite Inline-Relationen (Höchste Priorität):**
|
||||
Im Fließtext notierte, semantisch qualifizierte Relationen.
|
||||
|
|
@ -131,7 +134,19 @@ Regel: **Für jede gefundene explizite Referenz** (s. o.) werden **zusätzliche*
|
|||
Beispiel: Ein *project* mit `edge_defaults=["depends_on"]` erzeugt zu *jedem* explizit referenzierten Ziel **zusätzlich** eine `depends_on`-Kante.
|
||||
Diese Kanten tragen *provenance=rule* und eine **rule_id** der Form `edge_defaults:{note_type}:{relation}` sowie eine geringere Confidence (~0.7).
|
||||
|
||||
### 2.4 Matrix-Logik (Kontextsensitive Kanten) – Neu in v2.4
|
||||
### 2.4 Smart Edge Allocation (LLM-gefiltert) – Neu in v2.6
|
||||
Mit **WP15** wurde ein intelligenter Filter eingeführt, um das Problem des "Broadcasting" zu lösen. Früher erbte jeder Chunk einer Notiz *alle* Links der Notiz, was zu unpräzisem Retrieval führte.
|
||||
|
||||
**Der neue Prozess:**
|
||||
1. **Scan:** Das System sammelt alle expliziten Links der gesamten Notiz.
|
||||
2. **Chunking:** Der Text wird in Abschnitte zerlegt.
|
||||
3. **Analyse (LLM):** Ein lokales LLM (Semantic Analyzer) liest jeden Chunk einzeln.
|
||||
4. **Entscheidung:** Das LLM entscheidet für jeden Link aus Schritt 1: *"Ist dieser Link im Kontext dieses spezifischen Textabschnitts relevant?"*
|
||||
5. **Zuweisung:** Nur wenn das LLM zustimmt, wird die Kante am Chunk erstellt.
|
||||
|
||||
*Steuerung:* Dies ist pro Typ in der `types.yaml` konfigurierbar (`enable_smart_edge_allocation: true`).
|
||||
|
||||
### 2.5 Matrix-Logik (Kontextsensitive Kanten)
|
||||
Mit WP-11 wurde eine Intelligenz eingeführt, die Kanten-Typen nicht nur anhand des Quell-Typs, sondern auch anhand des Ziel-Typs bestimmt ("Matrix").
|
||||
|
||||
**Beispiel für `Source Type: experience`:**
|
||||
|
|
@ -154,7 +169,7 @@ Jede Kante hat mindestens:
|
|||
|
||||
Erweiterte/abgeleitete Felder (WP03 Superset):
|
||||
|
||||
- `provenance` – `"explicit"` (Wikilink/Inline/Callout) oder `"rule"` (Typ-Defaults)
|
||||
- `provenance` – `"explicit"` (Wikilink/Inline/Callout), `"rule"` (Typ-Defaults) oder neu `"smart"` (vom LLM validiert).
|
||||
- `rule_id` – maschinenlesbare Regelquelle (z.B. `inline:rel`, `edge_defaults:project:depends_on`)
|
||||
- `confidence` – 0.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.
|
||||
|
||||
### 6.3 Strategic Retrieval (Injektion von Werten)
|
||||
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.
|
||||
|
||||
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.** |
|
||||
| **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. |
|
||||
|
|
@ -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)**.
|
||||
|
||||
---
|
||||
<details>
|
||||
<summary>📖 <b>Inhaltsverzeichnis (Klicken zum Öffnen)</b></summary>
|
||||
|
||||
- [Mindnet v2.4 – Technische Architektur](#mindnet-v24--technische-architektur)
|
||||
- [Mindnet v2.6 – Technische Architektur](#mindnet-v26--technische-architektur)
|
||||
- [](#)
|
||||
- [1. Systemüberblick](#1-systemüberblick)
|
||||
- [1.1 Architektur-Zielbild](#11-architektur-zielbild)
|
||||
- [1.2 Verzeichnisstruktur \& Komponenten (Post-WP10)](#12-verzeichnisstruktur--komponenten-post-wp10)
|
||||
- [1.2 Verzeichnisstruktur \& Komponenten (Post-WP15)](#12-verzeichnisstruktur--komponenten-post-wp15)
|
||||
- [2. Datenmodell \& Collections (Qdrant)](#2-datenmodell--collections-qdrant)
|
||||
- [2.1 Notes Collection (`<prefix>_notes`)](#21-notes-collection-prefix_notes)
|
||||
- [2.2 Chunks Collection (`<prefix>_chunks`)](#22-chunks-collection-prefix_chunks)
|
||||
|
|
@ -27,22 +27,23 @@
|
|||
- [3.4 Prompts (`config/prompts.yaml`)](#34-prompts-configpromptsyaml)
|
||||
- [3.5 Environment (`.env`)](#35-environment-env)
|
||||
- [4. Import-Pipeline (Markdown → Qdrant)](#4-import-pipeline-markdown--qdrant)
|
||||
- [4.1 Verarbeitungsschritte (Async)](#41-verarbeitungsschritte-async)
|
||||
- [4.1 Verarbeitungsschritte (Async + Smart)](#41-verarbeitungsschritte-async--smart)
|
||||
- [5. Retriever-Architektur \& Scoring](#5-retriever-architektur--scoring)
|
||||
- [5.1 Betriebsmodi](#51-betriebsmodi)
|
||||
- [5.2 Scoring-Formel (WP04a)](#52-scoring-formel-wp04a)
|
||||
- [5.3 Explanation Layer (WP04b)](#53-explanation-layer-wp04b)
|
||||
- [5.4 Graph-Expansion](#54-graph-expansion)
|
||||
- [6. RAG \& Chat Architektur (WP06 Hybrid Router + WP07 Interview)](#6-rag--chat-architektur-wp06-hybrid-router--wp07-interview)
|
||||
- [6.1 Architektur-Pattern: Intent Router](#61-architektur-pattern-intent-router)
|
||||
- [6.2 Schritt 1: Intent Detection (Hybrid)](#62-schritt-1-intent-detection-hybrid)
|
||||
- [6.3 Schritt 2: Strategy Resolution (Late Binding)](#63-schritt-2-strategy-resolution-late-binding)
|
||||
- [6.4 Schritt 3: Retrieval vs. Extraction](#64-schritt-3-retrieval-vs-extraction)
|
||||
- [6.5 Schritt 4: Generation \& Response](#65-schritt-4-generation--response)
|
||||
- [6.1 Architektur-Pattern: Intent Router v5](#61-architektur-pattern-intent-router-v5)
|
||||
- [6.2 Traffic Control (Priorisierung)](#62-traffic-control-priorisierung)
|
||||
- [6.3 Schritt 1: Intent Detection (Question vs. Action)](#63-schritt-1-intent-detection-question-vs-action)
|
||||
- [6.4 Schritt 2: Strategy Resolution](#64-schritt-2-strategy-resolution)
|
||||
- [6.5 Schritt 3: Retrieval vs. Extraction](#65-schritt-3-retrieval-vs-extraction)
|
||||
- [6.6 Schritt 4: Generation \& Response](#66-schritt-4-generation--response)
|
||||
- [7. Frontend Architektur (WP10)](#7-frontend-architektur-wp10)
|
||||
- [7.1 Kommunikation](#71-kommunikation)
|
||||
- [7.2 Features \& UI-Logik](#72-features--ui-logik)
|
||||
- [7.3 Draft-Editor \& Sanitizer (Neu in WP10a)](#73-draft-editor--sanitizer-neu-in-wp10a)
|
||||
- [7.3 Draft-Editor \& Healing Parser (Neu in WP10a/v2.5)](#73-draft-editor--healing-parser-neu-in-wp10av25)
|
||||
- [7.4 State Management (Resurrection Pattern)](#74-state-management-resurrection-pattern)
|
||||
- [7.5 Deployment Ports](#75-deployment-ports)
|
||||
- [8. Feedback \& Logging Architektur (WP04c)](#8-feedback--logging-architektur-wp04c)
|
||||
|
|
@ -62,57 +63,63 @@
|
|||
Mindnet ist ein **lokales RAG-System (Retrieval Augmented Generation)** mit Web-Interface.
|
||||
1. **Source:** Markdown-Notizen in einem Vault (Obsidian-kompatibel).
|
||||
2. **Pipeline:** Ein Python-Importer transformiert diese in **Notes**, **Chunks** und **Edges**.
|
||||
* **Neu in v2.6:** Intelligente Kanten-Zuweisung durch lokale LLMs (**Smart Edge Allocation**).
|
||||
3. **Storage:**
|
||||
* **Qdrant:** Vektor-Datenbank für Graph und Semantik (Collections: notes, chunks, edges).
|
||||
* **Local Files (JSONL):** Append-Only Logs für Feedback und Search-History (Data Flywheel).
|
||||
4. **Backend:** Eine FastAPI-Anwendung stellt Endpunkte für **Semantische** und **Hybride Suche** sowie **Feedback** bereit.
|
||||
* **Update v2.3.10:** Der Core arbeitet nun vollständig **asynchron (AsyncIO)**, um Blockaden bei Embedding-Requests zu vermeiden.
|
||||
5. **Frontend:** Streamlit-App (`ui.py`) für Interaktion und Visualisierung inkl. **Draft Editor** und **Intelligence-Features**.
|
||||
* **Update v2.6:** Der Core arbeitet nun vollständig **asynchron (AsyncIO)** mit **Traffic Control** (Semaphore zur Lastverteilung).
|
||||
5. **Frontend:** Streamlit-App (`ui.py`) für Interaktion und Visualisierung inkl. **Draft Editor**, **Active Intelligence** und **Healing Parser**.
|
||||
6. **Inference:** Lokales LLM (Ollama: Phi-3 Mini) für RAG-Chat und Antwortgenerierung. Embedding via `nomic-embed-text`.
|
||||
|
||||
Das System arbeitet **deterministisch** (stabile IDs) und ist **konfigurationsgetrieben** (`types.yaml`, `retriever.yaml`, `decision_engine.yaml`, `prompts.yaml`).
|
||||
|
||||
### 1.2 Verzeichnisstruktur & Komponenten (Post-WP10)
|
||||
### 1.2 Verzeichnisstruktur & Komponenten (Post-WP15)
|
||||
|
||||
/mindnet/
|
||||
├── app/
|
||||
│ ├── main.py # FastAPI Einstiegspunkt
|
||||
│ ├── core/
|
||||
│ │ ├── ingestion.py # NEU: Async Ingestion Service (WP11)
|
||||
│ │ ├── ingestion.py # NEU: Async Ingestion mit Change Detection
|
||||
│ │ ├── qdrant.py # Client-Factory & Connection
|
||||
│ │ ├── qdrant_points.py # Low-Level Point Operations (Upsert/Delete)
|
||||
│ │ ├── note_payload.py # Bau der Note-Objekte
|
||||
│ │ ├── chunk_payload.py # Bau der Chunk-Objekte
|
||||
│ │ ├── chunker.py # Text-Zerlegung (Profiling)
|
||||
│ │ ├── chunker.py # Smart Chunker Orchestrator (WP15)
|
||||
│ │ ├── edges.py # Edge-Datenstrukturen
|
||||
│ │ ├── derive_edges.py # Logik der Kantenableitung (WP03)
|
||||
│ │ ├── graph_adapter.py # Subgraph & Reverse-Lookup (WP04b)
|
||||
│ │ └── retriever.py # Scoring, Expansion & Explanation (WP04a/b)
|
||||
│ ├── models/ # Pydantic DTOs
|
||||
│ │ └── dto.py # Zentrale DTO-Definition
|
||||
│ ├── routers/
|
||||
│ │ ├── query.py # Such-Endpunkt
|
||||
│ │ ├── ingest.py # NEU: API für Save & Analyze (WP11)
|
||||
│ │ ├── chat.py # Hybrid Router & Interview Logic (WP06/07)
|
||||
│ │ ├── chat.py # Hybrid Router v5 & Interview Logic (WP06/07)
|
||||
│ │ ├── feedback.py # Feedback-Endpunkt (WP04c)
|
||||
│ │ └── ...
|
||||
│ ├── services/
|
||||
│ │ ├── llm_service.py # Ollama Chat Client
|
||||
│ │ ├── embeddings_client.py# NEU: Async Embedding Client (HTTPX)
|
||||
│ │ └── feedback_service.py # JSONL Logging (WP04c)
|
||||
│ │ ├── llm_service.py # Ollama Chat Client mit Traffic Control (v2.8.0)
|
||||
│ │ ├── semantic_analyzer.py# NEU: LLM-Filter für Edges (WP15)
|
||||
│ │ ├── embeddings_client.py# NEU: Async Embeddings (HTTPX)
|
||||
│ │ ├── feedback_service.py # Logging (JSONL Writer)
|
||||
│ │ └── discovery.py # NEU: Intelligence Logic (WP11)
|
||||
│ ├── frontend/ # NEU (WP10)
|
||||
│ └── ui.py # Streamlit Application inkl. Sanitizer
|
||||
├── config/
|
||||
│ ├── types.yaml # Typ-Definitionen (Import-Zeit)
|
||||
│ ├── retriever.yaml # Scoring-Gewichte (Laufzeit)
|
||||
│ ├── decision_engine.yaml # Strategien & Schemas (WP06/WP07)
|
||||
│ └── prompts.yaml # LLM System-Prompts & Templates (WP06)
|
||||
│ │ └── ui.py # Streamlit Application inkl. Healing Parser
|
||||
│ └── main.py # Entrypoint der API
|
||||
├── config/ # YAML-Konfigurationen (Single Source of Truth)
|
||||
│ ├── types.yaml # Import-Regeln & Smart-Edge Config
|
||||
│ ├── prompts.yaml # LLM Prompts & Interview Templates (WP06/07)
|
||||
│ ├── decision_engine.yaml # Router-Strategien (Actions only)
|
||||
│ └── retriever.yaml # Scoring-Regeln & Kantengewichte
|
||||
├── data/
|
||||
│ └── logs/ # Lokale JSONL-Logs (WP04c)
|
||||
├── scripts/
|
||||
│ └── logs/ # Lokale Logs (search_history.jsonl, feedback.jsonl)
|
||||
├── scripts/ # CLI-Tools (Import, Diagnose, Reset)
|
||||
│ ├── import_markdown.py # Haupt-Importer CLI (Async)
|
||||
│ ├── payload_dryrun.py # Diagnose: JSON-Generierung ohne DB
|
||||
│ └── edges_full_check.py # Diagnose: Graph-Integrität
|
||||
└── tests/ # Pytest Suite
|
||||
├── tests/ # Pytest Suite & Smoke Scripts
|
||||
└── vault/ # Dein lokaler Markdown-Content (Git-ignored)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -140,7 +147,7 @@ Repräsentiert die Metadaten einer Datei.
|
|||
### 2.2 Chunks Collection (`<prefix>_chunks`)
|
||||
Die atomaren Sucheinheiten.
|
||||
* **Zweck:** Vektorsuche (Embeddings), Granulares Ergebnis.
|
||||
* **Update v2.3.10:** Vektor-Dimension ist jetzt **768** (für `nomic-embed-text`).
|
||||
* **Update v2.4:** Vektor-Dimension ist jetzt **768** (für `nomic-embed-text`).
|
||||
* **Schema (Payload):**
|
||||
|
||||
| Feld | Datentyp | Beschreibung |
|
||||
|
|
@ -171,6 +178,7 @@ Gerichtete Kanten. Massiv erweitert in WP03 für Provenienz-Tracking.
|
|||
| `scope` | Keyword | Geltungsbereich. | Immer `"chunk"` (v2.2). |
|
||||
| `rule_id` | Keyword | Herkunftsregel. | `explicit:wikilink`, `inline:rel` |
|
||||
| `confidence` | Float | Vertrauenswürdigkeit (0.0-1.0). | 1.0, 0.95, 0.7 |
|
||||
| `provenance` | Keyword | Quelle der Kante (Neu in v2.6). | `explicit`, `rule`, `smart` |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -181,13 +189,20 @@ Die Logik ist ausgelagert in YAML-Dateien.
|
|||
### 3.1 Typ-Registry (`config/types.yaml`)
|
||||
Steuert den Import-Prozess.
|
||||
* **Priorität:** Frontmatter > Pfad > Default.
|
||||
* **Neu in v2.6:**
|
||||
* `enable_smart_edge_allocation`: (Bool) Aktiviert den LLM-Filter für diesen Typ.
|
||||
* `detection_keywords`: (List) Keywords für den Hybrid Router ("Objekterkennung").
|
||||
* **Inhalt (Beispiel):**
|
||||
|
||||
types:
|
||||
concept:
|
||||
chunk_profile: medium
|
||||
chunk_profile: sliding_standard
|
||||
edge_defaults: ["references", "related_to"]
|
||||
retriever_weight: 0.60
|
||||
experience:
|
||||
chunk_profile: sliding_smart_edges
|
||||
enable_smart_edge_allocation: true
|
||||
detection_keywords: ["erfahrung", "erlebt"]
|
||||
|
||||
### 3.2 Retriever-Config (`config/retriever.yaml`)
|
||||
Steuert das Scoring zur Laufzeit (WP04a).
|
||||
|
|
@ -199,14 +214,14 @@ Steuert das Scoring zur Laufzeit (WP04a).
|
|||
centrality_weight: 0.5
|
||||
|
||||
### 3.3 Decision Engine (`config/decision_engine.yaml`)
|
||||
**Neu in WP06/07:** Steuert den Intent-Router und die Interview-Schemas.
|
||||
**Neu in WP06/07:** Steuert den Intent-Router.
|
||||
* Definiert Strategien (`DECISION`, `INTERVIEW`, etc.).
|
||||
* Definiert `schemas` für den Interview-Modus (Pflichtfelder pro Typ).
|
||||
* **Update v2.6:** Enthält nur noch **Handlungs-Keywords** (Verben wie "neu", "erstellen"). Objektnamen ("Projekt") werden nun über `types.yaml` aufgelöst.
|
||||
* Definiert LLM-Router-Settings (`llm_fallback_enabled`).
|
||||
|
||||
### 3.4 Prompts (`config/prompts.yaml`)
|
||||
Steuert die LLM-Persönlichkeit und Templates.
|
||||
* Enthält Templates für alle Strategien inkl. `interview_template` mit One-Shot Logik.
|
||||
* Enthält Templates für alle Strategien inkl. `interview_template` und neu `router_prompt`.
|
||||
|
||||
### 3.5 Environment (`.env`)
|
||||
Erweiterung für LLM-Steuerung und Embedding-Modell:
|
||||
|
|
@ -215,32 +230,33 @@ Erweiterung für LLM-Steuerung und Embedding-Modell:
|
|||
MINDNET_EMBEDDING_MODEL=nomic-embed-text # NEU in v2.3.10
|
||||
MINDNET_OLLAMA_URL=http://127.0.0.1:11434
|
||||
MINDNET_LLM_TIMEOUT=300.0 # Neu: Erhöht für CPU-Inference Cold-Starts
|
||||
MINDNET_API_TIMEOUT=60.0 # Neu: Timeout für Frontend-API Calls
|
||||
MINDNET_API_TIMEOUT=300.0 # Neu: Timeout für Frontend-API (Smart Edges brauchen Zeit)
|
||||
MINDNET_DECISION_CONFIG="config/decision_engine.yaml"
|
||||
MINDNET_VAULT_ROOT="./vault" # Neu: Pfad für Write-Back
|
||||
MINDNET_LLM_BACKGROUND_LIMIT=2 # Neu: Limit für parallele Hintergrund-Jobs (Traffic Control)
|
||||
|
||||
---
|
||||
|
||||
## 4. Import-Pipeline (Markdown → Qdrant)
|
||||
|
||||
Das Skript `scripts/import_markdown.py` orchestriert den Prozess.
|
||||
**Neu in v2.3.10:** Der Import nutzt `asyncio` und eine **Semaphore**, um Ollama nicht zu überlasten.
|
||||
**Neu in v2.6:** Der Import nutzt `asyncio` und **Traffic Control**, um Ollama nicht zu überlasten.
|
||||
|
||||
### 4.1 Verarbeitungsschritte (Async)
|
||||
### 4.1 Verarbeitungsschritte (Async + Smart)
|
||||
|
||||
1. **Discovery & Parsing:**
|
||||
* Einlesen der `.md` Dateien. Hash-Vergleich (Body/Frontmatter) zur Erkennung von Änderungen.
|
||||
2. **Typauflösung:**
|
||||
* Bestimmung des `type` via `types.yaml`.
|
||||
3. **Chunking:**
|
||||
* Zerlegung via `chunker.py` basierend auf `chunk_profile`.
|
||||
4. **Embedding (Async):**
|
||||
* Der `EmbeddingsClient` (`app/services/embeddings_client.py`) sendet Text-Chunks asynchron an Ollama.
|
||||
* Modell: `nomic-embed-text` (768d).
|
||||
* Semaphore: Max. 5 gleichzeitige Files, um OOM (Out-of-Memory) zu verhindern.
|
||||
1. **Discovery & Parsing:** Hash-Vergleich zur Erkennung von Änderungen.
|
||||
2. **Typauflösung:** Bestimmung des `type` via `types.yaml`.
|
||||
3. **Config Check:** Laden des `chunk_profile` und `enable_smart_edge_allocation`.
|
||||
4. **Chunking & Smart Edges (WP15):**
|
||||
* Zerlegung des Textes via `chunker.py`.
|
||||
* Wenn Smart Edges aktiv: Der `SemanticAnalyzer` sendet Chunks an das LLM.
|
||||
* **Traffic Control:** Der Request nutzt `priority="background"`. Die Semaphore (Limit: 2) drosselt die Parallelität.
|
||||
5. **Kantenableitung (Edge Derivation):**
|
||||
* `derive_edges.py` erzeugt Inline-, Callout- und Default-Edges.
|
||||
6. **Upsert:**
|
||||
6. **Embedding (Async):**
|
||||
* Der `EmbeddingsClient` (`app/services/embeddings_client.py`) sendet Text-Chunks asynchron an Ollama.
|
||||
* Modell: `nomic-embed-text` (768d).
|
||||
7. **Upsert:**
|
||||
* Schreiben in Qdrant. Nutzung von `--purge-before-upsert`.
|
||||
* **Strict Mode:** Der Prozess bricht ab, wenn Embeddings leer sind oder Dimension `0` haben.
|
||||
|
||||
|
|
@ -269,49 +285,55 @@ $$
|
|||
* **Gewichte ($W$):** Stammen aus `retriever.yaml`.
|
||||
|
||||
### 5.3 Explanation Layer (WP04b)
|
||||
Der Retriever kann Ergebnisse erklären (`explain=True`).
|
||||
* **Logik:**
|
||||
* Berechnung des `ScoreBreakdown` (Anteile von Semantik, Graph, Typ).
|
||||
* Analyse des lokalen Subgraphen mittels `graph_adapter.py`.
|
||||
* **Incoming Edges (Authority):** Wer zeigt auf diesen Treffer? (z.B. "Referenziert von...")
|
||||
* **Outgoing Edges (Hub):** Worauf zeigt dieser Treffer? (z.B. "Verweist auf...")
|
||||
* **Output:** `QueryHit` enthält ein `explanation` Objekt mit menschenlesbaren `reasons` und `related_edges`.
|
||||
Der Retriever ist keine Blackbox mehr. Er liefert auf Wunsch (`explain=True`) eine strukturierte Begründung (`Explanation`-Objekt).
|
||||
|
||||
**Die "Warum"-Logik:**
|
||||
1. **Semantik:** Prüfung der Cosine-Similarity ("Sehr hohe textuelle Übereinstimmung").
|
||||
2. **Typ:** Prüfung des `retriever_weight` ("Bevorzugt, da Entscheidung").
|
||||
3. **Graph (Kontext):**
|
||||
* **Hub (Outgoing):** Worauf verweist dieser Treffer? ("Verweist auf Qdrant").
|
||||
* **Authority (Incoming):** Wer verweist auf diesen Treffer? ("Wird referenziert von Projekt Alpha").
|
||||
|
||||
Die API gibt diese Analysen als menschenlesbare Sätze (`reasons`) und als Datenstruktur (`score_breakdown`) zurück.
|
||||
|
||||
### 5.4 Graph-Expansion
|
||||
Der Hybrid-Modus lädt dynamisch die Nachbarschaft der Top-K Vektor-Treffer ("Seeds") über `graph_adapter.expand`. Dies baut einen temporären `NetworkX`-artigen Graphen im Speicher (Klasse `Subgraph`), auf dem Boni berechnet werden.
|
||||
|
||||
---
|
||||
|
||||
## 6. RAG \& Chat Architektur (WP06 Hybrid Router + WP07 Interview)
|
||||
## 6. RAG & Chat Architektur (WP06 Hybrid Router + WP07 Interview)
|
||||
|
||||
Der Flow für eine Chat-Anfrage (`/chat`) wurde in WP06 auf eine **Configuration-Driven Architecture** umgestellt. Der `ChatRouter` (`app/routers/chat.py`) fungiert als zentraler Dispatcher.
|
||||
Der Flow für eine Chat-Anfrage (`/chat`) wurde in WP06 auf eine **Configuration-Driven Architecture** umgestellt und in WP15 (v2.6) verfeinert.
|
||||
|
||||
### 6.1 Architektur-Pattern: Intent Router
|
||||
### 6.1 Architektur-Pattern: Intent Router v5
|
||||
Die Behandlung einer Anfrage ist nicht mehr hartkodiert, sondern wird dynamisch zur Laufzeit entschieden.
|
||||
* **Input:** User Message.
|
||||
* **Config:** `config/decision_engine.yaml` (Strategien & Keywords).
|
||||
* **Config:** `decision_engine.yaml` (Strategien) + `types.yaml` (Objekte).
|
||||
* **Komponenten:**
|
||||
* **Fast Path:** Keyword Matching (CPU-schonend).
|
||||
* **Slow Path:** LLM-basierter Semantic Router (für subtile Intents).
|
||||
* **Traffic Control:** `LLMService` priorisiert Chat-Anfragen.
|
||||
* **Question Detection:** Unterscheidung Frage vs. Befehl.
|
||||
|
||||
### 6.2 Schritt 1: Intent Detection (Hybrid)
|
||||
### 6.2 Traffic Control (Priorisierung)
|
||||
Der `LLMService` implementiert zwei Spuren basierend auf `app/services/llm_service.py` (v2.8.0):
|
||||
* **Realtime (Chat):** `priority="realtime"`. Umgeht die Semaphore. Antwortet sofort.
|
||||
* **Background (Import/Analyse):** `priority="background"`. Wartet in der Semaphore (`asyncio.Semaphore`), die lazy mit `MINDNET_LLM_BACKGROUND_LIMIT` initialisiert wird.
|
||||
|
||||
### 6.3 Schritt 1: Intent Detection (Question vs. Action)
|
||||
Der Router ermittelt die Absicht (`Intent`) des Nutzers.
|
||||
1. **Keyword Scan (Fast Path):**
|
||||
* Iteration über alle Strategien in `decision_engine.yaml`.
|
||||
* Prüfung auf `trigger_keywords`.
|
||||
* **Best Match:** Bei mehreren Treffern gewinnt das längste/spezifischste Keyword (Robustheit gegen Shadowing).
|
||||
2. **LLM Fallback (Slow Path):**
|
||||
* Nur aktiv, wenn `llm_fallback_enabled: true`.
|
||||
* Greift, wenn keine Keywords gefunden wurden.
|
||||
* Sendet die Query an das LLM mit einem Klassifizierungs-Prompt (`llm_router_prompt`).
|
||||
* Ergebnis: `EMPATHY`, `DECISION`, `INTERVIEW`, `CODING` oder `FACT`.
|
||||
1. **Frage-Check:** Wenn der Input ein `?` enthält oder mit W-Wörtern beginnt, wird der **RAG-Modus** erzwungen (oder LLM-Entscheidung). Interviews werden hier unterdrückt.
|
||||
2. **Keyword Scan (Fast Path):**
|
||||
* Prüfung auf `trigger_keywords` (Handlung) in `decision_engine.yaml`.
|
||||
* Prüfung auf `detection_keywords` (Objekt) in `types.yaml`.
|
||||
* Treffer -> **INTERVIEW Modus** (Erfassen).
|
||||
3. **LLM Fallback (Slow Path):**
|
||||
* Greift, wenn keine Keywords passen. Sendet Query an LLM Router.
|
||||
|
||||
### 6.3 Schritt 2: Strategy Resolution (Late Binding)
|
||||
### 6.4 Schritt 2: Strategy Resolution
|
||||
Basierend auf dem Intent lädt der Router die Parameter:
|
||||
* **Bei RAG (FACT/DECISION):** `inject_types` für Strategic Retrieval.
|
||||
* **Bei INTERVIEW (WP07):** `schemas` (Pflichtfelder) basierend auf der erkannten Ziel-Entität (`_detect_target_type`).
|
||||
|
||||
### 6.4 Schritt 3: Retrieval vs. Extraction
|
||||
### 6.5 Schritt 3: Retrieval vs. Extraction
|
||||
Der Router verzweigt hier:
|
||||
|
||||
**A) RAG Modus (FACT, DECISION, EMPATHY):**
|
||||
|
|
@ -324,9 +346,9 @@ Der Router verzweigt hier:
|
|||
2. **Schema Injection:** Das Schema für den erkannten Typ (z.B. "Project") wird geladen.
|
||||
3. **Prompt Assembly:** Der `interview_template` Prompt wird mit der Schema-Definition ("Ziel", "Status") gefüllt.
|
||||
|
||||
### 6.5 Schritt 4: Generation & Response
|
||||
### 6.6 Schritt 4: Generation & Response
|
||||
* **Templating:** Das LLM erhält den Prompt basierend auf dem gewählten Template.
|
||||
* **Execution:** Der `LLMService` führt den Call aus. Ein konfigurierbarer Timeout (`MINDNET_LLM_TIMEOUT`) fängt Cold-Start-Verzögerungen auf CPU-Hardware ab.
|
||||
* **Execution:** Der `LLMService` führt den Call mit `priority="realtime"` aus.
|
||||
* **Response:** Rückgabe enthält Antworttext (im Interview-Modus: Markdown Codeblock), Quellenliste und den erkannten `intent`.
|
||||
|
||||
---
|
||||
|
|
@ -337,8 +359,12 @@ Das Frontend ist eine **Streamlit-Anwendung** (`app/frontend/ui.py`), die als se
|
|||
|
||||
### 7.1 Kommunikation
|
||||
* **Backend-URL:** Konfiguriert via `MINDNET_API_URL` (Default: `http://localhost:8002`).
|
||||
* **Endpoints:** Nutzt `/chat` für Interaktion, `/feedback` für Bewertungen und `/ingest/analyze` für Intelligence.
|
||||
* **Resilienz:** Das Frontend implementiert eigene Timeouts (`MINDNET_API_TIMEOUT`, Default 300s).
|
||||
* **Endpoints:**
|
||||
* `/chat` (Interaktion)
|
||||
* `/feedback` (Bewertungen)
|
||||
* `/ingest/analyze` (Active Intelligence / Link-Vorschläge)
|
||||
* `/ingest/save` (Persistence / Speichern im Vault)
|
||||
* **Resilienz:** Das Frontend nutzt `MINDNET_API_TIMEOUT` (300s), um auf langsame Backend-Prozesse (Smart Edges) zu warten.
|
||||
|
||||
### 7.2 Features & UI-Logik
|
||||
* **State Management:** Session-State speichert Chat-Verlauf und `query_id`.
|
||||
|
|
@ -347,16 +373,16 @@ Das Frontend ist eine **Streamlit-Anwendung** (`app/frontend/ui.py`), die als se
|
|||
* **Source Expanders:** Zeigt verwendete Chunks inkl. Score und "Why"-Explanation.
|
||||
* **Sidebar:** Zeigt Suchhistorie (Log-basiert) und Konfiguration.
|
||||
|
||||
### 7.3 Draft-Editor & Sanitizer (Neu in WP10a)
|
||||
### 7.3 Draft-Editor & Healing Parser (Neu in WP10a/v2.5)
|
||||
Wenn der Intent `INTERVIEW` ist, rendert die UI statt einer Textblase den **Draft-Editor**.
|
||||
|
||||
1. **Parsing:** Die Funktion `parse_markdown_draft` extrahiert den Codeblock aus der LLM-Antwort.
|
||||
* **Healing:** Erkennt und repariert defekte YAML-Frontmatter (z.B. fehlendes schließendes `---`), falls das LLM unter Last Fehler macht.
|
||||
2. **Sanitization (`normalize_meta_and_body`):**
|
||||
* Prüft den YAML-Frontmatter auf unerlaubte Felder (Halluzinationen des LLMs).
|
||||
* Verschiebt ungültige Felder (z.B. "Situation") in den Body der Notiz.
|
||||
* Stellt sicher, dass das Markdown valide bleibt.
|
||||
3. **Editor Widget:** `st.text_area` erlaubt das Bearbeiten des Inhalts vor dem Speichern.
|
||||
4. **Action:** Buttons zum Download oder Kopieren des fertigen Markdowns.
|
||||
4. **Save Action:** Speichert über `/ingest/save` atomar in Vault und DB. Erzeugt intelligente Dateinamen via `slugify`.
|
||||
|
||||
### 7.4 State Management (Resurrection Pattern)
|
||||
Um Datenverlust bei Tab-Wechseln (Chat <-> Editor) zu verhindern, nutzt `ui.py` ein Persistenz-Muster:
|
||||
|
|
@ -420,5 +446,5 @@ Validierung erfolgt über `tests/ensure_indexes_and_show.py`.
|
|||
2. **Unresolved Targets:**
|
||||
* Kanten zu Notizen, die noch nicht existieren, werden mit `target_id="Titel"` angelegt.
|
||||
* Heilung durch `scripts/resolve_unresolved_references.py` möglich.
|
||||
3. **Vektor-Konfiguration für Edges:**
|
||||
* `mindnet_edges` hat aktuell keine Vektoren (`vectors = null`). Eine semantische Suche *auf Kanten* ist noch nicht möglich.
|
||||
3. **Hardware-Last bei Smart Import:**
|
||||
* Der "Smart Edge" Import ist rechenintensiv. Trotz Traffic Control kann die Antwortzeit im Chat leicht steigen, wenn die GPU am VRAM-Limit arbeitet.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# mindnet v2.4 – Pipeline Playbook
|
||||
**Datei:** `docs/mindnet_pipeline_playbook_v2.4.md`
|
||||
**Stand:** 2025-12-11
|
||||
**Status:** **FINAL** (Inkl. Async Ingestion & Active Intelligence)
|
||||
**Datei:** `docs/mindnet_pipeline_playbook_v2.6.md`
|
||||
**Stand:** 2025-12-12
|
||||
**Status:** **FINAL** (Inkl. Smart Edges & Traffic Control)
|
||||
**Quellen:** `mindnet_v2_implementation_playbook.md`, `Handbuch.md`, `chunking_strategy.md`, `docs_mindnet_retriever.md`, `mindnet_admin_guide_v2.4.md`.
|
||||
|
||||
---
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
- [](#)
|
||||
- [1. Zweck \& Einordnung](#1-zweck--einordnung)
|
||||
- [2. Die Import-Pipeline (Runbook)](#2-die-import-pipeline-runbook)
|
||||
- [2.1 Der 12-Schritte-Prozess (Async)](#21-der-12-schritte-prozess-async)
|
||||
- [2.1 Der 13-Schritte-Prozess (Async + Smart)](#21-der-13-schritte-prozess-async--smart)
|
||||
- [2.2 Standard-Betrieb (Inkrementell)](#22-standard-betrieb-inkrementell)
|
||||
- [2.3 Deployment \& Restart (Systemd)](#23-deployment--restart-systemd)
|
||||
- [2.4 Full Rebuild (Clean Slate)](#24-full-rebuild-clean-slate)
|
||||
|
|
@ -21,12 +21,13 @@
|
|||
- [3.2 Payload-Felder](#32-payload-felder)
|
||||
- [4. Edge-Erzeugung (Die V2-Logik)](#4-edge-erzeugung-die-v2-logik)
|
||||
- [4.1 Prioritäten \& Provenance](#41-prioritäten--provenance)
|
||||
- [4.2 Typ-Defaults](#42-typ-defaults)
|
||||
- [4.2 Smart Edge Allocation (WP15)](#42-smart-edge-allocation-wp15)
|
||||
- [4.3 Typ-Defaults](#43-typ-defaults)
|
||||
- [5. Retriever, Chat \& Generation (RAG Pipeline)](#5-retriever-chat--generation-rag-pipeline)
|
||||
- [5.1 Retrieval (Hybrid)](#51-retrieval-hybrid)
|
||||
- [5.2 Intent Router (WP06/07)](#52-intent-router-wp0607)
|
||||
- [5.3 Context Enrichment](#53-context-enrichment)
|
||||
- [5.4 Generation (LLM)](#54-generation-llm)
|
||||
- [5.4 Generation (LLM) mit Traffic Control](#54-generation-llm-mit-traffic-control)
|
||||
- [5.5 Active Intelligence Pipeline (Neu in v2.4)](#55-active-intelligence-pipeline-neu-in-v24)
|
||||
- [6. Feedback \& Lernen (WP04c)](#6-feedback--lernen-wp04c)
|
||||
- [7. Quality Gates \& Tests](#7-quality-gates--tests)
|
||||
|
|
@ -34,7 +35,7 @@
|
|||
- [7.2 Smoke-Test (E2E)](#72-smoke-test-e2e)
|
||||
- [8. Ausblick \& Roadmap (Technische Skizzen)](#8-ausblick--roadmap-technische-skizzen)
|
||||
- [8.1 WP-08: Self-Tuning (Skizze)](#81-wp-08-self-tuning-skizze)
|
||||
- [16. Workpackage Status (v2.4.0)](#16-workpackage-status-v240)
|
||||
- [16. Workpackage Status (v2.6.0)](#16-workpackage-status-v260)
|
||||
|
||||
</details>
|
||||
---
|
||||
|
|
@ -45,7 +46,7 @@ Dieses Playbook ist das zentrale operative Handbuch für die **mindnet-Pipeline*
|
|||
|
||||
**Zielgruppe:** Dev/Ops, Tech-Leads.
|
||||
**Scope:**
|
||||
* **Ist-Stand (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.
|
||||
|
||||
|
|
@ -271,3 +286,6 @@ Aktueller Implementierungsstand der Module.
|
|||
| **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.** |
|
||||
| **WP15** | Smart Edge Allocation | 🟢 Live | **LLM-Filter & Traffic Control aktiv.** |
|
||||
| **WP16** | Auto-Discovery | 🟡 Geplant | UX & Retrieval Tuning. |
|
||||
| **WP17** | Conversational Memory | 🟡 Geplant | Dialog-Gedächtnis. |
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
105
tests/test_dialog_full_flow.py
Normal file
105
tests/test_dialog_full_flow.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import unittest
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from app.routers.ingest import save_note, SaveRequest
|
||||
from app.core.ingestion import IngestionService
|
||||
|
||||
# Wir simulieren den API-Aufruf direkt
|
||||
class TestDialogFullFlow(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Test Vault definieren
|
||||
self.test_vault = os.path.abspath("./vault_test_dialog")
|
||||
os.environ["MINDNET_VAULT_ROOT"] = self.test_vault
|
||||
|
||||
# Sicherstellen, dass das Verzeichnis existiert
|
||||
if not os.path.exists(self.test_vault):
|
||||
os.makedirs(self.test_vault)
|
||||
|
||||
def tearDown(self):
|
||||
# Cleanup nach dem Test (optional, zum Debuggen auskommentieren)
|
||||
if os.path.exists(self.test_vault):
|
||||
shutil.rmtree(self.test_vault)
|
||||
|
||||
async def test_a_save_journal_entry(self):
|
||||
"""
|
||||
Testet den 'Fast Path' (Journal) -> sliding_short.
|
||||
Erwartung: Sehr schnell, keine LLM-Warnungen im Log.
|
||||
"""
|
||||
print("\n--- TEST A: Journal Save (Fast Path) ---")
|
||||
|
||||
content = """---
|
||||
type: journal
|
||||
status: active
|
||||
title: Test Journal Entry
|
||||
---
|
||||
# Daily Log
|
||||
Heute haben wir das Chunking-System getestet.
|
||||
Es lief alles sehr schnell durch.
|
||||
"""
|
||||
req = SaveRequest(
|
||||
markdown_content=content,
|
||||
filename="2025-12-12-test-journal.md",
|
||||
folder="00_Inbox"
|
||||
)
|
||||
|
||||
# Rufe die API-Funktion auf (simuliert Frontend "Save")
|
||||
response = await save_note(req)
|
||||
|
||||
# Checks
|
||||
self.assertEqual(response.status, "success")
|
||||
self.assertTrue(response.stats['chunks'] > 0, "Keine Chunks erstellt!")
|
||||
|
||||
# File Check
|
||||
file_path = Path(response.file_path)
|
||||
self.assertTrue(file_path.exists(), "Datei wurde nicht geschrieben!")
|
||||
print(f"✅ Journal gespeichert: {response.note_id}")
|
||||
|
||||
async def test_b_save_project_entry(self):
|
||||
"""
|
||||
Testet den 'Smart Path' (Project) -> sliding_smart_edges.
|
||||
Erwartung: Smart Edge Allocation läuft (LLM Aufruf), dauert etwas länger.
|
||||
"""
|
||||
print("\n--- TEST B: Project Save (Smart Path) ---")
|
||||
|
||||
# Ein Text mit expliziten Kanten, um den Smart Chunker zu triggern
|
||||
content = """---
|
||||
type: project
|
||||
status: active
|
||||
title: Test Projekt Smart Chunking
|
||||
---
|
||||
# Mission
|
||||
Wir wollen [[leitbild-werte#Integrität]] sicherstellen.
|
||||
Das System muss stabil sein.
|
||||
|
||||
# Status
|
||||
Wir nutzen [[rel:uses|Ollama]] für die Intelligenz.
|
||||
"""
|
||||
req = SaveRequest(
|
||||
markdown_content=content,
|
||||
filename="Test_Projekt_Smart.md",
|
||||
folder="Projects"
|
||||
)
|
||||
|
||||
# API Call
|
||||
response = await save_note(req)
|
||||
|
||||
# Checks
|
||||
self.assertEqual(response.status, "success")
|
||||
self.assertTrue(response.stats['chunks'] > 0)
|
||||
self.assertTrue(response.stats['edges'] > 0, "Kanten sollten gefunden werden (mind. structure edges)")
|
||||
|
||||
print(f"✅ Projekt gespeichert: {response.note_id}")
|
||||
|
||||
# Optional: Prüfen ob Qdrant Daten hat (via IngestionService Helper)
|
||||
service = IngestionService()
|
||||
chunks_missing, edges_missing = service._artifacts_missing(response.note_id)
|
||||
|
||||
self.assertFalse(chunks_missing, "Chunks fehlen in Qdrant!")
|
||||
self.assertFalse(edges_missing, "Edges fehlen in Qdrant!")
|
||||
print("✅ Qdrant Persistenz verifiziert.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
110
tests/test_final_wp15_validation.py
Normal file
110
tests/test_final_wp15_validation.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# tests/test_final_wp15_validation.py
|
||||
|
||||
import asyncio
|
||||
import unittest
|
||||
from typing import List, Dict, Any
|
||||
import re
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# --- PFAD-KORREKTUR ---
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
from app.core import chunker
|
||||
from app.services.semantic_analyzer import SemanticAnalyzer
|
||||
|
||||
def get_config_for_test(strategy: str, enable_smart_edge: bool) -> Dict[str, Any]:
|
||||
cfg = chunker.get_chunk_config("concept")
|
||||
cfg['strategy'] = strategy
|
||||
cfg['enable_smart_edge_allocation'] = enable_smart_edge
|
||||
# WICHTIG: Setze sehr kleine Werte, um Split bei kurzem Text zu erzwingen
|
||||
cfg['target'] = 50
|
||||
cfg['max'] = 100
|
||||
return cfg
|
||||
|
||||
TEST_NOTE_ID_SMART = "20251212-test-smart"
|
||||
TEST_NOTE_ID_LEGACY = "20251212-test-legacy"
|
||||
|
||||
TEST_MARKDOWN_SMART = """
|
||||
---
|
||||
id: 20251212-test-smart
|
||||
title: Integrationstest - Smart Edges
|
||||
type: concept
|
||||
status: active
|
||||
---
|
||||
# Teil 1: Wichtige Definition
|
||||
Die Mission ist: präsent sein.
|
||||
Dies entspricht unseren Werten [[leitbild-werte#Integrität]].
|
||||
|
||||
## Teil 2: Konflikt
|
||||
Der Konflikt zwischen [[leitbild-rollen#Vater]] und [[leitbild-rollen#Beruf]].
|
||||
Lösung: [[rel:depends_on leitbild-review#Weekly Review]].
|
||||
"""
|
||||
|
||||
# Verlängerter Text, um Split > 1 zu erzwingen (bei Target 50)
|
||||
TEST_MARKDOWN_SLIDING = """
|
||||
---
|
||||
id: 20251212-test-legacy
|
||||
title: Fließtext Protokoll
|
||||
type: journal
|
||||
status: active
|
||||
---
|
||||
Dies ist der erste Absatz. Er muss lang genug sein, damit der Chunker ihn schneidet.
|
||||
Wir schreiben hier über Rituale wie [[leitbild-rituale-system]] und viele andere Dinge.
|
||||
Das Wetter ist schön und die Programmierung läuft gut. Dies sind Füllsätze für Länge.
|
||||
|
||||
Dies ist der zweite Absatz. Er ist durch eine Leerzeile getrennt und sollte einen neuen Kontext bilden.
|
||||
Auch hier schreiben wir viel Text, damit die Token-Anzahl die Grenze von 50 Tokens überschreitet.
|
||||
Das System muss hier splitten.
|
||||
"""
|
||||
|
||||
class TestFinalWP15Integration(unittest.TestCase):
|
||||
|
||||
_analyzer_instance = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls._analyzer_instance = SemanticAnalyzer()
|
||||
chunker._semantic_analyzer_instance = cls._analyzer_instance
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# FIX: Kein explizites Loop-Closing hier, um RuntimeError zu vermeiden
|
||||
pass
|
||||
|
||||
def test_a_smart_edge_allocation(self):
|
||||
"""A: Prüft Smart Edge Allocation (LLM-Filter)."""
|
||||
config = get_config_for_test('by_heading', enable_smart_edge=True)
|
||||
|
||||
chunks = asyncio.run(chunker.assemble_chunks(
|
||||
note_id=TEST_NOTE_ID_SMART,
|
||||
md_text=TEST_MARKDOWN_SMART,
|
||||
note_type='concept',
|
||||
config=config
|
||||
))
|
||||
|
||||
self.assertTrue(len(chunks) >= 2, f"A1 Fehler: Erwartete >= 2 Chunks, bekam {len(chunks)}")
|
||||
print(f" -> Chunks generiert (Smart): {len(chunks)}")
|
||||
|
||||
def test_b_backward_compatibility(self):
|
||||
"""B: Prüft Sliding Window (Legacy)."""
|
||||
config = get_config_for_test('sliding_window', enable_smart_edge=False)
|
||||
|
||||
chunks = asyncio.run(chunker.assemble_chunks(
|
||||
note_id=TEST_NOTE_ID_LEGACY,
|
||||
md_text=TEST_MARKDOWN_SLIDING,
|
||||
note_type='journal',
|
||||
config=config
|
||||
))
|
||||
|
||||
# Sliding Window muss bei diesem langen Text > 1 Chunk liefern
|
||||
self.assertTrue(len(chunks) >= 2, f"B1 Fehler: Sliding Window lieferte nur {len(chunks)} Chunk(s). Split defekt.")
|
||||
|
||||
# Check: Keine LLM Kanten (da deaktiviert)
|
||||
injected = re.search(r'\[\[rel:', chunks[0].text)
|
||||
self.assertIsNone(injected, "B2 Fehler: LLM-Kanten trotz Deaktivierung gefunden!")
|
||||
print(f" -> Chunks generiert (Legacy): {len(chunks)}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
147
tests/test_smart_chunking_integration.py
Normal file
147
tests/test_smart_chunking_integration.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# tests/test_smart_chunking_integration.py (Letzte Korrektur zur Umgehung des AsyncIO-Fehlers)
|
||||
|
||||
import asyncio
|
||||
import unittest
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
|
||||
# --- PFAD-KORREKTUR ---
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
# ----------------------
|
||||
|
||||
# Import der Kernkomponenten
|
||||
from app.core import chunker
|
||||
from app.core import derive_edges
|
||||
from app.services.semantic_analyzer import SemanticAnalyzer
|
||||
|
||||
|
||||
# 1. Definieren der Test-Note (Simuliert eine journal.md Datei)
|
||||
TEST_NOTE_ID = "20251211-journal-sem-test"
|
||||
TEST_NOTE_TYPE = "journal"
|
||||
|
||||
TEST_MARKDOWN = """
|
||||
---
|
||||
id: 20251211-journal-sem-test
|
||||
title: Tägliches Log - Semantischer Test
|
||||
type: journal
|
||||
status: active
|
||||
created: 2025-12-11
|
||||
tags: [test, daily-log]
|
||||
---
|
||||
# Log-Eintrag 2025-12-11
|
||||
|
||||
Heute war ein guter Tag. Zuerst habe ich mit der R1 Meditation begonnen, um meinen Nordstern Fokus zu klären. Das Ritual [[leitbild-rituale-system]] hat mir geholfen, ruhig in den Tag zu starten. Ich habe gespürt, wie wichtig meine [[leitbild-werte#Integrität]] für meine Entscheidungen ist. Das ist das Fundament.
|
||||
|
||||
Am Nachmittag gab es einen Konflikt bei der Karate-Trainer-Ausbildung. Ein Schüler war uneinsichtig. Ich habe die Situation nach [[leitbild-prinzipien#P4 Gerechtigkeit & Fairness]] behandelt und beide Seiten gehört (Steelman). Das war anstrengend, aber ich habe meine [[leitbild-rollen#Karate-Trainer]] Mission erfüllt. Die Konsequenz war klar und ruhig.
|
||||
|
||||
Abends habe ich den wöchentlichen Load-Check mit meinem Partner gemacht. Das Paar-Ritual [[leitbild-rituale-system#R5]] hilft, das Ziel [[leitbild-ziele-portfolio#Nordstern Partner]] aktiv zu verfolgen. Es ist der operative Rhythmus für uns beide.
|
||||
"""
|
||||
|
||||
# --- ENTFERNEN DER KOMPLEXEN TEARDOWN-HILFEN ---
|
||||
# Wir entfernen die fehleranfällige asynchrone Schließungslogik.
|
||||
|
||||
|
||||
class TestSemanticChunking(unittest.TestCase):
|
||||
|
||||
_analyzer_instance = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Initialisiert den SemanticAnalyzer einmalig."""
|
||||
cls._analyzer_instance = SemanticAnalyzer()
|
||||
# Stellt sicher, dass der Chunker diese Singleton-Instanz verwendet
|
||||
chunker._semantic_analyzer_instance = cls._analyzer_instance
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""
|
||||
PRAGMATISCHE LÖSUNG: Überspringe das explizite Aclose() im Teardown,
|
||||
um den Event Loop Konflikt zu vermeiden. Die GC wird die Verbindung schließen.
|
||||
"""
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
self.config = chunker.get_chunk_config(TEST_NOTE_TYPE)
|
||||
|
||||
def test_a_strategy_selection(self):
|
||||
"""Prüft, ob die Strategie 'semantic_llm' für den Typ 'journal' gewählt wird."""
|
||||
self.assertEqual(self.config['strategy'], 'semantic_llm',
|
||||
"Fehler: 'journal' sollte die Strategie 'semantic_llm' nutzen.")
|
||||
|
||||
def test_b_llm_chunking_and_injection(self):
|
||||
"""
|
||||
Prüft den gesamten End-to-End-Flow: 1. LLM-Chunking, 2. Kanten-Injektion, 3. Kanten-Erkennung.
|
||||
"""
|
||||
|
||||
# --- 1. Chunking (Asynchron) ---
|
||||
chunks = asyncio.run(chunker.assemble_chunks(
|
||||
note_id=TEST_NOTE_ID,
|
||||
md_text=TEST_MARKDOWN,
|
||||
note_type=TEST_NOTE_TYPE
|
||||
))
|
||||
|
||||
print(f"\n--- LLM Chunker Output: {len(chunks)} Chunks ---")
|
||||
|
||||
# Assertion B1: Zerlegung (Das LLM muss mehr als 1 Chunk liefern)
|
||||
self.assertTrue(len(chunks) > 1,
|
||||
"Assertion B1 Fehler: LLM hat nicht zerlegt (Fallback aktiv). Prüfe LLM-Stabilität.")
|
||||
|
||||
# --- 2. Injektion prüfen ---
|
||||
chunk_1_text = chunks[0].text
|
||||
self.assertIn("[[rel:", chunk_1_text,
|
||||
"Assertion B2 Fehler: Der Chunk-Text muss die injizierte [[rel: Kante enthalten.")
|
||||
|
||||
# --- 3. Kanten-Derivation (Synchron) ---
|
||||
edges = derive_edges.build_edges_for_note(
|
||||
note_id=TEST_NOTE_ID,
|
||||
chunks=[c.__dict__ for c in chunks]
|
||||
)
|
||||
|
||||
print(f"--- Edge Derivation Output: {len(edges)} Kanten ---")
|
||||
|
||||
# Assertion B3: Mindestens 3 LLM-Kanten (inline:rel)
|
||||
llm_generated_edges = [
|
||||
e for e in edges
|
||||
if e.get('rule_id') == 'inline:rel' and e.get('source_id').startswith(TEST_NOTE_ID + '#sem')
|
||||
]
|
||||
|
||||
self.assertTrue(len(llm_generated_edges) >= 3,
|
||||
"Assertion B3 Fehler: Es wurden weniger als 3 semantische Kanten gefunden.")
|
||||
|
||||
# Assertion B4: Check für die Matrix-Logik / Werte-Kante
|
||||
has_matrix_kante = any(
|
||||
e['target_id'].startswith('leitbild-werte') and e['kind'] in ['based_on', 'derived_from']
|
||||
for e in llm_generated_edges
|
||||
)
|
||||
self.assertTrue(has_matrix_kante,
|
||||
"Assertion B4 Fehler: Die Matrix-Logik / Werte-Kante wurde nicht erkannt.")
|
||||
|
||||
print("\n✅ Integrationstest für Semantic Chunking erfolgreich.")
|
||||
|
||||
def test_c_draft_status_prevention(self):
|
||||
"""Prüft, ob 'draft' Status semantic_llm auf by_heading überschreibt."""
|
||||
|
||||
DRAFT_MARKDOWN = TEST_MARKDOWN.replace("status: active", "status: draft")
|
||||
|
||||
# 1. Chunking mit Draft Status
|
||||
chunks = asyncio.run(chunker.assemble_chunks(
|
||||
note_id=TEST_NOTE_ID,
|
||||
md_text=DRAFT_MARKDOWN,
|
||||
note_type=TEST_NOTE_TYPE
|
||||
))
|
||||
|
||||
# 2. Prüfen der Chunker-IDs
|
||||
self.assertFalse(chunks[0].id.startswith(TEST_NOTE_ID + '#sem'),
|
||||
"Assertion C1 Fehler: LLM-Chunking wurde für den Status 'draft' nicht verhindert.")
|
||||
|
||||
self.assertTrue(chunks[0].id.startswith(TEST_NOTE_ID + '#c'),
|
||||
"Assertion C2 Fehler: Fallback-Strategie 'by_heading' wurde nicht korrekt ausgeführt.")
|
||||
|
||||
print(f"\n✅ Prevention Test: Draft-Status hat LLM-Chunking verhindert (Fallback ID: {chunks[0].id}).")
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Starte den Semantic Chunking Integrationstest.")
|
||||
unittest.main()
|
||||
76
tests/test_wp15_final.py
Normal file
76
tests/test_wp15_final.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import unittest
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock, patch
|
||||
from app.core import chunker
|
||||
|
||||
class TestWP15Orchestration(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Basis Config
|
||||
self.config = {
|
||||
"strategy": "sliding_window",
|
||||
"enable_smart_edge_allocation": True,
|
||||
"target": 100, "max": 200
|
||||
}
|
||||
|
||||
@patch("app.core.chunker.get_semantic_analyzer")
|
||||
@patch("app.core.chunker.build_edges_for_note")
|
||||
def test_smart_allocation_flow(self, mock_build_edges, mock_get_analyzer):
|
||||
"""
|
||||
Prüft, ob Kanten gefunden, gefiltert und injiziert werden.
|
||||
"""
|
||||
# 1. Mock Edge Discovery (Simuliert derive_edges.py)
|
||||
# Wir simulieren, dass im Text 2 Kanten gefunden wurden.
|
||||
mock_build_edges.return_value = [
|
||||
{"kind": "uses", "target_id": "tool_a"},
|
||||
{"kind": "references", "target_id": "doc_b"}
|
||||
]
|
||||
|
||||
# 2. Mock LLM Analyzer (Simuliert semantic_analyzer.py)
|
||||
mock_analyzer_instance = MagicMock()
|
||||
mock_get_analyzer.return_value = mock_analyzer_instance
|
||||
|
||||
# Simuliere LLM Antwort: Chunk 1 bekommt "tool_a", Chunk 2 bekommt nichts.
|
||||
async def mock_assign(text, candidates, type):
|
||||
if "Tool A" in text:
|
||||
return ["uses:tool_a"]
|
||||
return []
|
||||
|
||||
mock_analyzer_instance.assign_edges_to_chunk.side_effect = mock_assign
|
||||
|
||||
# 3. Run Chunker
|
||||
md_text = """
|
||||
# Intro
|
||||
Hier nutzen wir Tool A für Tests.
|
||||
|
||||
# Outro
|
||||
Hier ist nur Text ohne Tool.
|
||||
"""
|
||||
|
||||
# Wir führen assemble_chunks aus (im Event Loop des Tests)
|
||||
chunks = asyncio.run(chunker.assemble_chunks(
|
||||
"test_note", md_text, "concept", config=self.config
|
||||
))
|
||||
|
||||
# 4. Assertions
|
||||
|
||||
# Check: Wurde derive_edges aufgerufen?
|
||||
mock_build_edges.assert_called_once()
|
||||
|
||||
# Check: Wurde LLM Analyzer aufgerufen?
|
||||
self.assertTrue(mock_analyzer_instance.assign_edges_to_chunk.called)
|
||||
|
||||
# Check: Injection in Chunk 1 (Tool A Text)
|
||||
chunk_with_tool = next((c for c in chunks if "Tool A" in c.text), None)
|
||||
self.assertIsNotNone(chunk_with_tool)
|
||||
self.assertIn("[[rel:uses|tool_a]]", chunk_with_tool.text, "Kante wurde nicht injiziert!")
|
||||
|
||||
# Check: Fallback (Die Kante 'references:doc_b' wurde vom LLM nirgends zugeordnet)
|
||||
# Sie sollte also in ALLEN Chunks als Fallback landen.
|
||||
for c in chunks:
|
||||
self.assertIn("[[rel:references|doc_b]]", c.text, "Fallback-Kante fehlt!")
|
||||
|
||||
print("✅ WP-15 Logic Test passed.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Loading…
Reference in New Issue
Block a user