WP11 #8

Merged
Lars merged 30 commits from WP11 into main 2025-12-11 17:00:38 +01:00
24 changed files with 1385 additions and 886 deletions

View File

@ -1,10 +1,10 @@
# mindnet v2.2 — Programmplan
**Version:** 2.4.0 (Inkl. WP-07 Interview & WP-10a Draft Editor)
**Stand:** 2025-12-10
# mindnet v2.4 — Programmplan
**Version:** 2.4.0 (Inkl. WP-11 Backend Intelligence)
**Stand:** 2025-12-11
**Status:** Aktiv
---
- [mindnet v2.2 — Programmplan](#mindnet-v22--programmplan)
- [mindnet v2.4 — Programmplan](#mindnet-v24--programmplan)
- [1. Programmauftrag](#1-programmauftrag)
- [2. Vision](#2-vision)
- [3. Programmziele](#3-programmziele)
@ -29,7 +29,7 @@
- [WP-09 Vault-Onboarding \& Migration (geplant)](#wp-09--vault-onboarding--migration-geplant)
- [WP-10 Chat-Interface \& Writeback (abgeschlossen)](#wp-10--chat-interface--writeback-abgeschlossen)
- [WP-10a GUI Evolution: Draft Editor (abgeschlossen)](#wp-10a--gui-evolution-draft-editor-abgeschlossen)
- [WP-11 Knowledge-Builder \& Vernetzungs-Assistent (geplant)](#wp-11--knowledge-builder--vernetzungs-assistent-geplant)
- [WP-11 Backend Intelligence \& Persistence (abgeschlossen)](#wp-11--backend-intelligence--persistence-abgeschlossen)
- [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)
@ -51,7 +51,8 @@ mindnet v2.4 entwickelt ein persönliches, wachsendes KI-Gedächtnis, das:
- über mehrere Kanäle gefüttert wird:
- Obsidian-Markdown (primäre Quelle),
- Chat-basierter Agent (Decision Engine & RAG-Chat aktiv),
- **Interview-Assistent (One-Shot Extraction aktiv)**,
- Interview-Assistent (One-Shot Extraction aktiv),
- **Draft Editor (Active Intelligence aktiv)**,
- automatisch neue Zusammenhänge erkennt und vernetzt (Edges, Typen, Hinweise),
- sich durch Rückmeldungen (Feedback) selbst verbessert (Self-Tuning).
@ -116,7 +117,8 @@ Kernprinzipien der Vision:
- **Multi-Persona:** System wechselt den Tonfall (Empathisch vs. Analytisch) situativ (WP-06 abgeschlossen).
- **Chat Interface:** Web-basiertes Frontend (Streamlit) für einfache Interaktion und Feedback-Gabe (WP-10 abgeschlossen).
- **Interview-Assistent (WP-07):** One-Shot Extraction von Notizen ("Neues Projekt anlegen") ist live.
- Technische Basis: FastAPI, Qdrant, Ollama (Local LLM), Streamlit.
- **Active Intelligence (WP-11):** Automatische Link-Vorschläge (Matrix-Logik) während des Schreibens.
- Technische Basis: FastAPI (Async), Qdrant (768 Dim), Ollama (Phi-3/Nomic), Streamlit.
- Automatisierte Erkennung von Beziehungen:
- Wikilinks, Inline-Relationen, Callout-Edges, Typ-Defaults.
- „Mitwachsendes“ Schema ohne Obsidian-Umstrukturierungen:
@ -126,8 +128,7 @@ Kernprinzipien der Vision:
### 3.2 Mittelfristig (Nächste Schritte)
- **Self-Tuning (WP-08):** Optimierung der Gewichte in `retriever.yaml` basierend auf dem gesammelten Feedback.
- **Knowledge-Builder (WP-11):** Assistent zur Analyse und Vernetzung manuell erstellter Notizen.
- Agenten können über MCP-Tools (`mindnet_query`, `mindnet_chat`) auf mindnet zugreifen.
- Agenten können über MCP-Tools (`mindnet_query`, `mindnet_chat`) auf mindnet zugreifen (WP-13).
### 3.3 Langfristig
@ -180,7 +181,7 @@ Die folgenden Prinzipien steuern alle Workpackages und Entscheidungen:
- Jeder Importlauf, jede Retriever-Anfrage und jede Policy-Änderung soll prüfbar sein.
10. **Local First & Privacy**
- Nutzung lokaler LLMs (Ollama/Phi-3) für Inference. Keine Daten verlassen den Server.
- Nutzung lokaler LLMs (Ollama) für Inference. Keine Daten verlassen den Server.
---
@ -192,7 +193,7 @@ Die folgenden Prinzipien steuern alle Workpackages und Entscheidungen:
Phase D Agenten, MCP & Interaktion (Aktiv)
Phase E Review, Refactoring, Dokumentation
Alle Workpackages sind einer Phase zugeordnet. WP-01 bis WP-07 und WP-10/10a sind erfolgreich abgeschlossen.
Alle Workpackages sind einer Phase zugeordnet. WP-01 bis WP-07 und WP-10/10a/11 sind erfolgreich abgeschlossen.
---
@ -447,13 +448,20 @@ Anpassung der GUI an komplexe Interaktionsmuster, die durch den Interview-Assist
---
### WP-11 Knowledge-Builder & Vernetzungs-Assistent (geplant)
### WP-11 Backend Intelligence & Persistence (abgeschlossen)
**Phase:** D
**Status:** 🟡 geplant
**Status:** 🟢 abgeschlossen
**Ziel:**
Assistent, der manuell erstellte oder importierte Notizen analysiert und Vorschläge für Typen, Edges und Einordnung macht.
Ermöglichung von "Active Intelligence" durch asynchrone Verarbeitung und semantische Analyse im Hintergrund.
**Erreichte Ergebnisse:**
- **Async Core:** Umstellung der Pipeline auf `asyncio` und `httpx` (Vermeidung von Blockaden).
- **Nomic Embeddings:** Integration von `nomic-embed-text` (768 Dim) für State-of-the-Art Semantik.
- **Matrix Logic:** Regelwerk für kontextsensitive Kanten (`experience` + `value` -> `based_on`).
- **Sliding Window:** Analyse langer Texte für Link-Vorschläge.
- **Persistence API:** Neuer Endpunkt `/ingest/save` für atomares Speichern & Indizieren.
**Aufwand / Komplexität:**
- Aufwand: Hoch
@ -557,7 +565,7 @@ Aufräumen, dokumentieren, stabilisieren insbesondere für Onboarding Dritte
| WP09 | 🟡 |
| WP10 | 🟢 |
| WP10a | 🟢 |
| WP11 | 🟡 |
| WP11 | 🟢 |
| WP12 | 🟡 |
| WP13 | 🟡 |
| WP14 | 🟡 |
@ -585,7 +593,8 @@ mindnet v2.4 ist so aufgesetzt, dass:
- ein **Self-Healing- und Self-Tuning-Mechanismus** vorbereitet ist (durch WP-04c Feedback-Daten),
- ein **Persönlichkeitsmodell** (Decision Engine, Empathie) existiert und den Tonfall situativ anpasst,
- eine **grafische Oberfläche** (WP-10/10a) existiert, die komplexe Zusammenhänge visualisiert und Co-Creation ermöglicht,
- **Active Intelligence** (WP-11) dich beim Schreiben unterstützt, indem es automatisch Verknüpfungen vorschlägt,
- langfristig ein **KI-Zwilling** aufgebaut wird, der deine Werte, Erfahrungen und Denkweise spiegelt,
- die technische Architektur (FastAPI, Qdrant, YAML-Policies, MCP-Integration) lokal, nachvollziehbar und erweiterbar bleibt.
- die technische Architektur (AsyncIO, Qdrant 768d, YAML-Policies) lokal, nachvollziehbar und performant bleibt.
Dieser Programmplan bildet die konsolidierte Grundlage (v2.4.0) für alle weiteren Arbeiten.

View File

@ -5,6 +5,7 @@ app/core/chunk_payload.py (Mindnet V2 — types.yaml authoritative)
- neighbors_prev / neighbors_next sind Listen ([], [id]).
- retriever_weight / chunk_profile kommen aus types.yaml (Frontmatter wird ignoriert).
- Fallbacks: defaults.* in types.yaml; sonst 1.0 / "default".
- WP-11 Update: Injects 'title' into chunk payload for Discovery Service.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
@ -82,6 +83,11 @@ def make_chunk_payloads(note: Dict[str, Any],
file_path: Optional[str] = None) -> List[Dict[str, Any]]:
fm = (note or {}).get("frontmatter", {}) or {}
note_type = fm.get("type") or note.get("type") or "concept"
# WP-11 FIX: Title Extraction für Discovery Service
# Wir holen den Titel aus Frontmatter oder Fallback ID/Untitled
title = fm.get("title") or note.get("title") or fm.get("id") or "Untitled"
reg = types_cfg if isinstance(types_cfg, dict) else _load_types()
# types.yaml authoritative
@ -106,6 +112,7 @@ def make_chunk_payloads(note: Dict[str, Any],
pl: Dict[str, Any] = {
"note_id": nid,
"chunk_id": cid,
"title": title, # <--- HIER: Titel in Payload einfügen
"index": int(index),
"ord": int(index) + 1,
"type": note_type,

343
app/core/ingestion.py Normal file
View File

@ -0,0 +1,343 @@
"""
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.
"""
import os
import logging
from typing import Dict, List, Optional, Tuple, Any
# Core Module Imports
from app.core.parser import (
read_markdown,
normalize_frontmatter,
validate_required_frontmatter,
)
from app.core.note_payload import make_note_payload
from app.core.chunker import assemble_chunks
from app.core.chunk_payload import make_chunk_payloads
# Fallback für Edges Import (Robustheit)
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
from app.core.qdrant_points import (
points_for_chunks,
points_for_note,
points_for_edges,
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 ---
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 {}
try:
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
return "concept"
def effective_chunk_profile(note_type: str, reg: dict) -> str:
t_cfg = reg.get("types", {}).get(note_type, {})
if t_cfg and t_cfg.get("chunk_profile"):
return t_cfg.get("chunk_profile")
return reg.get("defaults", {}).get("chunk_profile", "default")
def effective_retriever_weight(note_type: str, reg: dict) -> float:
t_cfg = reg.get("types", {}).get(note_type, {})
if t_cfg and "retriever_weight" in t_cfg:
return float(t_cfg["retriever_weight"])
return float(reg.get("defaults", {}).get("retriever_weight", 1.0))
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
self.cfg = QdrantConfig.from_env()
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}")
async def process_file(
self,
file_path: str,
vault_root: str,
force_replace: bool = False,
apply: bool = False,
purge_before: bool = False,
note_scope_refs: bool = False,
hash_mode: str = "body",
hash_source: str = "parsed",
hash_normalize: str = "canonical"
) -> Dict[str, Any]:
"""
Verarbeitet eine einzelne Datei (ASYNC Version).
"""
result = {
"path": file_path,
"status": "skipped",
"changed": False,
"error": None
}
# 1. Parse & Frontmatter
try:
parsed = read_markdown(file_path)
if not parsed:
return {**result, "error": "Empty or unreadable file"}
fm = normalize_frontmatter(parsed.frontmatter)
validate_required_frontmatter(fm)
except Exception as e:
logger.error(f"Validation failed for {file_path}: {e}")
return {**result, "error": f"Validation failed: {str(e)}"}
# 2. Type & Config Resolution
note_type = resolve_note_type(fm.get("type"), self.registry)
fm["type"] = note_type
fm["chunk_profile"] = effective_chunk_profile(note_type, self.registry)
weight = fm.get("retriever_weight")
if weight is None:
weight = effective_retriever_weight(note_type, self.registry)
fm["retriever_weight"] = float(weight)
# 3. Build Note Payload
try:
note_pl = make_note_payload(
parsed,
vault_root=vault_root,
hash_mode=hash_mode,
hash_normalize=hash_normalize,
hash_source=hash_source,
file_path=file_path
)
if not note_pl.get("fulltext"):
note_pl["fulltext"] = getattr(parsed, "body", "") or ""
note_pl["retriever_weight"] = fm["retriever_weight"]
note_id = note_pl["note_id"]
except Exception as e:
logger.error(f"Payload build failed: {e}")
return {**result, "error": f"Payload build failed: {str(e)}"}
# 4. Change Detection
old_payload = None
if not force_replace:
old_payload = self._fetch_note_payload(note_id)
has_old = old_payload is not None
key_current = f"{hash_mode}:{hash_source}:{hash_normalize}"
old_hash = (old_payload or {}).get("hashes", {}).get(key_current)
new_hash = note_pl.get("hashes", {}).get(key_current)
hash_changed = (old_hash != new_hash)
chunks_missing, edges_missing = self._artifacts_missing(note_id)
should_write = force_replace or (not has_old) or hash_changed or chunks_missing or edges_missing
if not should_write:
return {**result, "status": "unchanged", "note_id": note_id}
if not apply:
return {**result, "status": "dry-run", "changed": True, "note_id": note_id}
# 5. Processing (Chunking, Embedding, Edges)
try:
body_text = getattr(parsed, "body", "") or ""
chunks = assemble_chunks(fm["id"], body_text, fm["type"])
chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text)
# --- EMBEDDING FIX (ASYNC) ---
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}")
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,
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:
logger.error(f"Processing failed: {e}", exc_info=True)
return {**result, "error": f"Processing failed: {str(e)}"}
# 6. Upsert Action
try:
if purge_before and has_old:
self._purge_artifacts(note_id)
n_name, n_pts = points_for_note(self.prefix, note_pl, None, self.dim)
upsert_batch(self.client, n_name, n_pts)
if chunk_pls and vecs:
c_name, c_pts = points_for_chunks(self.prefix, chunk_pls, vecs)
upsert_batch(self.client, c_name, c_pts)
if edges:
e_name, e_pts = points_for_edges(self.prefix, edges)
upsert_batch(self.client, e_name, e_pts)
return {
"path": file_path,
"status": "success",
"changed": True,
"note_id": note_id,
"chunks_count": len(chunk_pls),
"edges_count": len(edges)
}
except Exception as e:
logger.error(f"Upsert failed: {e}", exc_info=True)
return {**result, "error": f"DB Upsert failed: {e}"}
# --- Interne Qdrant Helper ---
def _fetch_note_payload(self, note_id: str) -> Optional[dict]:
from qdrant_client.http import models as rest
col = f"{self.prefix}_notes"
try:
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
pts, _ = self.client.scroll(collection_name=col, scroll_filter=f, limit=1, with_payload=True)
return pts[0].payload if pts else None
except: return None
def _artifacts_missing(self, note_id: str) -> Tuple[bool, bool]:
from qdrant_client.http import models as rest
c_col = f"{self.prefix}_chunks"
e_col = f"{self.prefix}_edges"
try:
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
c_pts, _ = self.client.scroll(collection_name=c_col, scroll_filter=f, limit=1)
e_pts, _ = self.client.scroll(collection_name=e_col, scroll_filter=f, limit=1)
return (not bool(c_pts)), (not bool(e_pts))
except: return True, True
def _purge_artifacts(self, note_id: str):
from qdrant_client.http import models as rest
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
selector = rest.FilterSelector(filter=f)
for suffix in ["chunks", "edges"]:
try:
self.client.delete(collection_name=f"{self.prefix}_{suffix}", points_selector=selector)
except Exception:
pass
async def create_from_text(
self,
markdown_content: str,
filename: str,
vault_root: str,
folder: str = "00_Inbox"
) -> 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)
# 3. Schreiben
try:
with open(file_path, "w", encoding="utf-8") as f:
f.write(markdown_content)
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)}"}
# 4. Indizieren (Async Aufruf!)
# Wir rufen process_file auf, das jetzt ASYNC ist
return await self.process_file(
file_path=file_path,
vault_root=vault_root,
apply=True,
force_replace=True,
purge_before=True
)

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
"""
Modul: app/core/note_payload.py
Version: 2.0.0
Version: 2.1.0 (WP-11 Update: Aliases support)
Zweck
-----
@ -145,6 +145,7 @@ def make_note_payload(note: Any, *args, **kwargs) -> Dict[str, Any]:
- retriever_weight: effektives Gewicht für den Retriever
- chunk_profile: Profil für Chunking (short|medium|long|default|...)
- edge_defaults: Liste von Kanten-Typen, die als Defaults gelten
- aliases: Liste von Synonymen (WP-11)
"""
n = _as_dict(note)
path_arg, types_cfg_explicit = _pick_args(*args, **kwargs)
@ -214,12 +215,21 @@ def make_note_payload(note: Any, *args, **kwargs) -> Dict[str, Any]:
if tags:
payload["tags"] = _ensure_list(tags)
# WP-11: Aliases übernehmen (für Discovery Service)
aliases = fm.get("aliases")
if aliases:
payload["aliases"] = _ensure_list(aliases)
# Zeitliche Metadaten (sofern vorhanden)
for k in ("created", "modified", "date"):
v = fm.get(k) or n.get(k)
if v:
payload[k] = str(v)
# Fulltext (Fallback, falls body im Input)
if "body" in n and n["body"]:
payload["fulltext"] = str(n["body"])
# JSON-Roundtrip zur harten Validierung (ASCII beibehalten)
json.loads(json.dumps(payload, ensure_ascii=False))

View File

@ -14,6 +14,8 @@ load_dotenv()
API_BASE_URL = os.getenv("MINDNET_API_URL", "http://localhost:8002")
CHAT_ENDPOINT = f"{API_BASE_URL}/chat"
FEEDBACK_ENDPOINT = f"{API_BASE_URL}/feedback"
INGEST_ANALYZE_ENDPOINT = f"{API_BASE_URL}/ingest/analyze"
INGEST_SAVE_ENDPOINT = f"{API_BASE_URL}/ingest/save"
HISTORY_FILE = Path("data/logs/search_history.jsonl")
# Timeout Strategy
@ -21,7 +23,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.2", page_icon="🧠", layout="wide")
st.set_page_config(page_title="mindnet v2.3.10", page_icon="🧠", layout="wide")
# --- CSS STYLING ---
st.markdown("""
@ -52,10 +54,13 @@ st.markdown("""
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
}
.debug-info {
font-size: 0.7rem;
color: #888;
margin-bottom: 5px;
.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)
@ -67,20 +72,14 @@ if "user_id" not in st.session_state: st.session_state.user_id = str(uuid.uuid4(
# --- HELPER FUNCTIONS ---
def normalize_meta_and_body(meta, body):
"""
Sanitizer: Stellt sicher, dass nur erlaubte Felder im Frontmatter bleiben.
Alles andere wird in den Body verschoben (Repair-Strategie).
"""
"""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 = []
# 1. Title/Titel Normalisierung
if "titel" in meta and "title" not in meta:
meta["title"] = meta.pop("titel")
# 2. Tags Normalisierung (Synonyme)
tag_candidates = ["tags", "emotionale_keywords", "keywords", "schluesselwoerter"]
all_tags = []
for key in tag_candidates:
@ -89,14 +88,12 @@ def normalize_meta_and_body(meta, body):
if isinstance(val, list): all_tags.extend(val)
elif isinstance(val, str): all_tags.extend([t.strip() for t in val.split(",")])
# 3. Filterung und Verschiebung
for key, val in meta.items():
if key in ALLOWED_KEYS:
clean_meta[key] = val
elif key in tag_candidates:
pass # Schon oben behandelt
pass
else:
# Unerlaubtes Feld (z.B. 'situation') -> Ab in den Body!
if val and isinstance(val, str):
header = key.replace("_", " ").title()
extra_content.append(f"## {header}\n{val}\n")
@ -104,7 +101,6 @@ def normalize_meta_and_body(meta, body):
if all_tags:
clean_meta["tags"] = list(set(all_tags))
# 4. Body Zusammenbau
if extra_content:
new_section = "\n".join(extra_content)
final_body = f"{new_section}\n{body}"
@ -114,18 +110,14 @@ def normalize_meta_and_body(meta, body):
return clean_meta, final_body
def parse_markdown_draft(full_text):
"""
Robustes Parsing + Sanitization.
"""
"""Robustes Parsing + Sanitization."""
clean_text = full_text
# Codeblock entfernen
pattern_block = r"```(?:markdown|md)?\s*(.*?)\s*```"
match_block = re.search(pattern_block, full_text, re.DOTALL | re.IGNORECASE)
if match_block:
clean_text = match_block.group(1).strip()
# Frontmatter splitten
parts = re.split(r"^---+\s*$", clean_text, maxsplit=2, flags=re.MULTILINE)
meta = {}
@ -152,7 +144,6 @@ def build_markdown_doc(meta, body):
meta["updated"] = datetime.now().strftime("%Y-%m-%d")
# Sortierung für UX
ordered_meta = {}
prio_keys = ["id", "type", "title", "status", "tags"]
for k in prio_keys:
@ -183,6 +174,8 @@ def load_history_from_logs(limit=10):
except: pass
return queries
# --- API CLIENT ---
def send_chat_message(message: str, top_k: int, explain: bool):
try:
response = requests.post(
@ -195,6 +188,32 @@ def send_chat_message(message: str, top_k: int, explain: bool):
except Exception as e:
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,
json={"text": text, "type": n_type},
timeout=15
)
response.raise_for_status()
return response.json()
except Exception as e:
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
)
response.raise_for_status()
return response.json()
except Exception as e:
return {"error": str(e)}
def submit_feedback(query_id, node_id, score, comment=None):
try:
requests.post(FEEDBACK_ENDPOINT, json={"query_id": query_id, "node_id": node_id, "score": score, "comment": comment}, timeout=2)
@ -206,7 +225,7 @@ def submit_feedback(query_id, node_id, score, comment=None):
def render_sidebar():
with st.sidebar:
st.title("🧠 mindnet")
st.caption("v2.3.2 | WP-10 UI")
st.caption("v2.3.10 | Mode Switch Fix")
mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0)
st.divider()
st.subheader("⚙️ Settings")
@ -221,64 +240,157 @@ def render_sidebar():
return mode, top_k, explain
def render_draft_editor(msg):
qid = msg.get('query_id', str(uuid.uuid4()))
# Ensure ID Stability
if "query_id" not in msg or not msg["query_id"]:
msg["query_id"] = str(uuid.uuid4())
qid = msg["query_id"]
key_base = f"draft_{qid}"
# 1. Init
# State Keys
data_meta_key = f"{key_base}_data_meta"
data_sugg_key = f"{key_base}_data_suggestions"
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) ---
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"
if "title" not in meta: meta["title"] = ""
tags = meta.get("tags", [])
meta["tags_str"] = ", ".join(tags) if isinstance(tags, list) else str(tags)
st.session_state[f"{key_base}_type"] = meta.get("type", "default")
st.session_state[f"{key_base}_title"] = meta.get("title", "")
# Persistent Data (Source of Truth)
st.session_state[data_meta_key] = meta
st.session_state[data_sugg_key] = []
st.session_state[data_body_key] = body.strip()
tags_raw = meta.get("tags", [])
st.session_state[f"{key_base}_tags"] = ", ".join(tags_raw) if isinstance(tags_raw, list) else str(tags_raw)
st.session_state[f"{key_base}_body"] = body.strip()
st.session_state[f"{key_base}_meta"] = meta
st.session_state[f"{key_base}_init"] = True
# 2. UI
# --- 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.
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_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
st.session_state[data_body_key] = new_text
def _remove_text(text_to_remove):
current = st.session_state.get(widget_body_key, "")
new_text = current.replace(text_to_remove, "").strip()
st.session_state[widget_body_key] = new_text
st.session_state[data_body_key] = new_text
# --- UI LAYOUT ---
st.markdown(f'<div class="draft-box">', unsafe_allow_html=True)
st.markdown("### 📝 Entwurf bearbeiten")
# Metadata
# Metadata Form
meta_ref = st.session_state[data_meta_key]
c1, c2 = st.columns([2, 1])
with c1:
new_title = st.text_input("Titel", value=st.session_state.get(f"{key_base}_title", ""), key=f"{key_base}_inp_title")
with c2:
known_types = ["concept", "project", "decision", "experience", "journal", "person", "value", "goal", "principle", "default"]
curr_type = st.session_state.get(f"{key_base}_type", "default")
if curr_type not in known_types: known_types.append(curr_type)
new_type = st.selectbox("Typ", known_types, index=known_types.index(curr_type), key=f"{key_base}_sel_type")
# 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)
new_tags = st.text_input("Tags (kommagetrennt)", value=st.session_state.get(f"{key_base}_tags", ""), key=f"{key_base}_inp_tags")
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)
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)
# Tabs
tab_edit, tab_view = st.tabs(["✏️ Inhalt", "👁️ Vorschau"])
tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"])
# --- TAB 1: EDITOR ---
with tab_edit:
st.caption("Bearbeite hier den Inhalt. Metadaten (oben) werden automatisch hinzugefügt.")
new_body = st.text_area(
# Hier kein 'value' Argument mehr, da wir den Key oben (Resurrection) initialisiert haben.
st.text_area(
"Body",
value=st.session_state.get(f"{key_base}_body", ""),
key=widget_body_key,
height=500,
key=f"{key_base}_txt_body",
on_change=_sync_body,
label_visibility="collapsed"
)
# Reassembly
final_tags_list = [t.strip() for t in new_tags.split(",") if t.strip()]
final_meta = st.session_state.get(f"{key_base}_meta", {}).copy()
final_meta.update({
"id": "generated_on_save",
"type": new_type,
"title": new_title,
"status": "draft",
"tags": final_tags_list
})
# --- TAB 2: INTELLIGENCE ---
with tab_intel:
st.info("Klicke auf 'Analysieren', um Verknüpfungen für den AKTUELLEN Text zu finden.")
final_doc = build_markdown_doc(final_meta, new_body)
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, ""))
with st.spinner("Analysiere..."):
analysis = analyze_draft_text(text_to_analyze, meta_ref["type"])
if "error" in analysis:
st.error(f"Fehler: {analysis['error']}")
else:
suggestions = analysis.get("suggestions", [])
st.session_state[data_sugg_key] = suggestions
if not suggestions:
st.warning("Keine Vorschläge gefunden.")
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, "")
for idx, sugg in enumerate(suggestions):
link_text = sugg.get('suggested_markdown', '')
is_inserted = link_text in current_text_state
bg_color = "#e6fffa" if is_inserted else "#ffffff"
border = "3px solid #28a745" if is_inserted else "3px solid #1a73e8"
st.markdown(f"""
<div style="border-left: {border}; background-color: {bg_color}; padding: 10px; margin-bottom: 8px; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<b>{sugg.get('target_title')}</b> <small>({sugg.get('type')})</small><br>
<i>{sugg.get('reason')}</i><br>
<code>{link_text}</code>
</div>
""", unsafe_allow_html=True)
if is_inserted:
st.button("❌ Entfernen", key=f"del_{idx}_{key_base}", on_click=_remove_text, args=(link_text,))
else:
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_meta = {
"id": "generated_on_save",
"type": meta_ref["type"],
"title": meta_ref["title"],
"status": "draft",
"tags": final_tags
}
# Final Doc aus Data
final_body = st.session_state.get(widget_body_key, st.session_state[data_body_key])
final_doc = build_markdown_doc(final_meta, final_body)
with tab_view:
st.markdown('<div class="preview-box">', unsafe_allow_html=True)
@ -287,11 +399,19 @@ def render_draft_editor(msg):
st.markdown("---")
# Actions
b1, b2 = st.columns([1, 1])
with b1:
fname = f"{datetime.now().strftime('%Y%m%d')}-{new_type}.md"
st.download_button("💾 Download .md", data=final_doc, file_name=fname, mime="text/markdown")
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"
fname = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md"
result = save_draft_to_vault(final_doc, filename=fname)
if "error" in result:
st.error(f"Fehler: {result['error']}")
else:
st.success(f"Gespeichert: {result.get('file_path')}")
st.balloons()
with b2:
if st.button("📋 Code anzeigen", key=f"{key_base}_btn_copy"):
st.code(final_doc, language="markdown")
@ -303,13 +423,12 @@ 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":
# Meta
# Header
intent = msg.get("intent", "UNKNOWN")
src = msg.get("intent_source", "?")
icon = {"EMPATHY":"❤️", "DECISION":"⚖️", "CODING":"💻", "FACT":"📚", "INTERVIEW":"📝"}.get(intent, "🧠")
st.markdown(f'<div class="intent-badge">{icon} Intent: {intent} <span style="opacity:0.6; font-size:0.8em">({src})</span></div>', unsafe_allow_html=True)
# Debugging (Always visible for safety)
with st.expander("🐞 Debug Raw Payload", expanded=False):
st.json(msg)
@ -359,15 +478,13 @@ def render_chat_interface(top_k, explain):
st.rerun()
def render_manual_editor():
st.header("📝 Manueller Editor")
c1, c2 = st.columns([1, 2])
n_type = c1.selectbox("Typ", ["concept", "project", "decision", "experience", "value", "goal"])
tags = c2.text_input("Tags")
body = st.text_area("Inhalt", height=400, placeholder="# Titel\n\nText...")
if st.button("Code anzeigen"):
meta = {"type": n_type, "status": "draft", "tags": [t.strip() for t in tags.split(",")]}
st.code(build_markdown_doc(meta, body), language="markdown")
mock_msg = {
"content": "---\ntype: concept\ntitle: Neue Notiz\nstatus: draft\ntags: []\n---\n# Titel\n",
"query_id": "manual_mode_v2"
}
render_draft_editor(mock_msg)
# --- MAIN ---
mode, top_k, explain = render_sidebar()
if mode == "💬 Chat":
render_chat_interface(top_k, explain)

View File

@ -13,6 +13,8 @@ from .routers.tools import router as tools_router
from .routers.feedback import router as feedback_router
# NEU: Chat Router (WP-05)
from .routers.chat import router as chat_router
# NEU: Ingest Router (WP-11)
from .routers.ingest import router as ingest_router
try:
from .routers.admin import router as admin_router
@ -20,7 +22,7 @@ except Exception:
admin_router = None
def create_app() -> FastAPI:
app = FastAPI(title="mindnet API", version="0.5.0") # Version bump WP-05
app = FastAPI(title="mindnet API", version="0.6.0") # Version bump WP-11
s = get_settings()
@app.get("/healthz")
@ -38,6 +40,9 @@ def create_app() -> FastAPI:
# NEU: Chat Endpoint
app.include_router(chat_router, prefix="/chat", tags=["chat"])
# NEU: Ingest Endpoint
app.include_router(ingest_router, prefix="/ingest", tags=["ingest"])
if admin_router:
app.include_router(admin_router, prefix="/admin", tags=["admin"])

89
app/routers/ingest.py Normal file
View File

@ -0,0 +1,89 @@
"""
app/routers/ingest.py
API-Endpunkte für WP-11 (Discovery & Persistence).
Delegiert an Services.
"""
import os
import time
import logging
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import Optional, Dict, Any
from app.core.ingestion import IngestionService
from app.services.discovery import DiscoveryService
logger = logging.getLogger(__name__)
router = APIRouter()
# Services Init (Global oder via Dependency Injection)
discovery_service = DiscoveryService()
class AnalyzeRequest(BaseModel):
text: str
type: str = "concept"
class SaveRequest(BaseModel):
markdown_content: str
filename: Optional[str] = None
folder: str = "00_Inbox"
class SaveResponse(BaseModel):
status: str
file_path: str
note_id: str
stats: Dict[str, Any]
@router.post("/analyze")
async def analyze_draft(req: AnalyzeRequest):
"""
WP-11 Intelligence: Liefert Link-Vorschläge via DiscoveryService.
"""
try:
# Hier rufen wir jetzt den verbesserten Service auf
result = await discovery_service.analyze_draft(req.text, req.type)
return result
except Exception as e:
logger.error(f"Analyze failed: {e}", exc_info=True)
return {"suggestions": [], "error": str(e)}
@router.post("/save", response_model=SaveResponse)
async def save_note(req: SaveRequest):
"""
WP-11 Persistence: Speichert und indiziert.
"""
try:
vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault")
abs_vault_root = os.path.abspath(vault_root)
if not os.path.exists(abs_vault_root):
try: os.makedirs(abs_vault_root, exist_ok=True)
except: pass
final_filename = req.filename or f"draft_{int(time.time())}.md"
ingest_service = IngestionService()
# Async Call
result = await ingest_service.create_from_text(
markdown_content=req.markdown_content,
filename=final_filename,
vault_root=abs_vault_root,
folder=req.folder
)
if result.get("status") == "error":
raise HTTPException(status_code=500, detail=result.get("error"))
return SaveResponse(
status="success",
file_path=result.get("path", "unknown"),
note_id=result.get("note_id", "unknown"),
stats={
"chunks": result.get("chunks_count", 0),
"edges": result.get("edges_count", 0)
}
)
except HTTPException as he: raise he
except Exception as e:
logger.error(f"Save failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Save failed: {str(e)}")

255
app/services/discovery.py Normal file
View File

@ -0,0 +1,255 @@
"""
app/services/discovery.py
Service für Link-Vorschläge und Knowledge-Discovery (WP-11).
Features:
- Sliding Window Analyse für lange Texte.
- Footer-Scan für Projekt-Referenzen.
- 'Matrix-Logic' für intelligente Kanten-Typen (Experience -> Value = based_on).
- Async & Nomic-Embeddings kompatibel.
"""
import logging
import asyncio
import os
from typing import List, Dict, Any, Optional, Set
import yaml
from app.core.qdrant import QdrantConfig, get_client
from app.models.dto import QueryRequest
from app.core.retriever import hybrid_retrieve
logger = logging.getLogger(__name__)
class DiscoveryService:
def __init__(self, collection_prefix: str = None):
self.cfg = QdrantConfig.from_env()
self.prefix = collection_prefix or self.cfg.prefix or "mindnet"
self.client = get_client(self.cfg)
self.registry = self._load_type_registry()
async def analyze_draft(self, text: str, current_type: str) -> Dict[str, Any]:
"""
Analysiert den Text und liefert Vorschläge mit kontext-sensitiven Kanten-Typen.
"""
suggestions = []
# Fallback, falls keine spezielle Regel greift
default_edge_type = self._get_default_edge_type(current_type)
# Tracking-Sets für Deduplizierung (Wir merken uns NOTE-IDs)
seen_target_note_ids = set()
# ---------------------------------------------------------
# 1. Exact Match: Titel/Aliases
# ---------------------------------------------------------
# Holt Titel, Aliases UND Typen aus dem Index
known_entities = self._fetch_all_titles_and_aliases()
found_entities = self._find_entities_in_text(text, known_entities)
for entity in found_entities:
if entity["id"] in seen_target_note_ids:
continue
seen_target_note_ids.add(entity["id"])
# INTELLIGENTE KANTEN-LOGIK (MATRIX)
target_type = entity.get("type", "concept")
smart_edge = self._resolve_edge_type(current_type, target_type)
suggestions.append({
"type": "exact_match",
"text_found": entity["match"],
"target_title": entity["title"],
"target_id": entity["id"],
"suggested_edge_type": smart_edge,
"suggested_markdown": f"[[rel:{smart_edge} {entity['title']}]]",
"confidence": 1.0,
"reason": f"Exakter Treffer: '{entity['match']}' ({target_type})"
})
# ---------------------------------------------------------
# 2. Semantic Match: Sliding Window & Footer Focus
# ---------------------------------------------------------
search_queries = self._generate_search_queries(text)
# Async parallel abfragen
tasks = [self._get_semantic_suggestions_async(q) for q in search_queries]
results_list = await asyncio.gather(*tasks)
# Ergebnisse verarbeiten
for hits in results_list:
for hit in hits:
note_id = hit.payload.get("note_id")
if not note_id: continue
# Deduplizierung (Notiz-Ebene)
if note_id in seen_target_note_ids:
continue
# Score Check (Threshold 0.50 für nomic-embed-text)
if hit.total_score > 0.50:
seen_target_note_ids.add(note_id)
target_title = hit.payload.get("title") or "Unbekannt"
# INTELLIGENTE KANTEN-LOGIK (MATRIX)
# Den Typ der gefundenen Notiz aus dem Payload lesen
target_type = hit.payload.get("type", "concept")
smart_edge = self._resolve_edge_type(current_type, target_type)
suggestions.append({
"type": "semantic_match",
"text_found": (hit.source.get("text") or "")[:60] + "...",
"target_title": target_title,
"target_id": note_id,
"suggested_edge_type": smart_edge,
"suggested_markdown": f"[[rel:{smart_edge} {target_title}]]",
"confidence": round(hit.total_score, 2),
"reason": f"Semantisch ähnlich zu {target_type} ({hit.total_score:.2f})"
})
# Sortieren nach Confidence
suggestions.sort(key=lambda x: x["confidence"], reverse=True)
return {
"draft_length": len(text),
"analyzed_windows": len(search_queries),
"suggestions_count": len(suggestions),
"suggestions": suggestions[:10]
}
# ---------------------------------------------------------
# Core Logic: Die Matrix
# ---------------------------------------------------------
def _resolve_edge_type(self, source_type: str, target_type: str) -> str:
"""
Entscheidungsmatrix für komplexe Verbindungen.
Definiert, wie Typ A auf Typ B verlinken sollte.
"""
st = source_type.lower()
tt = target_type.lower()
# Regeln für 'experience' (Erfahrungen)
if st == "experience":
if tt == "value": return "based_on"
if tt == "principle": return "derived_from"
if tt == "trip": return "part_of"
if tt == "lesson": return "learned"
if tt == "project": return "related_to" # oder belongs_to
# Regeln für 'project'
if st == "project":
if tt == "decision": return "depends_on"
if tt == "concept": return "uses"
if tt == "person": return "managed_by"
# Regeln für 'decision' (ADR)
if st == "decision":
if tt == "principle": return "compliant_with"
if tt == "requirement": return "addresses"
# Fallback: Standard aus der types.yaml für den Source-Typ
return self._get_default_edge_type(st)
# ---------------------------------------------------------
# Sliding Windows
# ---------------------------------------------------------
def _generate_search_queries(self, text: str) -> List[str]:
"""
Erzeugt intelligente Fenster + Footer Scan.
"""
text_len = len(text)
if not text: return []
queries = []
# 1. Start / Gesamtkontext
queries.append(text[:600])
# 2. Footer-Scan (Wichtig für "Projekt"-Referenzen am Ende)
if text_len > 150:
footer = text[-250:]
if footer not in queries:
queries.append(footer)
# 3. Sliding Window für lange Texte
if text_len > 800:
window_size = 500
step = 1500
for i in range(window_size, text_len - window_size, step):
end_pos = min(i + window_size, text_len)
chunk = text[i:end_pos]
if len(chunk) > 100:
queries.append(chunk)
return queries
# ---------------------------------------------------------
# Standard Helpers
# ---------------------------------------------------------
async def _get_semantic_suggestions_async(self, text: str):
req = QueryRequest(query=text, top_k=5, explain=False)
try:
res = hybrid_retrieve(req)
return res.results
except Exception as e:
logger.error(f"Semantic suggestion error: {e}")
return []
def _load_type_registry(self) -> dict:
path = 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 {}
try:
with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {}
except Exception: return {}
def _get_default_edge_type(self, note_type: str) -> str:
types_cfg = self.registry.get("types", {})
type_def = types_cfg.get(note_type, {})
defaults = type_def.get("edge_defaults")
return defaults[0] if defaults else "related_to"
def _fetch_all_titles_and_aliases(self) -> List[Dict]:
notes = []
next_page = None
col = f"{self.prefix}_notes"
try:
while True:
res, next_page = self.client.scroll(
collection_name=col, limit=1000, offset=next_page,
with_payload=True, with_vectors=False
)
for point in res:
pl = point.payload or {}
aliases = pl.get("aliases") or []
if isinstance(aliases, str): aliases = [aliases]
notes.append({
"id": pl.get("note_id"),
"title": pl.get("title"),
"aliases": aliases,
"type": pl.get("type", "concept") # WICHTIG: Typ laden für Matrix
})
if next_page is None: break
except Exception: pass
return notes
def _find_entities_in_text(self, text: str, entities: List[Dict]) -> List[Dict]:
found = []
text_lower = text.lower()
for entity in entities:
# Title Check
title = entity.get("title")
if title and title.lower() in text_lower:
found.append({"match": title, "title": title, "id": entity["id"], "type": entity["type"]})
continue
# Alias Check
for alias in entity.get("aliases", []):
if str(alias).lower() in text_lower:
found.append({"match": alias, "title": title, "id": entity["id"], "type": entity["type"]})
break
return found

View File

@ -1,46 +1,90 @@
"""
app/services/embeddings_client.py TextEmbedding (WP-04)
app/services/embeddings_client.py TextEmbedding Service
Zweck:
Liefert 384-d Embeddings für Textqueries (lazy load, einmal pro Prozess).
Standard: Sentence-Transformers (MODEL_NAME aus app.config.Settings).
Hinweis: Kein Netz-Zugriff; nutzt lokal installierte Modelle.
Kompatibilität:
Python 3.12+, sentence-transformers 5.x
Version:
0.1.0 (Erstanlage)
Stand:
2025-10-07
Bezug:
- app/core/retriever.py (nutzt embed_text_if_needed)
- app/config.py (MODEL_NAME, VECTOR_SIZE)
Nutzung:
from app.services.embeddings_client import embed_text
Änderungsverlauf:
0.1.0 (2025-10-07) Erstanlage.
"""
Einheitlicher Client für Embeddings via Ollama (Nomic).
Stellt sicher, dass sowohl Async (Ingestion) als auch Sync (Retriever)
denselben Vektorraum (768 Dim) nutzen.
Version: 2.5.0 (Unified Ollama)
"""
from __future__ import annotations
import os
import logging
import httpx
import requests # Für den synchronen Fallback
from typing import List
from functools import lru_cache
from app.config import get_settings
# Lazy import, damit Testläufe ohne Modell-Laden schnell sind
def _load_model():
from sentence_transformers import SentenceTransformer # import hier, nicht top-level
s = get_settings()
return SentenceTransformer(s.MODEL_NAME, device="cpu")
logger = logging.getLogger(__name__)
@lru_cache(maxsize=1)
def _cached_model():
return _load_model()
class EmbeddingsClient:
"""
Async Client für Embeddings via Ollama.
"""
def __init__(self):
self.settings = get_settings()
self.base_url = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434")
self.model = os.getenv("MINDNET_EMBEDDING_MODEL")
if not self.model:
self.model = os.getenv("MINDNET_LLM_MODEL", "phi3:mini")
logger.warning(f"No MINDNET_EMBEDDING_MODEL set. Fallback to '{self.model}'.")
async def embed_query(self, text: str) -> List[float]:
return await self._request_embedding(text)
async def embed_documents(self, texts: List[str]) -> List[List[float]]:
vectors = []
# Längeres Timeout für Batches
async with httpx.AsyncClient(timeout=120.0) as client:
for text in texts:
vec = await self._request_embedding_with_client(client, text)
vectors.append(vec)
return vectors
async def _request_embedding(self, text: str) -> List[float]:
async with httpx.AsyncClient(timeout=30.0) as client:
return await self._request_embedding_with_client(client, text)
async def _request_embedding_with_client(self, client: httpx.AsyncClient, text: str) -> List[float]:
if not text or not text.strip(): return []
url = f"{self.base_url}/api/embeddings"
try:
response = await client.post(url, json={"model": self.model, "prompt": text})
response.raise_for_status()
return response.json().get("embedding", [])
except Exception as e:
logger.error(f"Async embedding failed: {e}")
return []
# ==============================================================================
# TEIL 2: SYNCHRONER FALLBACK (Unified)
# ==============================================================================
def embed_text(text: str) -> List[float]:
"""
Erzeugt einen 384-d Vektor (oder laut Settings.VECTOR_SIZE) für den gegebenen Text.
LEGACY/SYNC: Nutzt jetzt ebenfalls OLLAMA via 'requests'.
Ersetzt SentenceTransformers, um Dimensionskonflikte (768 vs 384) zu lösen.
"""
if not text or not text.strip():
raise ValueError("embed_text: leerer Text")
model = _cached_model()
vec = model.encode([text], normalize_embeddings=True)[0]
return vec.astype(float).tolist()
return []
base_url = os.getenv("MINDNET_OLLAMA_URL", "http://127.0.0.1:11434")
model = os.getenv("MINDNET_EMBEDDING_MODEL")
# Fallback logik identisch zur Klasse
if not model:
model = os.getenv("MINDNET_LLM_MODEL", "phi3:mini")
url = f"{base_url}/api/embeddings"
try:
# Synchroner Request (blockierend)
response = requests.post(url, json={"model": model, "prompt": text}, timeout=30)
response.raise_for_status()
data = response.json()
return data.get("embedding", [])
except Exception as e:
logger.error(f"Sync embedding (Ollama) failed: {e}")
return []

View File

@ -1,7 +1,7 @@
# mindnet v2.4 Knowledge Design Manual
**Datei:** `docs/mindnet_knowledge_design_manual_v2.4.md`
**Stand:** 2025-12-10
**Status:** **FINAL** (Integrierter Stand WP01WP10a)
**Stand:** 2025-12-11
**Status:** **FINAL** (Integrierter Stand WP01WP11)
**Quellen:** `knowledge_design.md`, `TYPE_REGISTRY_MANUAL.md`, `chunking_strategy.md`, `mindnet_functional_architecture.md`.
---
@ -24,11 +24,11 @@ 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.3.1 verfügt Mindnet über:
Seit Version 2.4 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).
* **Web UI (WP10):** Du kannst direkt sehen, welche Quellen genutzt wurden.
* **Interview Modus (WP07):** Du kannst Notizen direkt im Chat entwerfen lassen.
* **Active Intelligence (WP11):** Das System hilft dir beim Schreiben und Vernetzen (Link-Vorschläge).
### 1.2 Der Vault als „Source of Truth“
Die Markdown-Dateien in deinem Vault sind die **einzige Quelle der Wahrheit**.
@ -59,7 +59,7 @@ Jede Datei muss mindestens folgende Felder enthalten, um korrekt verarbeitet zu
Diese Felder sind technisch nicht zwingend, aber für bestimmte Typen sinnvoll:
lang: de # Sprache (Default: de)
aliases: [Alpha Projekt, Project A] # Synonyme für die Suche
aliases: [Alpha Projekt, Project A] # Synonyme (WICHTIG für Exact Match in Intelligence)
visibility: internal # internal (default), public, private
> **Hinweis:** Felder wie `retriever_weight` oder `chunk_profile` sollten **nicht** mehr manuell im Frontmatter gesetzt werden. Diese werden zentral über den `type` gesteuert (siehe Kap. 3), um die Wartbarkeit zu sichern.
@ -86,17 +86,17 @@ Der `type` ist der wichtigste Hebel im Knowledge Design. Er steuert nicht nur da
Mindnet unterscheidet verschiedene Wissensarten. Wähle den Typ, der die **Rolle** der Notiz am besten beschreibt:
| Typ | Beschreibung & Einsatzzweck | Rolle im Chat (Intent) | Interview Schema (WP07) |
| :--- | :--- | :--- | :--- |
| **`concept`** | Fachbegriffe, Theorien. Zeitloses Wissen. | **FACT** | Titel, Definition, Tags |
| **`project`** | Ein Vorhaben mit Ziel, Dauer und Aufgaben. | **FACT / DECISION** | Ziel, Status, Stakeholder, Steps |
| **`experience`** | Persönliche Erfahrung, Lektion oder Erkenntnis. | **EMPATHY** | Situation, Erkenntnis, Emotionen |
| **`decision`** | Eine bewusst getroffene Entscheidung (ADR). | **DECISION** | Kontext, Entscheidung, Alternativen |
| **`value`** | Ein persönlicher Wert oder ein Prinzip. | **DECISION** | Definition, Anti-Beispiel |
| **`goal`** | Ein strategisches Ziel (kurz- oder langfristig). | **DECISION** | Zeitrahmen, KPIs, Werte |
| **`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 |
| Typ | Beschreibung & Einsatzzweck | Rolle im Chat (Intent) | Interview Schema (WP07) | Matrix-Logik (WP11) |
| :--- | :--- | :--- | :--- | :--- |
| **`concept`** | Fachbegriffe, Theorien. Zeitloses Wissen. | **FACT** | Titel, Definition, Tags | Ziel für `uses` |
| **`project`** | Ein Vorhaben mit Ziel, Dauer und Aufgaben. | **FACT / DECISION** | Ziel, Status, Stakeholder | Quelle für `uses`, `depends_on` |
| **`experience`** | Persönliche Erfahrung, Lektion oder Erkenntnis. | **EMPATHY** | Situation, Erkenntnis, Emotionen | Quelle für `based_on` |
| **`decision`** | Eine bewusst getroffene Entscheidung (ADR). | **DECISION** | Kontext, Entscheidung, Alternativen | Quelle für `depends_on` |
| **`value`** | Ein persönlicher Wert oder ein Prinzip. | **DECISION** | Definition, Anti-Beispiel | Ziel für `based_on` |
| **`goal`** | Ein strategisches Ziel (kurz- oder langfristig). | **DECISION** | Zeitrahmen, KPIs, Werte | Ziel für `related_to` |
| **`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 | - |
### 3.2 Zusammenspiel mit `types.yaml`
@ -139,6 +139,7 @@ Dies ist die **mächtigste** Methode. Du sagst dem System explizit, **wie** Ding
* `related_to`: Hat zu tun mit (allgemein).
* `caused_by`: Wurde verursacht durch.
* `solves`: Löst (Problem).
* **Neu (v2.4):** `based_on`, `uses`, `derived_from` (werden oft automatisch vorgeschlagen).
### 4.3 Callout-Edges (Kuratierte Listen)
Für Zusammenfassungen oder "Siehe auch"-Blöcke am Ende einer Notiz.

View File

@ -1,7 +1,7 @@
# Mindnet v2.4 Overview & Einstieg
**Datei:** `docs/mindnet_overview_v2.4.md`
**Stand:** 2025-12-10
**Status:** **FINAL** (Inkl. Interview-Assistent & Web-Editor)
**Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. Async Intelligence & Editor)
**Version:** 2.4.0
---
@ -13,6 +13,7 @@
Anders als herkömmliche Notiz-Apps (wie Obsidian oder Evernote), die Texte nur passiv speichern, ist Mindnet ein **aktives System**:
* Es **versteht** Zusammenhänge über einen Wissensgraphen.
* Es **begründet** Antworten ("Warum ist das so?").
* Es **unterstützt** beim Schreiben: Es schlägt automatisch Verbindungen zu bestehendem Wissen vor ("Active Intelligence").
* Es **antwortet** situativ angepasst: Mal als Strategieberater, mal als empathischer Spiegel, und neu: **als Interviewer, der hilft, Wissen zu erfassen.**
### Die Vision
@ -27,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:** Import-Pipeline, Chunking, Vektor-Datenbank (Qdrant).
* **Status:** 🟢 Live (WP01WP03).
* **Technik:** Async Import-Pipeline, Chunking, Vektor-Datenbank (Qdrant).
* **Status:** 🟢 Live (WP01WP03, WP11).
### 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 + Vektor), Explanation Engine.
* **Status:** 🟢 Live (WP04).
* **Technik:** Hybrider Retriever (Graph + Nomic Embeddings), Explanation Engine.
* **Status:** 🟢 Live (WP04, WP11).
### Ebene 3: Identität & Interaktion (Die Persönlichkeit)
* **Funktion:** Interaktion, Bewertung und Co-Creation.
@ -45,6 +46,7 @@ Mindnet arbeitet auf drei Schichten, die aufeinander aufbauen:
* **Intent Router:** Erkennt Absichten (Fakt vs. Gefühl vs. Entscheidung vs. Interview).
* **Strategic Retrieval:** Lädt gezielt Werte oder Erfahrungen nach.
* **One-Shot Extraction:** Generiert Entwürfe für neue Notizen.
* **Active Intelligence:** Schlägt Links während des Schreibens vor.
* **Status:** 🟢 Live (WP05WP07, WP10).
---
@ -54,18 +56,19 @@ Mindnet arbeitet auf drei Schichten, die aufeinander aufbauen:
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. **Ingest:** Ein Python-Skript importiert, zerlegt (Chunking) und vernetzt (Edges) die Daten in Qdrant.
3. **Intent Recognition:** Der Router analysiert deine Frage: Willst du Fakten, Code, Empathie oder etwas dokumentieren?
4. **Retrieval / Action:**
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?
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).
5. **Generation:** Ein lokales LLM (Ollama) formuliert die Antwort oder den Markdown-Draft.
6. **Feedback:** Du bewertest die Antwort. Das System lernt (langfristig) daraus.
6. **Generation:** Ein lokales LLM (Ollama) formuliert die Antwort oder den Markdown-Draft.
7. **Feedback:** Du bewertest die Antwort. Das System lernt (langfristig) daraus.
**Tech-Stack:**
* **Backend:** Python 3.10+, FastAPI.
* **Datenbank:** Qdrant (Vektor & Graph).
* **KI:** Ollama (Phi-3 Mini) 100% lokal.
* **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).
---
@ -96,5 +99,5 @@ Wo findest du was?
## 6. Aktueller Fokus
Wir haben den **Interview-Assistenten (WP07)** und den **Draft-Editor (WP10a)** erfolgreich integriert.
Das System kann nun aktiv helfen, Wissen zu strukturieren, anstatt es nur abzurufen. Der Fokus verschiebt sich nun in Richtung **Self-Tuning (WP08)**, um aus dem gesammelten Feedback automatisch zu lernen.
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.

View File

@ -1,7 +1,7 @@
# Mindnet v2.4 Admin Guide
**Datei:** `docs/mindnet_admin_guide_v2.4.md`
**Stand:** 2025-12-10
**Status:** **FINAL** (Inkl. Frontend Deployment & Interview Config)
**Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. Async Architecture & Nomic Model)
**Quellen:** `Handbuch.md`, `mindnet_developer_guide_v2.4.md`.
> Dieses Handbuch richtet sich an **Administratoren**. Es beschreibt Installation, Konfiguration, Backup-Strategien, Monitoring und den sicheren Betrieb der Mindnet-Instanz (API + UI + DB).
@ -23,7 +23,7 @@ Wir unterscheiden strikt zwischen:
* **OS:** Linux (Ubuntu 22.04+ empfohlen) oder macOS.
* **Runtime:** Python 3.10+, Docker (für Qdrant), Ollama (für LLM).
* **Hardware:**
* CPU: 4+ Cores empfohlen (für Import & Inference).
* CPU: 4+ Cores empfohlen (für Async Import & Inference).
* RAM: Min. 8GB empfohlen (4GB System + 4GB für Phi-3/Qdrant).
* Disk: SSD empfohlen für Qdrant-Performance.
@ -37,11 +37,11 @@ Wir unterscheiden strikt zwischen:
python3 -m venv .venv
source .venv/bin/activate
# 3. Dependencies installieren (inkl. Streamlit)
# 3. Dependencies installieren (inkl. Streamlit, HTTPX)
pip install -r requirements.txt
# 4. Verzeichnisse anlegen
mkdir -p logs qdrant_storage data/logs
mkdir -p logs qdrant_storage data/logs vault
### 2.3 Qdrant Setup (Docker)
Wir nutzen Qdrant als Vektor-Datenbank. Persistenz ist wichtig.
@ -53,20 +53,25 @@ Wir nutzen Qdrant als Vektor-Datenbank. Persistenz ist wichtig.
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrant
### 2.4 Ollama Setup (LLM Service)
Mindnet benötigt einen lokalen LLM-Server für den Chat.
### 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.
# 1. Installieren (Linux Script)
curl -fsSL [https://ollama.com/install.sh](https://ollama.com/install.sh) | sh
curl -fsSL https://ollama.com/install.sh | sh
# 2. Modell laden (Phi-3 Mini für CPU-Performance)
ollama pull phi3:mini
# 2. Modelle laden
ollama pull phi3:mini # Für Chat/Reasoning
ollama pull nomic-embed-text # Für Vektoren (768 Dim)
# 3. Testen
curl http://localhost:11434/api/generate -d '{"model": "phi3:mini", "prompt":"Hi"}'
### 2.5 Konfiguration (ENV)
Erstelle eine `.env` Datei im Root-Verzeichnis. Die neuen Settings für WP-06/WP-07 (Timeout, Decision Config) sind essenziell für stabilen Betrieb auf CPUs.
Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM` und `MINDNET_EMBEDDING_MODEL`.
# Server Config
UVICORN_HOST=0.0.0.0
# Qdrant Verbindung
QDRANT_URL="http://localhost:6333"
@ -74,15 +79,23 @@ Erstelle eine `.env` Datei im Root-Verzeichnis. Die neuen Settings für WP-06/WP
# Mindnet Core Settings
COLLECTION_PREFIX="mindnet"
MINDNET_TYPES_FILE="./config/types.yaml"
MINDNET_VAULT_ROOT="./vault"
# LLM / RAG Settings
MINDNET_LLM_MODEL="phi3:mini"
# WICHTIG: Dimension auf 768 setzen (für Nomic)
VECTOR_DIM=768
# AI Modelle (Ollama)
MINDNET_OLLAMA_URL="http://127.0.0.1:11434"
MINDNET_LLM_MODEL="phi3:mini"
MINDNET_EMBEDDING_MODEL="nomic-embed-text" # NEU
# Config & Timeouts
# Timeouts (Erhöht für Async/Nomic)
MINDNET_LLM_TIMEOUT=300.0
MINDNET_API_TIMEOUT=60.0
# Configs
MINDNET_PROMPTS_PATH="./config/prompts.yaml"
MINDNET_DECISION_CONFIG="./config/decision_engine.yaml"
MINDNET_LLM_TIMEOUT=300.0
### 2.6 Deployment via Systemd (Backend & Frontend)
@ -98,6 +111,7 @@ Mindnet benötigt zwei Services pro Umgebung: API (Uvicorn) und UI (Streamlit).
User=llmadmin
Group=llmadmin
WorkingDirectory=/home/llmadmin/mindnet
# Async Server Start
ExecStart=/home/llmadmin/mindnet/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8001 --env-file .env
Restart=always
RestartSec=5
@ -141,11 +155,11 @@ 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.
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.
**Cronjob-Beispiel (stündlich):**
0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault /path/to/vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1
0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1
### 3.2 Health-Checks
Prüfe regelmäßig, ob alle Komponenten laufen.
@ -157,34 +171,28 @@ Prüfe regelmäßig, ob alle Komponenten laufen.
### 3.3 Logs & Monitoring
* **Backend Fehler:** `journalctl -u mindnet-prod -f`
* **Frontend Fehler:** `journalctl -u mindnet-ui-prod -f`
* Achte auf "Timeout"-Meldungen im Frontend, wenn das Backend zu langsam antwortet.
* **LLM Fehler:** `journalctl -u ollama -f`
* **Fachliche Logs:** `data/logs/search_history.jsonl`
---
## 4. Update-Prozess
## 4. Troubleshooting (Update v2.4)
Wenn neue Versionen ausgerollt werden (Deployment):
### "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.
* **Lösung:** Full Reset erforderlich.
1. `python -m scripts.reset_qdrant --mode wipe --prefix mindnet --yes` (Löscht DB).
2. `python -m scripts.import_markdown ...` (Baut neu auf).
1. **Code aktualisieren:**
### "500 Internal Server Error" beim Speichern
* **Ursache:** Oft Timeout bei Ollama, wenn `nomic-embed-text` noch nicht im RAM geladen ist ("Cold Start").
* **Lösung:**
1. Sicherstellen, dass Modell existiert: `ollama list`.
2. API neustarten (re-initialisiert Async Clients).
cd /home/llmadmin/mindnet
git pull origin main
2. **Dependencies prüfen:**
source .venv/bin/activate
pip install -r requirements.txt
3. **Dienste neustarten (Zwingend!):**
sudo systemctl restart mindnet-prod
sudo systemctl restart mindnet-ui-prod
4. **Schema-Migration (falls nötig):**
python3 -m scripts.import_markdown ... --apply
### "NameError: name 'os' is not defined"
* **Ursache:** Fehlender Import in Skripten nach Updates.
* **Lösung:** `git pull` (Fix wurde in v2.3.10 deployed).
---
@ -202,16 +210,13 @@ Für schnelle Wiederherstellung des Suchindex.
tar -czf qdrant_backup_$(date +%F).tar.gz ./qdrant_storage
docker start mindnet_qdrant
### 5.3 Log-Daten (Priorität 3)
Sichere den Ordner `data/logs/`. Verlust dieser Daten bedeutet Verlust des Trainingsmaterials für Self-Tuning.
### 5.4 Notfall-Wiederherstellung (Rebuild)
Wenn die Datenbank korrupt ist:
### 5.3 Notfall-Wiederherstellung (Rebuild)
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
python3 -m scripts.import_markdown --vault /path/to/vault --prefix "mindnet" --apply
python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
---
@ -222,6 +227,3 @@ Mindnet hat aktuell **keine integrierte Authentifizierung**.
* **Frontend:** Streamlit auf Port 8501 ist offen. Nutze Nginx Basic Auth oder VPN.
* **API:** Sollte nicht direkt im öffentlichen Netz stehen.
* **Qdrant:** Auf `127.0.0.1` beschränken.
### 6.2 Typen-Governance
Änderungen an der `types.yaml` (z.B. neue Gewichte) wirken global und erfordern Tests.

View File

@ -1,7 +1,7 @@
# Mindnet v2.4 Appendices & Referenzen
**Datei:** `docs/mindnet_appendices_v2.4.md`
**Stand:** 2025-12-10
**Status:** **FINAL** (Integrierter Stand WP01WP10a)
**Stand:** 2025-12-11
**Status:** **FINAL** (Integrierter Stand WP01WP11)
**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.
@ -43,7 +43,9 @@ 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". |
| `derived_from` | Default (Exp) | Nein | Herkunft. "Erkenntnis stammt aus Quelle X". |
| `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". |
---
@ -104,28 +106,35 @@ Diese Variablen steuern das Verhalten der Skripte und Container.
| `QDRANT_URL` | `http://localhost:6333` | URL zur Vektor-DB. |
| `QDRANT_API_KEY` | *(leer)* | API-Key für Absicherung (optional). |
| `COLLECTION_PREFIX` | `mindnet` | Namensraum für Collections (`{prefix}_notes` etc). |
| `VECTOR_DIM` | `768` | **NEU:** Dimension für Embeddings (für Nomic). |
| `MINDNET_TYPES_FILE` | `config/types.yaml` | Pfad zur Typ-Registry. |
| `MINDNET_RETRIEVER_CONFIG`| `config/retriever.yaml`| Pfad zur Scoring-Konfiguration. |
| `MINDNET_PROMPTS_PATH` | `config/prompts.yaml` | Pfad zu LLM-Prompts (Neu in v2.2). |
| `MINDNET_DECISION_CONFIG` | `config/decision_engine.yaml` | Router & Interview Config (Neu in v2.3). |
| `MINDNET_LLM_MODEL` | `phi3:mini` | Name des Ollama-Modells (Neu in v2.2). |
| `MINDNET_LLM_MODEL` | `phi3:mini` | Name des Chat-Modells. |
| `MINDNET_EMBEDDING_MODEL` | `nomic-embed-text` | **NEU:** Name des Vektor-Modells. |
| `MINDNET_OLLAMA_URL` | `http://127.0.0.1:11434`| URL zum LLM-Server (Neu in v2.2). |
| `MINDNET_LLM_TIMEOUT` | `300.0` | Timeout für Ollama (Erhöht für CPU-Inference). |
| `MINDNET_API_TIMEOUT` | `300.0` | Frontend Timeout (Streamlit). |
| `MINDNET_API_TIMEOUT` | `60.0` | **NEU:** Frontend Timeout (Streamlit). |
| `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`). |
| `VECTOR_DIM` | `384` | Dimension der Embeddings (Modellabhängig). |
---
## Anhang E: Glossar
* **Active Intelligence:** Feature, das während des Schreibens Links vorschlägt.
* **Async Ingestion:** Non-blocking Import-Prozess zur Vermeidung von Timeouts.
* **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.
* **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.
---
@ -147,3 +156,4 @@ Aktueller Implementierungsstand der Module.
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
| **WP11** | Backend Intelligence | 🟢 Live | **Async Core, Nomic, Matrix.** |

View File

@ -1,6 +1,6 @@
# Mindnet v2.4 Entwickler-Workflow
**Datei:** `docs/DEV_WORKFLOW.md`
**Stand:** 2025-12-10 (Aktualisiert: Inkl. Interview-Tests WP07)
**Stand:** 2025-12-11 (Aktualisiert: Inkl. Async Intelligence & Nomic)
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/wp07-interview`).
* Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp11-async-fix`).
* 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`).
* Nimm deine Änderungen vor (z.B. neue Schemas in `decision_engine.yaml` oder Async-Logik in `ingestion.py`).
5. **Sichern & Hochladen:**
* **Source Control** Icon (Gabel-Symbol) -> Nachricht eingeben -> **Commit**.
@ -64,14 +64,16 @@ 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/wp07-interview
git checkout feature/wp11-async-fix
git pull
```
4. **Umgebung vorbereiten (bei Bedarf):**
4. **Umgebung vorbereiten (WICHTIG für v2.4):**
```bash
source .venv/bin/activate
pip install -r requirements.txt # Nur nötig bei neuen Paketen
pip install -r requirements.txt # HTTPX usw.
# Sicherstellen, dass das neue Embedding-Modell da ist:
ollama pull nomic-embed-text
```
5. **Test-Server aktualisieren (WICHTIG):**
@ -87,8 +89,6 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft.
# Logs prüfen (um Fehler zu sehen):
journalctl -u mindnet-dev -f
# Oder Frontend Logs:
journalctl -u mindnet-ui-dev -f
```
**Option B: Manuell Debuggen (Direct Output)**
@ -100,13 +100,6 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft.
# 2. Manuell starten (z.B. API)
uvicorn app.main:app --host 0.0.0.0 --port 8002 --env-file .env
# ... Testen ...
# 3. Wenn fertig: Services wieder anschalten (Optional)
# Strg+C drücken
sudo systemctl start mindnet-dev
sudo systemctl start mindnet-ui-dev
```
6. **Validieren (Smoke Tests):**
@ -114,16 +107,15 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft.
* **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: Decision Engine**
**Test A: Intelligence / Aliases (Neu in WP11)**
```bash
python tests/test_wp06_decision.py -p 8002 -q "Soll ich Qdrant nutzen?"
# Erwartung: Intent DECISION
python debug_analysis.py
# Erwartung: "✅ ALIAS GEFUNDEN"
```
**Test B: Interview Modus (Neu!)**
**Test B: API Check**
```bash
python tests/test_wp06_decision.py -p 8002 -q "Ich will ein neues Projekt starten"
# Erwartung: Intent INTERVIEW, Output ist Markdown Codeblock
curl -X POST "http://localhost:8002/ingest/analyze" -d '{"text": "mindnet", "type": "journal"}'
```
---
@ -150,16 +142,18 @@ Jetzt bringen wir die Änderung in das Live-System (Port 8001 / 8501).
cd /home/llmadmin/mindnet
git pull origin main
# Dependencies updaten
# Dependencies updaten & Modelle checken
source .venv/bin/activate
pip install -r requirements.txt
ollama pull nomic-embed-text
# 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
# Produktions-Services neustarten
sudo systemctl restart mindnet-prod
sudo systemctl restart mindnet-ui-prod
# Kurz prüfen, ob er läuft
sudo systemctl status mindnet-prod
```
---
@ -174,7 +168,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/wp07-interview
git branch -d feature/wp11-async-fix
```
3. **VS Code:**
* Auf `main` wechseln.
@ -187,27 +181,25 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch.
| Wo? | Befehl | Was tut es? |
| :--- | :--- | :--- |
| **VS Code** | `Sync (auf main)` | **WICHTIG:** Holt neuesten Code vom Server. |
| **Beelink** | `git fetch` | Aktualisiert Liste der Remote-Branches. |
| **Beelink** | `sudo systemctl restart mindnet-dev` | **Neustart Dev-Backend (Port 8002).** |
| **Beelink** | `sudo systemctl restart mindnet-ui-dev` | **Neustart Dev-Frontend (Port 8502).** |
| **Beelink** | `journalctl -u mindnet-dev -f` | **Live-Logs Backend.** |
| **Beelink** | `journalctl -u mindnet-ui-dev -f` | **Live-Logs Frontend.** |
| **Beelink** | `python debug_analysis.py` | **Prüft Aliases & Scores.** |
| **Beelink** | `python -m scripts.reset_qdrant ...` | **Löscht & Repariert DB.** |
---
## 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 (300s)" / 500 Error beim Interview**
* **Ursache:** Das LLM (Ollama) braucht für den One-Shot Draft länger als das Timeout erlaubt.
* **Lösung:**
1. Erhöhe in `.env` den Wert: `MINDNET_LLM_TIMEOUT=300.0`.
2. Starte die Server neu.
**"Port 8002 / 8502 already in use"**
* **Ursache:** Du willst `uvicorn` oder `streamlit` manuell starten, aber der Service läuft noch.
* **Lösung:** `sudo systemctl stop mindnet-dev` bzw. `mindnet-ui-dev`.
**"UnicodeDecodeError in .env"**
* **Ursache:** Umlaute oder Sonderzeichen in der `.env` Datei.
* **Lösung:** `.env` bereinigen (nur ASCII nutzen) und sicherstellen, dass sie UTF-8 ohne BOM ist.

View File

@ -1,7 +1,7 @@
# Mindnet v2.4 Developer Guide
**Datei:** `docs/mindnet_developer_guide_v2.4.md`
**Stand:** 2025-12-10
**Status:** **FINAL** (Inkl. RAG, Interview Mode & Frontend WP10)
**Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. Async Core, Nomic & Frontend State)
**Quellen:** `mindnet_technical_architecture.md`, `Handbuch.md`, `DEV_WORKFLOW.md`.
> **Zielgruppe:** Entwickler:innen.
@ -20,6 +20,7 @@
- [3.2 Der Hybrid Router (`app.routers.chat`)](#32-der-hybrid-router-approuterschat)
- [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)
- [4. Tests \& Debugging](#4-tests--debugging)
- [4.1 Unit Tests (Pytest)](#41-unit-tests-pytest)
- [4.2 Integration / Pipeline Tests](#42-integration--pipeline-tests)
@ -40,6 +41,7 @@ Der Code ist modular in `app` (Logik), `scripts` (CLI) und `config` (Steuerung)
mindnet/
├── app/
│ ├── core/ # Kernlogik
│ │ ├── ingestion.py # NEU: Async Ingestion Service (WP11)
│ │ ├── chunker.py # Text-Zerlegung
│ │ ├── derive_edges.py # Edge-Erzeugung (WP03 Logik)
│ │ ├── retriever.py # Scoring & Hybrid Search
@ -49,13 +51,15 @@ Der Code ist modular in `app` (Logik), `scripts` (CLI) und `config` (Steuerung)
│ │ └── dto.py # Zentrale DTO-Definition
│ ├── routers/ # FastAPI Endpoints
│ │ ├── query.py # Suche
│ │ ├── ingest.py # NEU: Save/Analyze (WP11)
│ │ ├── chat.py # Hybrid Router & Interview Logic (WP06/WP07)
│ │ ├── feedback.py # Feedback (WP04c)
│ │ └── ...
│ ├── services/ # Interne & Externe Dienste
│ │ ├── llm_service.py # Ollama Client (Mit Timeout & Raw-Mode)
│ │ ├── embeddings_client.py# NEU: Async Embeddings (HTTPX)
│ │ ├── feedback_service.py # Logging (JSONL Writer)
│ │ └── embeddings_client.py
│ │ └── discovery.py # NEU: Intelligence Logic (WP11)
│ ├── frontend/ # NEU (WP10)
│ │ └── ui.py # Streamlit Application inkl. Draft-Editor
│ └── main.py # Entrypoint der API
@ -77,7 +81,7 @@ Der Code ist modular in `app` (Logik), `scripts` (CLI) und `config` (Steuerung)
### 2.1 Voraussetzungen
* **Python:** 3.10 oder höher.
* **Docker:** Für Qdrant.
* **Ollama:** Für lokale LLM-Inference (erforderlich für `/chat`).
* **Ollama:** Für lokale LLM-Inference (erforderlich für `/chat` und Embeddings).
* **Vault:** Ein Ordner mit Markdown-Dateien (z.B. `./mindnet_v2_test_vault` für Tests).
### 2.2 Installation
@ -93,9 +97,11 @@ Der Code ist modular in `app` (Logik), `scripts` (CLI) und `config` (Steuerung)
# 3. Abhängigkeiten installieren (inkl. Streamlit)
pip install -r requirements.txt
# 4. Ollama Setup (Modell laden)
# Wir nutzen Phi-3 Mini für schnelle CPU-Inference
# 4. Ollama Setup (Modelle laden)
# Chat-Modell (Phi-3)
ollama pull phi3:mini
# Embedding-Modell (Nomic) - PFLICHT für v2.4!
ollama pull nomic-embed-text
### 2.3 Konfiguration (Environment)
Erstelle eine `.env` Datei im Root-Verzeichnis.
@ -106,18 +112,21 @@ Erstelle eine `.env` Datei im Root-Verzeichnis.
# Mindnet Core Settings
COLLECTION_PREFIX="mindnet_dev"
VECTOR_DIM=768 # NEU: 768 für Nomic (vorher 384)
MINDNET_TYPES_FILE="./config/types.yaml"
MINDNET_RETRIEVER_CONFIG="./config/retriever.yaml"
MINDNET_VAULT_ROOT="./vault"
# LLM / RAG Settings (WP06/07)
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_PROMPTS_PATH="./config/prompts.yaml"
MINDNET_DECISION_CONFIG="./config/decision_engine.yaml"
# Frontend Settings (WP10)
MINDNET_API_URL="http://localhost:8002"
MINDNET_API_TIMEOUT=60.0
# Import-Strategie
MINDNET_HASH_COMPARE="Body"
@ -144,7 +153,8 @@ 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()`.
* **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.
* **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`.
@ -161,9 +171,15 @@ 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.
* **State:** Nutzt `st.session_state` für Chat-History und Drafts.
* **Logik:** Ruft `/chat` und `/feedback` Endpoints der API auf.
* **Logik:** Ruft `/chat` und `/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.
* Unterstützt dediziertes Embedding-Modell (`nomic-embed-text`) getrennt vom Chat-Modell.
* Enthält Legacy-Funktion `embed_text` für synchrone Skripte.
---
@ -201,6 +217,9 @@ Prüfen das laufende System gegen eine echte Qdrant-Instanz und Ollama.
# 3. Feedback Test
python tests/test_feedback_smoke.py --url http://localhost:8002/query
# 4. Intelligence Test (WP11)
python debug_analysis.py
---
## 5. Das "Teach-the-AI" Paradigma (Context Intelligence)
@ -263,6 +282,7 @@ Der `One-Shot Extractor` (Prompt Template) liest diese Liste dynamisch und weist
**DB komplett zurücksetzen (Vorsicht!):**
# --yes überspringt die Bestätigung
python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet_dev" --yes
**Einen einzelnen File inspizieren (Parser-Sicht):**

View File

@ -1,9 +1,9 @@
# Mindnet v2.4 Fachliche Architektur
**Datei:** `docs/mindnet_functional_architecture_v2.4.md`
**Stand:** 2025-12-10
**Status:** **FINAL** (Integrierter Stand WP01WP10 + WP07)
**Stand:** 2025-12-11
**Status:** **FINAL** (Integrierter Stand WP01WP11: Async Intelligence)
> 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). Die technische Umsetzung wird im technischen Dokument detailliert.
> 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).
---
<details>
@ -19,6 +19,7 @@
- [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)
- [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)
@ -26,12 +27,13 @@
- [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/WP07)](#6-context-intelligence--intent-router-wp06wp07)
- [6) Context Intelligence \& Intent Router (WP06WP11)](#6-context-intelligence--intent-router-wp06wp11)
- [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) Neu in v2.4](#65-der-interview-modus-one-shot-extraction--neu-in-v24)
- [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)
- [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)
@ -45,7 +47,7 @@
- [11) Semantik ausgewählter `kind`-Werte](#11-semantik-ausgewählter-kind-werte)
- [12) Frontmatter-Eigenschaften Rolle \& Empfehlung](#12-frontmatter-eigenschaften--rolle--empfehlung)
- [13) Lösch-/Update-Garantien (Idempotenz)](#13-lösch-update-garantien-idempotenz)
- [14) Beispiel Von Markdown zu Kanten (v2.2)](#14-beispiel--von-markdown-zu-kanten-v22)
- [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)
@ -61,7 +63,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 erzeugt diese Artefakte **deterministisch** und **idempotent** (erneute Läufe überschreiben konsistent statt zu duplizieren). Die Import-Schritte sind: *parse → chunk → edge → upsert*.
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*.
---
@ -77,6 +79,7 @@ Die Import-Pipeline erzeugt diese Artefakte **deterministisch** und **idempotent
- Ausschnitt/Textfenster aus der Note, als eigenständiger Such-Anker.
- 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.
> **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.
@ -128,13 +131,23 @@ 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
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`:**
* Wenn Ziel ist `value` -> Kante: `based_on`
* Wenn Ziel ist `principle` -> Kante: `derived_from`
* Wenn Ziel ist `project` -> Kante: `related_to`
Dies ermöglicht im Graphen präzise Abfragen wie "Zeige alle Erfahrungen, die auf Wert X basieren" (via `based_on`), was mit generischen `related_to` Kanten nicht möglich wäre.
---
## 3) Edge-Payload Felder & Semantik
Jede Kante hat mindestens:
- `kind` Beziehungsart *(belongs_to, next, prev, references, related_to, depends_on, similar_to, …)*
- `kind` Beziehungsart *(belongs_to, next, prev, references, related_to, depends_on, similar_to, based_on, uses, …)*
- `scope` `"chunk"` (Standard in v2.2)
- `source_id`, `target_id` Quell-/Ziel-Knoten (Chunk-IDs oder Note-Titel bei unresolved Targets)
- `note_id` **Owner-Note** (die Note, aus der die Kante stammt)
@ -209,9 +222,9 @@ Die API gibt diese Analysen als menschenlesbare Sätze (`reasons`) und als Daten
---
## 6) Context Intelligence & Intent Router (WP06/WP07)
## 6) Context Intelligence & Intent Router (WP06WP11)
Seit WP06/WP07 agiert Mindnet nicht mehr statisch, sondern passt seine Suchstrategie dem **Intent** (der Absicht) des Nutzers an. Dies ist die Transformation vom reinen Wissens-Abrufer zum strategischen Partner.
Seit WP06 agiert Mindnet nicht mehr statisch, sondern passt seine Suchstrategie dem **Intent** (der Absicht) des Nutzers an. Dies ist die Transformation vom reinen Wissens-Abrufer zum strategischen Partner.
### 6.1 Das Problem: Statische vs. Dynamische Antworten
* **Früher (Pre-WP06):** Jede Frage ("Was ist X?" oder "Soll ich X?") wurde gleich behandelt -> Fakten-Retrieval.
@ -223,7 +236,7 @@ Der Router prüft vor jeder Antwort die Absicht über konfigurierbare Strategien
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 (Neu in WP07):** Wunsch, Wissen zu erfassen ("Neues Projekt anlegen"). → Aktiviert den Draft-Generator.
4. **INTERVIEW (WP07):** Wunsch, Wissen zu erfassen ("Neues Projekt anlegen"). → Aktiviert den Draft-Generator.
5. **CODING:** Technische Anfragen.
### 6.3 Strategic Retrieval (Injektion von Werten)
@ -236,7 +249,7 @@ Im Modus `DECISION` führt das System eine **zweite Suchstufe** aus. Es sucht ni
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) Neu in v2.4
### 6.5 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).
@ -244,6 +257,14 @@ Wenn der User Wissen erfassen will ("Ich möchte ein neues Projekt anlegen"), we
* **Draft-Status:** Fehlende Pflichtfelder werden mit `[TODO]` markiert.
* **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
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.
---
## 7) Future Concepts: The Empathic Digital Twin (Ausblick)
@ -353,6 +374,10 @@ Eine typische Gewichtung (konfigurierbar in `retriever.yaml`) ist:
- `related_to` Ähnlichkeit/Verwandtschaft (symmetrisch interpretierbar).
- `similar_to` noch engere Ähnlichkeit; oft aus Inline-Rel (bewusst gesetzt).
- `depends_on` fachliche Abhängigkeit (z. B. „Projekt X hängt von Y ab“).
- **Neu in v2.4 (Matrix):**
- `based_on` Erfahrung basiert auf Wert.
- `derived_from` Erkenntnis stammt aus Prinzip.
- `uses` Projekt nutzt Konzept.
- `belongs_to`, `next`, `prev` Struktur.
> Symmetrische Relationen (z. B. `related_to`, `similar_to`) können **explizit** nur einseitig notiert sein, aber im Retriever beidseitig interpretiert werden.
@ -377,7 +402,7 @@ Frontmatter-Eigenschaften (Properties) bleiben **minimiert**:
---
## 14) Beispiel Von Markdown zu Kanten (v2.2)
## 14) Beispiel Von Markdown zu Kanten
**Markdown (Auszug)**
# Relations Showcase
@ -406,6 +431,7 @@ Frontmatter-Eigenschaften (Properties) bleiben **minimiert**:
- Decision Engine: `config/decision_engine.yaml`.
- Logging Service: `app/services/feedback_service.py`.
- Frontend UI: `app/frontend/ui.py`.
- Intelligence Logic: `app/services/discovery.py`.
---
@ -425,5 +451,6 @@ Aktueller Implementierungsstand der Module.
| **WP06** | Decision Engine | 🟢 Live | Intent-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-UI mit Feedback & Intents. |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktiver Editor für WP07 Drafts.** |
| **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.** |

View File

@ -1,7 +1,7 @@
# Mindnet v2.4 Technische Architektur
**Datei:** `docs/mindnet_technical_architecture_v2.4.md`
**Stand:** 2025-12-10
**Status:** **FINAL** (Integrierter Stand WP01WP10 + WP07)
**Stand:** 2025-12-11
**Status:** **FINAL** (Integrierter Stand WP01WP11: Async Intelligence)
**Quellen:** `Programmplan_V2.2.md`, `Handbuch.md`, `chunking_strategy.md`, `wp04_retriever_scoring.md`.
> **Ziel dieses Dokuments:**
@ -27,7 +27,7 @@
- [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](#41-verarbeitungsschritte)
- [4.1 Verarbeitungsschritte (Async)](#41-verarbeitungsschritte-async)
- [5. Retriever-Architektur \& Scoring](#5-retriever-architektur--scoring)
- [5.1 Betriebsmodi](#51-betriebsmodi)
- [5.2 Scoring-Formel (WP04a)](#52-scoring-formel-wp04a)
@ -43,7 +43,8 @@
- [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.4 Deployment Ports](#74-deployment-ports)
- [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)
- [8.1 Komponenten](#81-komponenten)
- [8.2 Log-Dateien](#82-log-dateien)
@ -65,8 +66,9 @@ Mindnet ist ein **lokales RAG-System (Retrieval Augmented Generation)** mit Web-
* **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.
5. **Frontend:** Streamlit-App (`ui.py`) für Interaktion und Visualisierung inkl. **Draft Editor**.
6. **Inference:** Lokales LLM (Ollama: Phi-3 Mini) für RAG-Chat und Antwortgenerierung.
* **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**.
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`).
@ -76,6 +78,7 @@ Das System arbeitet **deterministisch** (stabile IDs) und ist **konfigurationsge
├── app/
│ ├── main.py # FastAPI Einstiegspunkt
│ ├── core/
│ │ ├── ingestion.py # NEU: Async Ingestion Service (WP11)
│ │ ├── qdrant.py # Client-Factory & Connection
│ │ ├── qdrant_points.py # Low-Level Point Operations (Upsert/Delete)
│ │ ├── note_payload.py # Bau der Note-Objekte
@ -88,13 +91,14 @@ Das System arbeitet **deterministisch** (stabile IDs) und ist **konfigurationsge
│ ├── models/ # Pydantic DTOs
│ ├── routers/
│ │ ├── query.py # Such-Endpunkt
│ │ ├── ingest.py # NEU: API für Save & Analyze (WP11)
│ │ ├── chat.py # Hybrid Router & Interview Logic (WP06/07)
│ │ ├── feedback.py # Feedback-Endpunkt (WP04c)
│ │ └── ...
│ ├── services/
│ │ ├── llm_service.py # Ollama Client mit Timeout & Raw-Mode
│ │ ├── feedback_service.py # JSONL Logging (WP04c)
│ │ └── embeddings_client.py
│ │ ├── llm_service.py # Ollama Chat Client
│ │ ├── embeddings_client.py# NEU: Async Embedding Client (HTTPX)
│ │ └── feedback_service.py # JSONL Logging (WP04c)
│ ├── frontend/ # NEU (WP10)
│ └── ui.py # Streamlit Application inkl. Sanitizer
├── config/
@ -105,7 +109,7 @@ Das System arbeitet **deterministisch** (stabile IDs) und ist **konfigurationsge
├── data/
│ └── logs/ # Lokale JSONL-Logs (WP04c)
├── scripts/
│ ├── import_markdown.py # Haupt-Importer CLI
│ ├── 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
@ -136,6 +140,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`).
* **Schema (Payload):**
| Feld | Datentyp | Beschreibung |
@ -204,37 +209,40 @@ Steuert die LLM-Persönlichkeit und Templates.
* Enthält Templates für alle Strategien inkl. `interview_template` mit One-Shot Logik.
### 3.5 Environment (`.env`)
Erweiterung für LLM-Steuerung:
Erweiterung für LLM-Steuerung und Embedding-Modell:
MINDNET_LLM_MODEL=phi3:mini
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_DECISION_CONFIG="config/decision_engine.yaml"
MINDNET_VAULT_ROOT="./vault" # Neu: Pfad für Write-Back
---
## 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.
### 4.1 Verarbeitungsschritte
### 4.1 Verarbeitungsschritte (Async)
1. **Discovery & Parsing:**
* Einlesen der `.md` Dateien. Hash-Vergleich (Body/Frontmatter) zur Erkennung von Änderungen.
2. **Typauflösung:**
* Laden der `types.yaml`. Bestimmen des effektiven Typs und der `edge_defaults`.
* Bestimmung des `type` via `types.yaml`.
3. **Chunking:**
* Zerlegung via `chunker.py` basierend auf `chunk_profile` (z.B. `by_heading`, `short`, `long`).
* Trennung von `text` (Kern) und `window` (Embedding-Kontext).
4. **Kantenableitung (Edge Derivation):**
Die `derive_edges.py` erzeugt Kanten in strikter Reihenfolge:
1. **Inline-Edges:** `[[rel:depends_on X]]``kind=depends_on`, `rule_id=inline:rel`, `conf=0.95`.
2. **Callout-Edges:** `> [!edge] related_to: [[X]]``kind=related_to`, `rule_id=callout:edge`, `conf=0.90`.
3. **Explizite Referenzen:** `[[X]]``kind=references`, `rule_id=explicit:wikilink`, `conf=1.0`.
4. **Typ-Defaults:** Für jede Referenz werden Zusatzkanten gemäß `edge_defaults` erzeugt (z.B. `project` -> `depends_on`). `rule_id=edge_defaults:...`, `conf=0.7`.
5. **Struktur:** `belongs_to`, `next`, `prev` (automatisch).
5. **Upsert:**
* Schreiben in Qdrant. Nutzung von `--purge-before-upsert` für saubere Updates.
* 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.
5. **Kantenableitung (Edge Derivation):**
* `derive_edges.py` erzeugt Inline-, Callout- und Default-Edges.
6. **Upsert:**
* Schreiben in Qdrant. Nutzung von `--purge-before-upsert`.
* **Strict Mode:** Der Prozess bricht ab, wenn Embeddings leer sind oder Dimension `0` haben.
---
@ -243,7 +251,7 @@ Das Skript `scripts/import_markdown.py` orchestriert den Prozess.
Der Retriever (`app/core/retriever.py`) unterstützt zwei Modi. Für den Chat wird **zwingend** der Hybrid-Modus genutzt.
### 5.1 Betriebsmodi
* **Semantic:** Reine Vektorsuche. Schnell.
* **Semantic:** Reine Vektorsuche (768d).
* **Hybrid:** Vektorsuche + Graph-Expansion (Tiefe N) + Re-Ranking.
### 5.2 Scoring-Formel (WP04a)
@ -274,7 +282,7 @@ Der Hybrid-Modus lädt dynamisch die Nachbarschaft der Top-K Vektor-Treffer ("Se
---
## 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.
@ -329,7 +337,7 @@ 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 und `/feedback` für Bewertungen.
* **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).
### 7.2 Features & UI-Logik
@ -350,7 +358,14 @@ Wenn der Intent `INTERVIEW` ist, rendert die UI statt einer Textblase den **Draf
3. **Editor Widget:** `st.text_area` erlaubt das Bearbeiten des Inhalts vor dem Speichern.
4. **Action:** Buttons zum Download oder Kopieren des fertigen Markdowns.
### 7.4 Deployment Ports
### 7.4 State Management (Resurrection Pattern)
Um Datenverlust bei Tab-Wechseln (Chat <-> Editor) zu verhindern, nutzt `ui.py` ein Persistenz-Muster:
* Daten liegen in `st.session_state[data_key]`.
* Widgets liegen in `st.session_state[widget_key]`.
* Callbacks (`on_change`) synchronisieren Widget -> Data.
* Beim Neu-Rendern wird Widget-State aus Data-State wiederhergestellt.
### 7.5 Deployment Ports
Zur sauberen Trennung von Prod und Dev laufen Frontend und Backend auf dedizierten Ports:
| Umgebung | Backend (FastAPI) | Frontend (Streamlit) |

View File

@ -1,7 +1,7 @@
# mindnet v2.4 Pipeline Playbook
**Datei:** `docs/mindnet_pipeline_playbook_v2.4.md`
**Stand:** 2025-12-10
**Status:** **FINAL** (Inkl. WP07 Interview & WP10a Editor)
**Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. Async Ingestion & Active Intelligence)
**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](#21-der-12-schritte-prozess)
- [2.1 Der 12-Schritte-Prozess (Async)](#21-der-12-schritte-prozess-async)
- [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)
@ -27,6 +27,7 @@
- [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.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)
- [7.1 Pflicht-Tests vor Commit](#71-pflicht-tests-vor-commit)
@ -44,7 +45,7 @@ Dieses Playbook ist das zentrale operative Handbuch für die **mindnet-Pipeline*
**Zielgruppe:** Dev/Ops, Tech-Leads.
**Scope:**
* **Ist-Stand (WP01WP10a):** Import, Chunking, Edge-Erzeugung, Hybrider Retriever, RAG-Chat (Hybrid Router), Feedback Loop, Frontend, Draft Editor.
* **Ist-Stand (WP01WP11):** Async Import, 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).
---
@ -53,8 +54,8 @@ 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
Gemäß WP03-Spezifikation läuft der Import intern wie folgt ab:
### 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.
1. **Markdown lesen:** Rekursives Scannen des Vaults.
2. **Frontmatter extrahieren:** Validierung von Pflichtfeldern (`id`, `type`, `title`).
@ -65,8 +66,10 @@ Gemäß WP03-Spezifikation läuft der Import intern wie folgt ab:
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. **Chunks upserten:** Schreiben in Qdrant (`mindnet_chunks`).
11. **Edges upserten:** Schreiben in Qdrant (`mindnet_edges`).
10. **Embedding & Upsert (Async):**
* Das System nutzt eine **Semaphore** (Limit: 5 Files concurrent), um Ollama nicht zu überlasten.
* 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.
### 2.2 Standard-Betrieb (Inkrementell)
@ -99,13 +102,18 @@ 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 Embedding-Modellen.
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`).
# 1. Qdrant Collections löschen und neu anlegen (Wipe inkl. Schema)
**WICHTIG:** Vorher das Modell pullen, sonst schlägt der Import fehl!
# 0. Modell sicherstellen (WICHTIG für v2.4+)
ollama pull nomic-embed-text
# 1. Qdrant Collections löschen und neu anlegen (Wipe inkl. Schema 768d)
python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes
# 2. Vollständiger Import aller Dateien
python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply
python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
---
@ -177,6 +185,17 @@ Der Router (`chat.py`) reichert die gefundenen Chunks mit Metadaten an:
* **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.
### 5.5 Active Intelligence Pipeline (Neu in v2.4)
Ein paralleler Datenfluss im Frontend ("Draft Editor") zur Unterstützung des Autors.
1. **Trigger:** User klickt "Analyse starten" oder tippt.
2. **Service:** `ingest/analyze` (Backend).
3. **Discovery:**
* **Sliding Window:** Zerlegt Text in Abschnitte.
* **Embedding:** Vektorisiert Abschnitte via Nomic (Async).
* **Exact Match:** Sucht nach Aliases ("KI-Gedächtnis").
* **Matrix Logic:** Bestimmt Kanten-Typ (`experience` -> `based_on` -> `value`).
4. **Feedback:** UI zeigt Vorschläge (`[[rel:...]]`) zum Einfügen an.
---
## 6. Feedback & Lernen (WP04c)
@ -209,12 +228,12 @@ Prüft am laufenden System (Prod oder Dev), ob Semantik, Graph und Feedback funk
# Retriever Test
python scripts/test_retriever_smoke.py --mode hybrid --top-k 5
# Intelligence Test (WP11)
curl -X POST "http://localhost:8002/ingest/analyze" -d '{"text": "mindnet", "type": "journal"}'
# Decision Engine Test (WP06)
python tests/test_wp06_decision.py -p 8002 -e EMPATHY -q "Alles ist grau"
# Interview Test (WP07)
python tests/test_wp06_decision.py -p 8002 -e INTERVIEW -q "Neues Projekt starten"
# Feedback Test
python tests/test_feedback_smoke.py --url http://localhost:8001/query
@ -251,3 +270,4 @@ Aktueller Implementierungsstand der Module.
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** |

View File

@ -1,7 +1,7 @@
# Mindnet v2.4 User Guide
**Datei:** `docs/mindnet_user_guide_v2.4.md`
**Stand:** 2025-12-10
**Status:** **FINAL** (Inkl. RAG, Web-Interface & Interview-Assistent)
**Stand:** 2025-12-11
**Status:** **FINAL** (Inkl. RAG, Web-Interface & Interview-Assistent & Intelligence)
**Quellen:** `knowledge_design.md`, `wp04_retriever_scoring.md`, `Programmplan_V2.2.md`, `Handbuch.md`.
> **Willkommen bei Mindnet.**
@ -42,6 +42,7 @@ Seit Version 2.3.1 bedienst du Mindnet über eine grafische Oberfläche im Brows
### 2.2 Die Sidebar (Einstellungen & Verlauf)
* **Modus-Wahl:** Umschalten zwischen "💬 Chat" und "📝 Manueller Editor".
* *Neu in v2.4:* Der manuelle Editor speichert deine Eingaben auch beim Wechseln der Tabs ("State Resurrection").
* **Verlauf:** Die letzten Suchanfragen sind hier gelistet. Ein Klick führt die Suche erneut aus.
* **Settings:**
* **Top-K:** Wie viele Quellen sollen gelesen werden? (Standard: 5).
@ -68,7 +69,7 @@ Wenn du frustriert bist oder reflektieren willst, wechselt Mindnet in den "Ich"-
* **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") Neu!
### 3.3 Modus: Interview ("Der Analyst")
Wenn du Wissen festhalten willst, statt zu suchen.
* **Auslöser:** "Neues Projekt", "Notiz erstellen", "Ich will etwas festhalten", "Neue Entscheidung dokumentieren".
@ -129,3 +130,14 @@ Mindnet kann dir helfen, Markdown-Notizen zu schreiben.
* 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.
### 6.4 Der Intelligence-Workflow (Neu in v2.4)
Wenn du Texte im **manuellen Editor** schreibst, unterstützt dich Mindnet aktiv bei der Vernetzung:
1. **Schreiben:** Tippe deinen Text im Tab **"✏️ Inhalt"**.
2. **Analysieren:** Wechsle zum Tab **"🧠 Intelligence"** und klicke auf **"🔍 Analyse starten"**. Das System scannt deinen Text (Vektor-Suche & Exact Match).
3. **Vorschläge nutzen:**
* **Exakte Treffer:** Das System erkennt Begriffe wie "KI-Gedächtnis" automatisch als Alias für "Mindnet (System)".
* **Semantische Treffer:** Das System findet inhaltlich verwandte Notizen.
* **Klick auf " Einfügen":** Fügt den Link (z.B. `[[rel:related_to Mindnet]]`) an der Cursor-Position oder am Ende ein.
4. **Speichern:** Klicke auf "💾 Speichern & Indizieren". Der Text wird sofort in den Vault geschrieben und in Qdrant indiziert.

View File

@ -1,593 +1,100 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
scripts/import_markdown.py
Zweck
-----
- Liest Markdown-Notizen aus einem Vault ein
- Erzeugt Note-Payload, Chunk-Payloads (+ optionale Embeddings) und Edges
- Schreibt alles idempotent in Qdrant (Notes, Chunks, Edges)
- Integriert eine optionale Type-Registry (types.yaml), um z. B. chunk_profile
und retriever_weight pro Notiz-Typ zu steuern.
Wesentliche Fixes ggü. vorherigen fehlerhaften Ständen
------------------------------------------------------
- `embed_texts` wird optional importiert und defensiv geprüft (kein NameError mehr)
- `effective_chunk_profile` / `effective_retriever_weight` und Registry-Helfer
sind VOR `main()` definiert (kein NameError mehr)
- `retriever_weight` wird in Note- und Chunk-Payload zuverlässig gesetzt
- Robuste Kantenbildung; Fehler bei Edges blockieren Notes/Chunks nicht
- Korrekte Verwendung von `scroll_filter` beim Qdrant-Client
- `--purge-before-upsert` entfernt alte Chunks/Edges einer Note vor dem Upsert
Qdrant / ENV
------------
- QDRANT_URL | QDRANT_HOST/QDRANT_PORT | QDRANT_API_KEY
- COLLECTION_PREFIX (Default: mindnet), via --prefix überschreibbar
- VECTOR_DIM (Default: 384)
- MINDNET_NOTE_SCOPE_REFS: true|false (Default: false)
- MINDNET_TYPES_FILE: Pfad zu types.yaml (optional; Default: ./types.yaml)
Beispiele
---------
# Standard (Body, parsed, canonical)
python3 -m scripts.import_markdown --vault ./vault
# Erstimport nach truncate (Create-Fall)
python3 -m scripts.import_markdown --vault ./vault --apply --purge-before-upsert
# Nur eine Datei (Diagnose)
python3 -m scripts.import_markdown --vault ./vault --only-path ./vault/30_projects/project-demo.md --apply
# Sync-Deletes (Dry-Run → Apply)
python3 -m scripts.import_markdown --vault ./vault --sync-deletes
python3 -m scripts.import_markdown --vault ./vault --sync-deletes --apply
CLI-Tool zum Importieren von Markdown-Dateien in Qdrant.
Updated for Mindnet v2.3.6 (Async Ingestion Support).
"""
from __future__ import annotations
import argparse
import json
import asyncio
import os
import sys
from typing import Dict, List, Optional, Tuple, Any, Set
import argparse
import logging
from pathlib import Path
from dotenv import load_dotenv
from qdrant_client.http import models as rest
# --- Projekt-Imports ---
from app.core.parser import (
read_markdown,
normalize_frontmatter,
validate_required_frontmatter,
)
from app.core.note_payload import make_note_payload
from app.core.chunker import assemble_chunks
from app.core.chunk_payload import make_chunk_payloads
try:
from app.core.derive_edges import build_edges_for_note
except Exception: # pragma: no cover
from app.core.edges import build_edges_for_note # type: ignore
from app.core.qdrant import (
QdrantConfig,
get_client,
ensure_collections,
ensure_payload_indexes,
)
from app.core.qdrant_points import (
points_for_chunks,
points_for_note,
points_for_edges,
upsert_batch,
)
# embeddings sind optional (z. B. im reinen Payload-Backfill)
try:
from app.core.embed import embed_texts # optional
except Exception: # pragma: no cover
embed_texts = None
# ---------------------------------------------------------------------
# Type-Registry (types.yaml) Helper (robust, optional)
# ---------------------------------------------------------------------
def _env(name: str, default: Optional[str] = None) -> Optional[str]:
v = os.getenv(name)
return v if v is not None else default
def _load_json_or_yaml(path: str) -> dict:
import io
data: dict = {}
if not path or not os.path.exists(path):
return data
try:
import yaml # type: ignore
with io.open(path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
if not isinstance(data, dict):
return {}
return data
except Exception:
# YAML evtl. nicht installiert versuche JSON
try:
with io.open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
return {}
return data
except Exception:
return {}
def load_type_registry() -> dict:
# Reihenfolge: ENV > ./types.yaml (im aktuellen Arbeitsverzeichnis)
p = _env("MINDNET_TYPES_FILE", None)
if p and os.path.exists(p):
return _load_json_or_yaml(p)
fallback = os.path.abspath("./config/types.yaml") if os.path.exists("./config/types.yaml") else os.path.abspath("./types.yaml")
return _load_json_or_yaml(fallback)
def get_type_config(note_type: Optional[str], reg: dict) -> dict:
if not reg or not isinstance(reg, dict):
return {}
types = reg.get("types", {}) if isinstance(reg.get("types"), dict) else {}
if note_type and isinstance(note_type, str) and note_type in types:
return types[note_type] or {}
# Fallback: concept
return types.get("concept", {}) or {}
def resolve_note_type(requested: Optional[str], reg: dict) -> str:
if requested and isinstance(requested, str):
return requested
# Fallback wenn nichts gesetzt ist
types = reg.get("types", {}) if isinstance(reg.get("types"), dict) else {}
return "concept" if "concept" in types else (requested or "concept")
def effective_chunk_profile(note_type: str, reg: dict) -> Optional[str]:
"""Resolve chunk_profile for type or from defaults/global.
Accepts symbolic profiles: short|medium|long|default.
"""
cfg = get_type_config(note_type, reg)
prof = (cfg.get("chunk_profile") if isinstance(cfg, dict) else None)
if isinstance(prof, str) and prof:
return prof
# defaults fallbacks
for key in ("defaults", "default", "global"):
dcfg = reg.get(key) if isinstance(reg, dict) else None
if isinstance(dcfg, dict):
dprof = dcfg.get("chunk_profile")
if isinstance(dprof, str) and dprof:
return dprof
return "default"
def effective_retriever_weight(note_type: str, reg: dict) -> Optional[float]:
"""Resolve retriever_weight for type or defaults; returns float.
"""
cfg = get_type_config(note_type, reg)
w = (cfg.get("retriever_weight") if isinstance(cfg, dict) else None)
try:
if w is not None:
return float(w)
except Exception:
pass
# defaults fallbacks
for key in ("defaults", "default", "global"):
dcfg = reg.get(key) if isinstance(reg, dict) else None
if isinstance(dcfg, dict):
dw = dcfg.get("retriever_weight")
try:
if dw is not None:
return float(dw)
except Exception:
pass
return 1.0
# ---------------------------------------------------------------------
# Sonstige Helper
# ---------------------------------------------------------------------
def iter_md(root: str) -> List[str]:
out: List[str] = []
for dirpath, _, filenames in os.walk(root):
for fn in filenames:
if not fn.lower().endswith(".md"):
continue
p = os.path.join(dirpath, fn)
pn = p.replace("\\", "/")
if any(ex in pn for ex in ["/.obsidian/", "/_backup_frontmatter/", "/_imported/"]):
continue
out.append(p)
return sorted(out)
def collections(prefix: str) -> Tuple[str, str, str]:
return f"{prefix}_notes", f"{prefix}_chunks", f"{prefix}_edges"
def fetch_existing_note_payload(client, prefix: str, note_id: str) -> Optional[Dict]:
notes_col, _, _ = collections(prefix)
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
points, _ = client.scroll(
collection_name=notes_col,
scroll_filter=f, # wichtig: scroll_filter (nicht: filter)
with_payload=True,
with_vectors=False,
limit=1,
)
if not points:
return None
return points[0].payload or {}
def list_qdrant_note_ids(client, prefix: str) -> Set[str]:
notes_col, _, _ = collections(prefix)
out: Set[str] = set()
next_page = None
while True:
pts, next_page = client.scroll(
collection_name=notes_col,
with_payload=True,
with_vectors=False,
limit=256,
offset=next_page,
)
if not pts:
break
for p in pts:
pl = p.payload or {}
nid = pl.get("note_id")
if isinstance(nid, str):
out.add(nid)
if next_page is None:
break
return out
def purge_note_artifacts(client, prefix: str, note_id: str) -> None:
_, chunks_col, edges_col = collections(prefix)
filt = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
for col in (chunks_col, edges_col):
try:
client.delete(
collection_name=col,
points_selector=rest.FilterSelector(filter=filt),
wait=True
# Importiere den neuen Async Service
# Stellen wir sicher, dass der Pfad stimmt (Pythonpath)
import sys
sys.path.append(os.getcwd())
from app.core.ingestion import IngestionService
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("importer")
async def main_async(args):
vault_path = Path(args.vault).resolve()
if not vault_path.exists():
logger.error(f"Vault path does not exist: {vault_path}")
return
# Service initialisieren (startet Async Clients)
logger.info(f"Initializing IngestionService (Prefix: {args.prefix})")
service = IngestionService(collection_prefix=args.prefix)
logger.info(f"Scanning {vault_path}...")
files = list(vault_path.rglob("*.md"))
# Exclude .obsidian folder if present
files = [f for f in files if ".obsidian" not in str(f)]
files.sort()
logger.info(f"Found {len(files)} markdown files.")
stats = {"processed": 0, "skipped": 0, "errors": 0}
# Wir nutzen eine Semaphore, um nicht zu viele Files gleichzeitig zu öffnen/embedden
sem = asyncio.Semaphore(5) # Max 5 concurrent files to avoid OOM or Rate Limit
async def process_with_limit(f_path):
async with sem:
try:
res = await service.process_file(
file_path=str(f_path),
vault_root=str(vault_path),
force_replace=args.force,
apply=args.apply,
purge_before=True
)
return res
except Exception as e:
print(json.dumps({"note_id": note_id, "warn": f"delete in {col} via filter failed: {e}"}))
return {"status": "error", "error": str(e), "path": str(f_path)}
def delete_note_everywhere(client, prefix: str, note_id: str) -> None:
notes_col, chunks_col, edges_col = collections(prefix)
filt = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
for col in (edges_col, chunks_col, notes_col):
try:
client.delete(
collection_name=col,
points_selector=rest.FilterSelector(filter=filt),
wait=True
)
except Exception as e:
print(json.dumps({"note_id": note_id, "warn": f"delete in {col} failed: {e}"}))
# Batch Processing
# Wir verarbeiten in Chunks, um den Progress zu sehen
batch_size = 20
for i in range(0, len(files), batch_size):
batch = files[i:i+batch_size]
logger.info(f"Processing batch {i} to {i+len(batch)}...")
tasks = [process_with_limit(f) for f in batch]
results = await asyncio.gather(*tasks)
# --- Neu: Existenz-Checks für Artefakte (fehlertoleranter Rebuild) ---
def _has_any_point(client, collection: str, note_id: str) -> bool:
"""Prüft, ob es mind. einen Punkt mit note_id in der Collection gibt."""
filt = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
pts, _ = client.scroll(
collection_name=collection,
scroll_filter=filt,
with_payload=False,
with_vectors=False,
limit=1,
)
return bool(pts)
def artifacts_missing(client, prefix: str, note_id: str) -> Tuple[bool, bool]:
"""Gibt (chunks_missing, edges_missing) zurück."""
_, chunks_col, edges_col = collections(prefix)
chunks_missing = not _has_any_point(client, chunks_col, note_id)
edges_missing = not _has_any_point(client, edges_col, note_id)
return chunks_missing, edges_missing
# ---------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------
def _resolve_mode(m: Optional[str]) -> str:
m = (m or "body").strip().lower()
return m if m in {"body", "frontmatter", "full"} else "body"
def main() -> None:
load_dotenv()
ap = argparse.ArgumentParser(
prog="scripts.import_markdown",
description="Importiert Markdown-Notizen in Qdrant (Notes/Chunks/Edges)."
)
ap.add_argument("--vault", required=True, help="Pfad zum Vault (Ordner mit .md-Dateien)")
ap.add_argument("--only-path", help="Nur diese Datei verarbeiten (absolut oder relativ)")
ap.add_argument("--apply", action="store_true", help="Schreibt nach Qdrant (sonst Dry-Run)")
ap.add_argument("--purge-before-upsert", action="store_true", help="Alte Chunks/Edges der Note vorher löschen")
ap.add_argument("--force-replace", action="store_true", help="Note/Chunks/Edges unabhängig von Hash neu schreiben")
ap.add_argument("--note-id", help="Nur Notes mit dieser ID verarbeiten (Filter)")
ap.add_argument("--note-scope-refs", action="store_true", help="Note-scope References/Backlinks erzeugen")
ap.add_argument("--hash-mode", help="body|frontmatter|full (Default body)")
ap.add_argument("--hash-source", help="parsed|raw (Default parsed)")
ap.add_argument("--hash-normalize", help="canonical|none (Default canonical)")
ap.add_argument("--compare-text", action="store_true", help="Parsed fulltext zusätzlich direkt vergleichen")
ap.add_argument("--baseline-modes", action="store_true", help="Fehlende Hash-Varianten still nachtragen (Notes)")
ap.add_argument("--sync-deletes", action="store_true", help="Qdrant->Vault Lösch-Sync (Dry-Run; mit --apply ausführen)")
ap.add_argument("--prefix", help="Collection-Prefix (überschreibt ENV COLLECTION_PREFIX)")
args = ap.parse_args()
mode = _resolve_mode(args.hash_mode) # body|frontmatter|full
src = _env("MINDNET_HASH_SOURCE", args.hash_source or "parsed") # parsed|raw
norm = _env("MINDNET_HASH_NORMALIZE", args.hash_normalize or "canonical") # canonical|none
note_scope_refs_env = (_env("MINDNET_NOTE_SCOPE_REFS", "false") == "true")
note_scope_refs = args.note_scope_refs or note_scope_refs_env
compare_text = args.compare_text or (_env("MINDNET_COMPARE_TEXT", "false") == "true")
# Qdrant
cfg = QdrantConfig.from_env()
if args.prefix:
cfg.prefix = args.prefix.strip()
client = get_client(cfg)
ensure_collections(client, cfg.prefix, cfg.dim)
ensure_payload_indexes(client, cfg.prefix)
# Type-Registry laden (optional)
reg = load_type_registry()
root = os.path.abspath(args.vault)
# Dateiliste
if args.only_path:
only = os.path.abspath(args.only_path)
files = [only]
for res in results:
if res.get("status") == "success":
stats["processed"] += 1
elif res.get("status") == "error":
stats["errors"] += 1
logger.error(f"Error in {res.get('path')}: {res.get('error')}")
else:
files = iter_md(root)
if not files:
print("Keine Markdown-Dateien gefunden.", file=sys.stderr)
sys.exit(2)
stats["skipped"] += 1
# Optional: Sync-Deletes vorab
if args.sync_deletes:
vault_note_ids: Set[str] = set()
for path in files:
try:
parsed = read_markdown(path)
if not parsed:
continue
fm = normalize_frontmatter(parsed.frontmatter)
nid = fm.get("id")
if isinstance(nid, str):
vault_note_ids.add(nid)
except Exception:
continue
qdrant_note_ids = list_qdrant_note_ids(client, cfg.prefix)
to_delete = sorted(qdrant_note_ids - vault_note_ids)
print(json.dumps({
"action": "sync-deletes",
"prefix": cfg.prefix,
"qdrant_total": len(qdrant_note_ids),
"vault_total": len(vault_note_ids),
"to_delete_count": len(to_delete),
"to_delete": to_delete[:50] + ([""] if len(to_delete) > 50 else [])
}, ensure_ascii=False))
if args.apply and to_delete:
for nid in to_delete:
print(json.dumps({"action": "delete", "note_id": nid, "decision": "apply"}))
delete_note_everywhere(client, cfg.prefix, nid)
key_current = f"{mode}:{src}:{norm}"
processed = 0
for path in files:
try:
parsed = read_markdown(path)
if not parsed:
continue
except Exception as e:
print(json.dumps({"path": path, "error": f"read_markdown failed: {type(e).__name__}: {e}"}))
continue
# --- Frontmatter prüfen ---
try:
fm = normalize_frontmatter(parsed.frontmatter)
validate_required_frontmatter(fm)
except Exception as e:
print(json.dumps({"path": path, "error": f"Frontmatter invalid: {type(e).__name__}: {e}"}))
continue
if args.note_id and not args.only_path and fm.get("id") != args.note_id:
continue
processed += 1
# --- Type-Registry anwenden (chunk_profile / retriever_weight) ---
try:
note_type = resolve_note_type(fm.get("type"), reg)
except Exception:
note_type = (fm.get("type") or "concept")
fm["type"] = note_type or fm.get("type") or "concept"
prof = effective_chunk_profile(note_type, reg)
if prof:
fm["chunk_profile"] = prof
weight = effective_retriever_weight(note_type, reg)
if weight is not None:
try:
fm["retriever_weight"] = float(weight)
except Exception:
pass # falls FM string-inkonsistent ist
# --- Payload aufbauen (inkl. Hashes) ---
try:
note_pl = make_note_payload(
parsed,
vault_root=root,
hash_mode=mode,
hash_normalize=norm,
hash_source=src,
file_path=path,
)
except Exception as e:
print(json.dumps({"path": path, "error": f"make_note_payload failed: {type(e).__name__}: {e}"}))
continue
if not note_pl.get("fulltext"):
note_pl["fulltext"] = getattr(parsed, "body", "") or ""
# retriever_weight sicher in Note-Payload spiegeln (für spätere Filter)
if "retriever_weight" not in note_pl and fm.get("retriever_weight") is not None:
try:
note_pl["retriever_weight"] = float(fm.get("retriever_weight"))
except Exception:
pass
note_id = note_pl.get("note_id") or fm.get("id")
if not note_id:
print(json.dumps({"path": path, "error": "Missing note_id after payload build"}))
continue
# --- bestehenden Payload laden (zum Diff) ---
old_payload = None if args.force_replace else fetch_existing_note_payload(client, cfg.prefix, note_id)
has_old = old_payload is not None
old_hashes = (old_payload or {}).get("hashes") or {}
old_hash_exact = old_hashes.get(key_current)
new_hash_exact = (note_pl.get("hashes") or {}).get(key_current)
needs_baseline = (old_hash_exact is None)
hash_changed = (old_hash_exact is not None and new_hash_exact is not None and old_hash_exact != new_hash_exact)
text_changed = False
if compare_text:
old_text = (old_payload or {}).get("fulltext") or ""
new_text = note_pl.get("fulltext") or ""
text_changed = (old_text != new_text)
changed = args.force_replace or (not has_old) or hash_changed or text_changed
do_baseline_only = (args.baseline_modes and has_old and needs_baseline and not changed)
# --- Chunks + Embeddings vorbereiten ---
try:
body_text = getattr(parsed, "body", "") or ""
chunks = assemble_chunks(fm["id"], body_text, fm.get("type", "concept"))
chunk_pls: List[Dict[str, Any]] = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text)
except Exception as e:
print(json.dumps({"path": path, "note_id": note_id, "error": f"chunk build failed: {type(e).__name__}: {e}"}))
continue
# retriever_weight auf Chunk-Payload spiegeln
if fm.get("retriever_weight") is not None:
try:
rw = float(fm.get("retriever_weight"))
for pl in chunk_pls:
# Feld nur setzen, wenn noch nicht vorhanden
if "retriever_weight" not in pl:
pl["retriever_weight"] = rw
except Exception:
pass
# Embeddings (fallback: Nullvektoren)
vecs: List[List[float]] = [[0.0] * int(cfg.dim) for _ in chunk_pls]
if embed_texts and chunk_pls:
try:
texts_for_embed = [(pl.get("window") or pl.get("text") or "") for pl in chunk_pls]
vecs = embed_texts(texts_for_embed)
except Exception as e:
print(json.dumps({"path": path, "note_id": note_id, "warn": f"embed_texts failed, using zeros: {e}"}))
# --- Fehlende Artefakte in Qdrant ermitteln ---
chunks_missing, edges_missing = artifacts_missing(client, cfg.prefix, note_id)
# --- Edges (robust) ---
edges: List[Dict[str, Any]] = []
edges_failed = False
should_build_edges = (changed and (not do_baseline_only)) or edges_missing
if should_build_edges:
try:
note_refs = note_pl.get("references") or []
edges = build_edges_for_note(
note_id,
chunk_pls,
note_level_references=note_refs,
include_note_scope_refs=note_scope_refs,
)
except Exception as e:
edges_failed = True
edges = []
print(json.dumps({"path": path, "note_id": note_id, "warn": f"build_edges_for_note failed, skipping edges: {type(e).__name__}: {e}"}))
# --- Summary (stdout) ---
summary = {
"note_id": note_id,
"title": fm.get("title"),
"chunks": len(chunk_pls),
"edges": len(edges),
"edges_failed": edges_failed,
"changed": changed,
"chunks_missing": chunks_missing,
"edges_missing": edges_missing,
"needs_baseline_for_mode": needs_baseline,
"decision": ("baseline-only" if args.apply and do_baseline_only else
"apply" if args.apply and (changed or chunks_missing or edges_missing) else
"apply-skip-unchanged" if args.apply and not (changed or chunks_missing or edges_missing) else
"dry-run"),
"path": note_pl["path"],
"hash_mode": mode,
"hash_normalize": norm,
"hash_source": src,
"prefix": cfg.prefix,
}
print(json.dumps(summary, ensure_ascii=False))
# --- Writes ---
logger.info(f"Done. Stats: {stats}")
if not args.apply:
continue
logger.info("DRY RUN. Use --apply to write to DB.")
if do_baseline_only:
merged_hashes = {}
merged_hashes.update(old_hashes)
merged_hashes.update(note_pl.get("hashes") or {})
if old_payload:
note_pl["hash_fulltext"] = old_payload.get("hash_fulltext", note_pl.get("hash_fulltext"))
note_pl["hash_signature"] = old_payload.get("hash_signature", note_pl.get("hash_signature"))
note_pl["hashes"] = merged_hashes
notes_name, note_pts = points_for_note(cfg.prefix, note_pl, None, cfg.dim)
upsert_batch(client, notes_name, note_pts)
continue
def main():
load_dotenv()
default_prefix = os.getenv("COLLECTION_PREFIX", "mindnet")
# Wenn nichts geändert und keine Artefakte fehlen → nichts zu tun
if not changed and not (chunks_missing or edges_missing):
continue
parser = argparse.ArgumentParser(description="Import Vault to Qdrant (Async)")
parser.add_argument("--vault", default="./vault", help="Path to vault root")
parser.add_argument("--prefix", default=default_prefix, help="Collection prefix")
parser.add_argument("--force", action="store_true", help="Force re-index all files")
parser.add_argument("--apply", action="store_true", help="Perform writes to Qdrant")
# Purge nur bei echten Änderungen (unverändert + fehlende Artefakte ≠ Purge)
if args.purge_before_upsert and has_old and changed:
try:
purge_note_artifacts(client, cfg.prefix, note_id)
except Exception as e:
print(json.dumps({"path": path, "note_id": note_id, "warn": f"purge failed: {e}"}))
# Note nur bei Änderungen neu schreiben
if changed:
notes_name, note_pts = points_for_note(cfg.prefix, note_pl, None, cfg.dim)
upsert_batch(client, notes_name, note_pts)
# Chunks schreiben, wenn geändert ODER vorher fehlend
if chunk_pls and (changed or chunks_missing):
chunks_name, chunk_pts = points_for_chunks(cfg.prefix, chunk_pls, vecs)
upsert_batch(client, chunks_name, chunk_pts)
# Edges schreiben, wenn vorhanden und (geändert ODER vorher fehlend)
if edges and (changed or edges_missing):
edges_name, edge_pts = points_for_edges(cfg.prefix, edges)
upsert_batch(client, edges_name, edge_pts)
print(f"Done. Processed notes: {processed}")
args = parser.parse_args()
# Starte den Async Loop
asyncio.run(main_async(args))
if __name__ == "__main__":
main()

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
"""
Name: scripts/reset_qdrant.py
Version: v1.2.0 (2025-11-11)
Version: v1.2.1 (2025-12-11)
Kurzbeschreibung:
Sicheres Zurücksetzen der Qdrant-Collections für EIN Projektpräfix. Das Skript
ermittelt zunächst die tatsächlich betroffenen Collections und zeigt eine
@ -39,6 +39,7 @@ Exitcodes:
0 = OK, 1 = abgebrochen/keine Aktion, 2 = Verbindungs-/Konfigurationsfehler
Changelog:
v1.2.1: Fix: load_dotenv() hinzugefügt, damit VECTOR_DIM aus .env gelesen wird.
v1.2.0: ensure_payload_indexes() nach wipe/truncate standardmäßig ausführen (idempotent); --no-indexes Flag ergänzt.
v1.1.1: Stabilisierung & Preview (2025-09-05).
v1.1.0: Interaktive Bestätigung, --yes/--dry-run hinzugefügt, Preview der betroffenen Collections.
@ -50,6 +51,9 @@ import os
import sys
from typing import List
# FIX: Dotenv laden
from dotenv import load_dotenv
from qdrant_client import QdrantClient
from qdrant_client.http import models as rest
@ -124,6 +128,9 @@ def wipe_collections(client: QdrantClient, all_col_names: List[str], existing: L
def main():
# FIX: Umgebungsvariablen aus .env laden
load_dotenv()
ap = argparse.ArgumentParser(description="Wipe oder truncate mindnet-Collections in Qdrant (mit Bestätigung & Index-Setup).")
ap.add_argument("--mode", choices=["wipe", "truncate"], required=True,
help="wipe = Collections löschen & neu anlegen; truncate = nur Inhalte löschen")
@ -135,6 +142,7 @@ def main():
# Qdrant-Konfiguration
try:
# Hier wird jetzt VECTOR_DIM=768 korrekt berücksichtigt
cfg = QdrantConfig.from_env()
except Exception as e:
print(f"Konfigurationsfehler: {e}", file=sys.stderr)
@ -156,6 +164,9 @@ def main():
existing = resolve_existing_collections(client, cfg.prefix)
nonexisting = [c for c in all_col_names if c not in existing]
# Debug-Info zur Dimension
print(f"Info: Nutze Vektor-Dimension: {cfg.dim}")
# Preview & Bestätigung
if not confirm_or_abort(args.mode, existing, nonexisting, args.yes):
print("Abgebrochen keine Änderungen vorgenommen.")