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