diff --git a/Programmmanagement/Programmplan_V2.2.md b/Programmmanagement/Programmplan_V2.2.md
index 91ee315..9b09f5f 100644
--- a/Programmmanagement/Programmplan_V2.2.md
+++ b/Programmmanagement/Programmplan_V2.2.md
@@ -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.
\ No newline at end of file
diff --git a/app/core/chunk_payload.py b/app/core/chunk_payload.py
index e48493d..1e56eda 100644
--- a/app/core/chunk_payload.py
+++ b/app/core/chunk_payload.py
@@ -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,
@@ -126,4 +133,4 @@ def make_chunk_payloads(note: Dict[str, Any],
out.append(pl)
- return out
+ return out
\ No newline at end of file
diff --git a/app/core/ingestion.py b/app/core/ingestion.py
new file mode 100644
index 0000000..cd6b293
--- /dev/null
+++ b/app/core/ingestion.py
@@ -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
+ )
\ No newline at end of file
diff --git a/app/core/note_payload.py b/app/core/note_payload.py
index 1c5e6bc..285012f 100644
--- a/app/core/note_payload.py
+++ b/app/core/note_payload.py
@@ -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,13 +215,22 @@ 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))
- return payload
+ return payload
\ No newline at end of file
diff --git a/app/frontend/ui.py b/app/frontend/ui.py
index d3e714b..733bcb4 100644
--- a/app/frontend/ui.py
+++ b/app/frontend/ui.py
@@ -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);
}
""", 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)
+
+ # 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()
- st.session_state[f"{key_base}_type"] = meta.get("type", "default")
- st.session_state[f"{key_base}_title"] = meta.get("title", "")
-
- 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'
', 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")
+ # 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)
+
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")
+ 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)
- new_tags = st.text_input("Tags (kommagetrennt)", value=st.session_state.get(f"{key_base}_tags", ""), key=f"{key_base}_inp_tags")
+ 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({
+
+ # --- TAB 2: INTELLIGENCE ---
+ with tab_intel:
+ st.info("Klicke auf 'Analysieren', um Verknüpfungen für den AKTUELLEN Text zu finden.")
+
+ 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"""
+
+ {sugg.get('target_title')} ({sugg.get('type')})
+ {sugg.get('reason')}
+ {link_text}
+
+ """, 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": new_type,
- "title": new_title,
+ "type": meta_ref["type"],
+ "title": meta_ref["title"],
"status": "draft",
- "tags": final_tags_list
- })
-
- final_doc = build_markdown_doc(final_meta, new_body)
+ "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('
', 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'
{icon} Intent: {intent} ({src})
', 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)
diff --git a/app/main.py b/app/main.py
index 3afd514..fa23b73 100644
--- a/app/main.py
+++ b/app/main.py
@@ -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"])
diff --git a/app/routers/ingest.py b/app/routers/ingest.py
new file mode 100644
index 0000000..d40b529
--- /dev/null
+++ b/app/routers/ingest.py
@@ -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)}")
\ No newline at end of file
diff --git a/app/services/discovery.py b/app/services/discovery.py
new file mode 100644
index 0000000..995abde
--- /dev/null
+++ b/app/services/discovery.py
@@ -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
\ No newline at end of file
diff --git a/app/services/embeddings_client.py b/app/services/embeddings_client.py
index 4f8636f..afad847 100644
--- a/app/services/embeddings_client.py
+++ b/app/services/embeddings_client.py
@@ -1,46 +1,90 @@
"""
-app/services/embeddings_client.py — Text→Embedding (WP-04)
+app/services/embeddings_client.py — Text→Embedding 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 []
\ No newline at end of file
diff --git a/docs/Knowledge_Design_Manual.md b/docs/Knowledge_Design_Manual.md
index 95de77e..e5d2a44 100644
--- a/docs/Knowledge_Design_Manual.md
+++ b/docs/Knowledge_Design_Manual.md
@@ -1,7 +1,7 @@
# mindnet v2.4 – Knowledge Design Manual
**Datei:** `docs/mindnet_knowledge_design_manual_v2.4.md`
-**Stand:** 2025-12-10
-**Status:** **FINAL** (Integrierter Stand WP01–WP10a)
+**Stand:** 2025-12-11
+**Status:** **FINAL** (Integrierter Stand WP01–WP11)
**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.
diff --git a/docs/Overview.md b/docs/Overview.md
index d8aacdc..b7cc8f0 100644
--- a/docs/Overview.md
+++ b/docs/Overview.md
@@ -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 (WP01–WP03).
+* **Technik:** Async Import-Pipeline, Chunking, Vektor-Datenbank (Qdrant).
+* **Status:** 🟢 Live (WP01–WP03, 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 (WP05–WP07, 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.
\ No newline at end of file
+Wir haben den **Interview-Assistenten (WP07)** und die **Backend Intelligence (WP11)** erfolgreich integriert.
+Das System kann nun aktiv helfen, Wissen zu strukturieren und zu vernetzen. Der Fokus verschiebt sich nun in Richtung **Self-Tuning (WP08)**, um aus dem gesammelten Feedback automatisch zu lernen.
\ No newline at end of file
diff --git a/docs/admin_guide.md b/docs/admin_guide.md
index c9bafe8..ef87828 100644
--- a/docs/admin_guide.md
+++ b/docs/admin_guide.md
@@ -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,36 +53,49 @@ 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"
# 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:**
-
- cd /home/llmadmin/mindnet
- git pull origin main
+### "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).
-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
---
@@ -221,7 +226,4 @@ Wenn die Datenbank korrupt ist:
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.
\ No newline at end of file
+* **Qdrant:** Auf `127.0.0.1` beschränken.
\ No newline at end of file
diff --git a/docs/appendix.md b/docs/appendix.md
index d9fc8f4..2244d5d 100644
--- a/docs/appendix.md
+++ b/docs/appendix.md
@@ -1,7 +1,7 @@
# Mindnet v2.4 – Appendices & Referenzen
**Datei:** `docs/mindnet_appendices_v2.4.md`
-**Stand:** 2025-12-10
-**Status:** **FINAL** (Integrierter Stand WP01–WP10a)
+**Stand:** 2025-12-11
+**Status:** **FINAL** (Integrierter Stand WP01–WP11)
**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.
---
@@ -146,4 +155,5 @@ Aktueller Implementierungsstand der Module.
| **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** |
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
-| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
\ No newline at end of file
+| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
+| **WP11** | Backend Intelligence | 🟢 Live | **Async Core, Nomic, Matrix.** |
\ No newline at end of file
diff --git a/Programmmanagement/ARCHITECTURE_SNAPSHOT_v2.2.1.md b/docs/archiv/ARCHITECTURE_SNAPSHOT_v2.2.1.md
similarity index 100%
rename from Programmmanagement/ARCHITECTURE_SNAPSHOT_v2.2.1.md
rename to docs/archiv/ARCHITECTURE_SNAPSHOT_v2.2.1.md
diff --git a/Programmmanagement/Überarbeitungshinweise_WP03.md b/docs/archiv/Überarbeitungshinweise_WP03.md
similarity index 100%
rename from Programmmanagement/Überarbeitungshinweise_WP03.md
rename to docs/archiv/Überarbeitungshinweise_WP03.md
diff --git a/Programmmanagement/Überarbeitungshinweise_WP04.md b/docs/archiv/Überarbeitungshinweise_WP04.md
similarity index 100%
rename from Programmmanagement/Überarbeitungshinweise_WP04.md
rename to docs/archiv/Überarbeitungshinweise_WP04.md
diff --git a/docs/dev_workflow.md b/docs/dev_workflow.md
index 70537eb..806f19e 100644
--- a/docs/dev_workflow.md
+++ b/docs/dev_workflow.md
@@ -1,6 +1,6 @@
# Mindnet v2.4 – Entwickler-Workflow
**Datei:** `docs/DEV_WORKFLOW.md`
-**Stand:** 2025-12-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://
: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.
\ No newline at end of file
diff --git a/docs/developer_guide.md b/docs/developer_guide.md
index e536dcd..ba6abf2 100644
--- a/docs/developer_guide.md
+++ b/docs/developer_guide.md
@@ -1,7 +1,7 @@
# Mindnet v2.4 – Developer Guide
**Datei:** `docs/mindnet_developer_guide_v2.4.md`
-**Stand:** 2025-12-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):**
diff --git a/docs/mindnet_functional_architecture.md b/docs/mindnet_functional_architecture.md
index 25dfff5..32b5dd2 100644
--- a/docs/mindnet_functional_architecture.md
+++ b/docs/mindnet_functional_architecture.md
@@ -1,9 +1,9 @@
# Mindnet v2.4 – Fachliche Architektur
**Datei:** `docs/mindnet_functional_architecture_v2.4.md`
-**Stand:** 2025-12-10
-**Status:** **FINAL** (Integrierter Stand WP01–WP10 + WP07)
+**Stand:** 2025-12-11
+**Status:** **FINAL** (Integrierter Stand WP01–WP11: 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).
---
@@ -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 (WP06–WP11)](#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 (WP06–WP11)
-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.** |
\ No newline at end of file
+| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
+| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
+| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** |
\ No newline at end of file
diff --git a/docs/mindnet_technical_architecture.md b/docs/mindnet_technical_architecture.md
index 7636dd6..875bc98 100644
--- a/docs/mindnet_technical_architecture.md
+++ b/docs/mindnet_technical_architecture.md
@@ -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 WP01–WP10 + WP07)
+**Stand:** 2025-12-11
+**Status:** **FINAL** (Integrierter Stand WP01–WP11: Async Intelligence)
**Quellen:** `Programmplan_V2.2.md`, `Handbuch.md`, `chunking_strategy.md`, `wp04_retriever_scoring.md`.
> **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 (`_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) |
diff --git a/docs/pipeline_playbook.md b/docs/pipeline_playbook.md
index 2834ee3..ac1170a 100644
--- a/docs/pipeline_playbook.md
+++ b/docs/pipeline_playbook.md
@@ -1,7 +1,7 @@
# mindnet v2.4 – Pipeline Playbook
**Datei:** `docs/mindnet_pipeline_playbook_v2.4.md`
-**Stand:** 2025-12-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 (WP01–WP10a):** Import, Chunking, Edge-Erzeugung, Hybrider Retriever, RAG-Chat (Hybrid Router), Feedback Loop, Frontend, Draft Editor.
+* **Ist-Stand (WP01–WP11):** 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
@@ -250,4 +269,5 @@ Aktueller Implementierungsstand der Module.
| **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** |
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
-| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
\ No newline at end of file
+| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
+| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** |
\ No newline at end of file
diff --git a/docs/user_guide.md b/docs/user_guide.md
index 09756d2..8f77940 100644
--- a/docs/user_guide.md
+++ b/docs/user_guide.md
@@ -1,7 +1,7 @@
# Mindnet v2.4 – User Guide
**Datei:** `docs/mindnet_user_guide_v2.4.md`
-**Stand:** 2025-12-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".
@@ -128,4 +129,15 @@ Mindnet kann dir helfen, Markdown-Notizen zu schreiben.
* Du siehst das generierte Frontmatter (`type: project`, `status: draft`).
* 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.
\ No newline at end of file
+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.
\ No newline at end of file
diff --git a/scripts/import_markdown.py b/scripts/import_markdown.py
index ac04459..da86fc0 100644
--- a/scripts/import_markdown.py
+++ b/scripts/import_markdown.py
@@ -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,
-)
+# Importiere den neuen Async Service
+# Stellen wir sicher, dass der Pfad stimmt (Pythonpath)
+import sys
+sys.path.append(os.getcwd())
-# 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
+from app.core.ingestion import IngestionService
+logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
+logger = logging.getLogger("importer")
-# ---------------------------------------------------------------------
-# Type-Registry (types.yaml) – Helper (robust, optional)
-# ---------------------------------------------------------------------
+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
-def _env(name: str, default: Optional[str] = None) -> Optional[str]:
- v = os.getenv(name)
- return v if v is not None else default
+ # 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.")
-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 {}
+ stats = {"processed": 0, "skipped": 0, "errors": 0}
-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)
+ # 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
-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")
+ async def process_with_limit(f_path):
+ async with sem:
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
- )
- except Exception as e:
- print(json.dumps({"note_id": note_id, "warn": f"delete in {col} via filter failed: {e}"}))
-
-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}"}))
-
-
-# --- 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]
- else:
- files = iter_md(root)
- if not files:
- print("Keine Markdown-Dateien gefunden.", file=sys.stderr)
- sys.exit(2)
-
- # 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,
+ 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:
- 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}"}))
+ return {"status": "error", "error": str(e), "path": str(f_path)}
- # --- 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))
+ # 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)
+
+ 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:
+ stats["skipped"] += 1
- # --- Writes ---
- if not args.apply:
- continue
+ logger.info(f"Done. Stats: {stats}")
+ if not args.apply:
+ 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
-
- # Wenn nichts geändert und keine Artefakte fehlen → nichts zu tun
- if not changed and not (chunks_missing or edges_missing):
- continue
-
- # 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}")
+def main():
+ load_dotenv()
+ default_prefix = os.getenv("COLLECTION_PREFIX", "mindnet")
+ 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")
+
+ args = parser.parse_args()
+
+ # Starte den Async Loop
+ asyncio.run(main_async(args))
if __name__ == "__main__":
- main()
+ main()
\ No newline at end of file
diff --git a/scripts/reset_qdrant.py b/scripts/reset_qdrant.py
index 015fa2d..de43c08 100644
--- a/scripts/reset_qdrant.py
+++ b/scripts/reset_qdrant.py
@@ -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.")
@@ -188,4 +199,4 @@ def main():
if __name__ == "__main__":
- main()
+ main()
\ No newline at end of file