WP11 #8
|
|
@ -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.
|
||||
|
|
@ -5,6 +5,7 @@ app/core/chunk_payload.py (Mindnet V2 — types.yaml authoritative)
|
|||
- neighbors_prev / neighbors_next sind Listen ([], [id]).
|
||||
- retriever_weight / chunk_profile kommen aus types.yaml (Frontmatter wird ignoriert).
|
||||
- Fallbacks: defaults.* in types.yaml; sonst 1.0 / "default".
|
||||
- WP-11 Update: Injects 'title' into chunk payload for Discovery Service.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
|
@ -82,6 +83,11 @@ def make_chunk_payloads(note: Dict[str, Any],
|
|||
file_path: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
fm = (note or {}).get("frontmatter", {}) or {}
|
||||
note_type = fm.get("type") or note.get("type") or "concept"
|
||||
|
||||
# WP-11 FIX: Title Extraction für Discovery Service
|
||||
# Wir holen den Titel aus Frontmatter oder Fallback ID/Untitled
|
||||
title = fm.get("title") or note.get("title") or fm.get("id") or "Untitled"
|
||||
|
||||
reg = types_cfg if isinstance(types_cfg, dict) else _load_types()
|
||||
|
||||
# types.yaml authoritative
|
||||
|
|
@ -106,6 +112,7 @@ def make_chunk_payloads(note: Dict[str, Any],
|
|||
pl: Dict[str, Any] = {
|
||||
"note_id": nid,
|
||||
"chunk_id": cid,
|
||||
"title": title, # <--- HIER: Titel in Payload einfügen
|
||||
"index": int(index),
|
||||
"ord": int(index) + 1,
|
||||
"type": note_type,
|
||||
|
|
|
|||
343
app/core/ingestion.py
Normal file
343
app/core/ingestion.py
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
"""
|
||||
app/core/ingestion.py
|
||||
|
||||
Zentraler Service für die Transformation von Markdown-Dateien in Qdrant-Objekte (Notes, Chunks, Edges).
|
||||
Dient als Shared Logic für:
|
||||
1. CLI-Imports (scripts/import_markdown.py)
|
||||
2. API-Uploads (WP-11)
|
||||
Refactored for Async Embedding Support.
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
|
||||
# Core Module Imports
|
||||
from app.core.parser import (
|
||||
read_markdown,
|
||||
normalize_frontmatter,
|
||||
validate_required_frontmatter,
|
||||
)
|
||||
from app.core.note_payload import make_note_payload
|
||||
from app.core.chunker import assemble_chunks
|
||||
from app.core.chunk_payload import make_chunk_payloads
|
||||
|
||||
# Fallback für Edges Import (Robustheit)
|
||||
try:
|
||||
from app.core.derive_edges import build_edges_for_note
|
||||
except ImportError:
|
||||
try:
|
||||
from app.core.derive_edges import derive_edges_for_note as build_edges_for_note
|
||||
except ImportError:
|
||||
try:
|
||||
from app.core.edges import build_edges_for_note
|
||||
except ImportError:
|
||||
# Fallback Mock
|
||||
logging.warning("Could not import edge derivation logic. Edges will be empty.")
|
||||
def build_edges_for_note(*args, **kwargs): return []
|
||||
|
||||
from app.core.qdrant import QdrantConfig, get_client, ensure_collections, ensure_payload_indexes
|
||||
from app.core.qdrant_points import (
|
||||
points_for_chunks,
|
||||
points_for_note,
|
||||
points_for_edges,
|
||||
upsert_batch,
|
||||
)
|
||||
|
||||
# WICHTIG: Wir nutzen den API-Client für Embeddings (Async Support)
|
||||
from app.services.embeddings_client import EmbeddingsClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- Helper für Type-Registry ---
|
||||
def load_type_registry(custom_path: Optional[str] = None) -> dict:
|
||||
import yaml
|
||||
path = custom_path or os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
|
||||
if not os.path.exists(path):
|
||||
if os.path.exists("types.yaml"):
|
||||
path = "types.yaml"
|
||||
else:
|
||||
return {}
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def resolve_note_type(requested: Optional[str], reg: dict) -> str:
|
||||
types = reg.get("types", {})
|
||||
if requested and requested in types:
|
||||
return requested
|
||||
return "concept"
|
||||
|
||||
def effective_chunk_profile(note_type: str, reg: dict) -> str:
|
||||
t_cfg = reg.get("types", {}).get(note_type, {})
|
||||
if t_cfg and t_cfg.get("chunk_profile"):
|
||||
return t_cfg.get("chunk_profile")
|
||||
return reg.get("defaults", {}).get("chunk_profile", "default")
|
||||
|
||||
def effective_retriever_weight(note_type: str, reg: dict) -> float:
|
||||
t_cfg = reg.get("types", {}).get(note_type, {})
|
||||
if t_cfg and "retriever_weight" in t_cfg:
|
||||
return float(t_cfg["retriever_weight"])
|
||||
return float(reg.get("defaults", {}).get("retriever_weight", 1.0))
|
||||
|
||||
|
||||
class IngestionService:
|
||||
def __init__(self, collection_prefix: str = None):
|
||||
# Prefix Logik vereinheitlichen
|
||||
env_prefix = os.getenv("COLLECTION_PREFIX", "mindnet")
|
||||
self.prefix = collection_prefix or env_prefix
|
||||
|
||||
self.cfg = QdrantConfig.from_env()
|
||||
self.cfg.prefix = self.prefix
|
||||
self.client = get_client(self.cfg)
|
||||
self.dim = self.cfg.dim
|
||||
|
||||
# Registry laden
|
||||
self.registry = load_type_registry()
|
||||
|
||||
# Embedding Service initialisieren (Async Client)
|
||||
self.embedder = EmbeddingsClient()
|
||||
|
||||
# Init DB Checks (Fehler abfangen, falls DB nicht erreichbar)
|
||||
try:
|
||||
ensure_collections(self.client, self.prefix, self.dim)
|
||||
ensure_payload_indexes(self.client, self.prefix)
|
||||
except Exception as e:
|
||||
logger.warning(f"DB initialization warning: {e}")
|
||||
|
||||
async def process_file(
|
||||
self,
|
||||
file_path: str,
|
||||
vault_root: str,
|
||||
force_replace: bool = False,
|
||||
apply: bool = False,
|
||||
purge_before: bool = False,
|
||||
note_scope_refs: bool = False,
|
||||
hash_mode: str = "body",
|
||||
hash_source: str = "parsed",
|
||||
hash_normalize: str = "canonical"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Verarbeitet eine einzelne Datei (ASYNC Version).
|
||||
"""
|
||||
result = {
|
||||
"path": file_path,
|
||||
"status": "skipped",
|
||||
"changed": False,
|
||||
"error": None
|
||||
}
|
||||
|
||||
# 1. Parse & Frontmatter
|
||||
try:
|
||||
parsed = read_markdown(file_path)
|
||||
if not parsed:
|
||||
return {**result, "error": "Empty or unreadable file"}
|
||||
|
||||
fm = normalize_frontmatter(parsed.frontmatter)
|
||||
validate_required_frontmatter(fm)
|
||||
except Exception as e:
|
||||
logger.error(f"Validation failed for {file_path}: {e}")
|
||||
return {**result, "error": f"Validation failed: {str(e)}"}
|
||||
|
||||
# 2. Type & Config Resolution
|
||||
note_type = resolve_note_type(fm.get("type"), self.registry)
|
||||
fm["type"] = note_type
|
||||
fm["chunk_profile"] = effective_chunk_profile(note_type, self.registry)
|
||||
|
||||
weight = fm.get("retriever_weight")
|
||||
if weight is None:
|
||||
weight = effective_retriever_weight(note_type, self.registry)
|
||||
fm["retriever_weight"] = float(weight)
|
||||
|
||||
# 3. Build Note Payload
|
||||
try:
|
||||
note_pl = make_note_payload(
|
||||
parsed,
|
||||
vault_root=vault_root,
|
||||
hash_mode=hash_mode,
|
||||
hash_normalize=hash_normalize,
|
||||
hash_source=hash_source,
|
||||
file_path=file_path
|
||||
)
|
||||
if not note_pl.get("fulltext"):
|
||||
note_pl["fulltext"] = getattr(parsed, "body", "") or ""
|
||||
note_pl["retriever_weight"] = fm["retriever_weight"]
|
||||
|
||||
note_id = note_pl["note_id"]
|
||||
except Exception as e:
|
||||
logger.error(f"Payload build failed: {e}")
|
||||
return {**result, "error": f"Payload build failed: {str(e)}"}
|
||||
|
||||
# 4. Change Detection
|
||||
old_payload = None
|
||||
if not force_replace:
|
||||
old_payload = self._fetch_note_payload(note_id)
|
||||
|
||||
has_old = old_payload is not None
|
||||
key_current = f"{hash_mode}:{hash_source}:{hash_normalize}"
|
||||
old_hash = (old_payload or {}).get("hashes", {}).get(key_current)
|
||||
new_hash = note_pl.get("hashes", {}).get(key_current)
|
||||
|
||||
hash_changed = (old_hash != new_hash)
|
||||
chunks_missing, edges_missing = self._artifacts_missing(note_id)
|
||||
|
||||
should_write = force_replace or (not has_old) or hash_changed or chunks_missing or edges_missing
|
||||
|
||||
if not should_write:
|
||||
return {**result, "status": "unchanged", "note_id": note_id}
|
||||
|
||||
if not apply:
|
||||
return {**result, "status": "dry-run", "changed": True, "note_id": note_id}
|
||||
|
||||
# 5. Processing (Chunking, Embedding, Edges)
|
||||
try:
|
||||
body_text = getattr(parsed, "body", "") or ""
|
||||
chunks = assemble_chunks(fm["id"], body_text, fm["type"])
|
||||
chunk_pls = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text)
|
||||
|
||||
# --- EMBEDDING FIX (ASYNC) ---
|
||||
vecs = []
|
||||
if chunk_pls:
|
||||
texts = [c.get("window") or c.get("text") or "" for c in chunk_pls]
|
||||
try:
|
||||
# Async Aufruf des Embedders (via Batch oder Loop)
|
||||
if hasattr(self.embedder, 'embed_documents'):
|
||||
vecs = await self.embedder.embed_documents(texts)
|
||||
else:
|
||||
# Fallback Loop falls Client kein Batch unterstützt
|
||||
for t in texts:
|
||||
v = await self.embedder.embed_query(t)
|
||||
vecs.append(v)
|
||||
|
||||
# Validierung der Dimensionen
|
||||
if vecs and len(vecs) > 0:
|
||||
dim_got = len(vecs[0])
|
||||
if dim_got != self.dim:
|
||||
# Wirf keinen Fehler, aber logge Warnung. Qdrant Upsert wird failen wenn 0.
|
||||
logger.warning(f"Vector dimension mismatch. Expected {self.dim}, got {dim_got}")
|
||||
if dim_got == 0:
|
||||
raise ValueError("Embedding returned empty vectors (Dim 0)")
|
||||
except Exception as e:
|
||||
logger.error(f"Embedding generation failed: {e}")
|
||||
raise RuntimeError(f"Embedding failed: {e}")
|
||||
|
||||
# Edges
|
||||
note_refs = note_pl.get("references") or []
|
||||
# Versuche flexible Signatur für Edges (V1 vs V2)
|
||||
try:
|
||||
edges = build_edges_for_note(
|
||||
note_id,
|
||||
chunk_pls,
|
||||
note_level_references=note_refs,
|
||||
include_note_scope_refs=note_scope_refs
|
||||
)
|
||||
except TypeError:
|
||||
# Fallback für ältere Signatur
|
||||
edges = build_edges_for_note(note_id, chunk_pls)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Processing failed: {e}", exc_info=True)
|
||||
return {**result, "error": f"Processing failed: {str(e)}"}
|
||||
|
||||
# 6. Upsert Action
|
||||
try:
|
||||
if purge_before and has_old:
|
||||
self._purge_artifacts(note_id)
|
||||
|
||||
n_name, n_pts = points_for_note(self.prefix, note_pl, None, self.dim)
|
||||
upsert_batch(self.client, n_name, n_pts)
|
||||
|
||||
if chunk_pls and vecs:
|
||||
c_name, c_pts = points_for_chunks(self.prefix, chunk_pls, vecs)
|
||||
upsert_batch(self.client, c_name, c_pts)
|
||||
|
||||
if edges:
|
||||
e_name, e_pts = points_for_edges(self.prefix, edges)
|
||||
upsert_batch(self.client, e_name, e_pts)
|
||||
|
||||
return {
|
||||
"path": file_path,
|
||||
"status": "success",
|
||||
"changed": True,
|
||||
"note_id": note_id,
|
||||
"chunks_count": len(chunk_pls),
|
||||
"edges_count": len(edges)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Upsert failed: {e}", exc_info=True)
|
||||
return {**result, "error": f"DB Upsert failed: {e}"}
|
||||
|
||||
# --- Interne Qdrant Helper ---
|
||||
|
||||
def _fetch_note_payload(self, note_id: str) -> Optional[dict]:
|
||||
from qdrant_client.http import models as rest
|
||||
col = f"{self.prefix}_notes"
|
||||
try:
|
||||
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
|
||||
pts, _ = self.client.scroll(collection_name=col, scroll_filter=f, limit=1, with_payload=True)
|
||||
return pts[0].payload if pts else None
|
||||
except: return None
|
||||
|
||||
def _artifacts_missing(self, note_id: str) -> Tuple[bool, bool]:
|
||||
from qdrant_client.http import models as rest
|
||||
c_col = f"{self.prefix}_chunks"
|
||||
e_col = f"{self.prefix}_edges"
|
||||
try:
|
||||
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
|
||||
c_pts, _ = self.client.scroll(collection_name=c_col, scroll_filter=f, limit=1)
|
||||
e_pts, _ = self.client.scroll(collection_name=e_col, scroll_filter=f, limit=1)
|
||||
return (not bool(c_pts)), (not bool(e_pts))
|
||||
except: return True, True
|
||||
|
||||
def _purge_artifacts(self, note_id: str):
|
||||
from qdrant_client.http import models as rest
|
||||
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
|
||||
selector = rest.FilterSelector(filter=f)
|
||||
for suffix in ["chunks", "edges"]:
|
||||
try:
|
||||
self.client.delete(collection_name=f"{self.prefix}_{suffix}", points_selector=selector)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def create_from_text(
|
||||
self,
|
||||
markdown_content: str,
|
||||
filename: str,
|
||||
vault_root: str,
|
||||
folder: str = "00_Inbox"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
WP-11 Persistence API Entrypoint.
|
||||
Schreibt Text in Vault und indiziert ihn sofort.
|
||||
"""
|
||||
# 1. Zielordner
|
||||
target_dir = os.path.join(vault_root, folder)
|
||||
try:
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": f"Could not create folder {target_dir}: {e}"}
|
||||
|
||||
# 2. Dateiname
|
||||
safe_filename = os.path.basename(filename)
|
||||
if not safe_filename.endswith(".md"):
|
||||
safe_filename += ".md"
|
||||
file_path = os.path.join(target_dir, safe_filename)
|
||||
|
||||
# 3. Schreiben
|
||||
try:
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(markdown_content)
|
||||
logger.info(f"Written file to {file_path}")
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": f"Disk write failed at {file_path}: {str(e)}"}
|
||||
|
||||
# 4. Indizieren (Async Aufruf!)
|
||||
# Wir rufen process_file auf, das jetzt ASYNC ist
|
||||
return await self.process_file(
|
||||
file_path=file_path,
|
||||
vault_root=vault_root,
|
||||
apply=True,
|
||||
force_replace=True,
|
||||
purge_before=True
|
||||
)
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Modul: app/core/note_payload.py
|
||||
Version: 2.0.0
|
||||
Version: 2.1.0 (WP-11 Update: Aliases support)
|
||||
|
||||
Zweck
|
||||
-----
|
||||
|
|
@ -145,6 +145,7 @@ def make_note_payload(note: Any, *args, **kwargs) -> Dict[str, Any]:
|
|||
- retriever_weight: effektives Gewicht für den Retriever
|
||||
- chunk_profile: Profil für Chunking (short|medium|long|default|...)
|
||||
- edge_defaults: Liste von Kanten-Typen, die als Defaults gelten
|
||||
- aliases: Liste von Synonymen (WP-11)
|
||||
"""
|
||||
n = _as_dict(note)
|
||||
path_arg, types_cfg_explicit = _pick_args(*args, **kwargs)
|
||||
|
|
@ -214,12 +215,21 @@ def make_note_payload(note: Any, *args, **kwargs) -> Dict[str, Any]:
|
|||
if tags:
|
||||
payload["tags"] = _ensure_list(tags)
|
||||
|
||||
# WP-11: Aliases übernehmen (für Discovery Service)
|
||||
aliases = fm.get("aliases")
|
||||
if aliases:
|
||||
payload["aliases"] = _ensure_list(aliases)
|
||||
|
||||
# Zeitliche Metadaten (sofern vorhanden)
|
||||
for k in ("created", "modified", "date"):
|
||||
v = fm.get(k) or n.get(k)
|
||||
if v:
|
||||
payload[k] = str(v)
|
||||
|
||||
# Fulltext (Fallback, falls body im Input)
|
||||
if "body" in n and n["body"]:
|
||||
payload["fulltext"] = str(n["body"])
|
||||
|
||||
# JSON-Roundtrip zur harten Validierung (ASCII beibehalten)
|
||||
json.loads(json.dumps(payload, ensure_ascii=False))
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ load_dotenv()
|
|||
API_BASE_URL = os.getenv("MINDNET_API_URL", "http://localhost:8002")
|
||||
CHAT_ENDPOINT = f"{API_BASE_URL}/chat"
|
||||
FEEDBACK_ENDPOINT = f"{API_BASE_URL}/feedback"
|
||||
INGEST_ANALYZE_ENDPOINT = f"{API_BASE_URL}/ingest/analyze"
|
||||
INGEST_SAVE_ENDPOINT = f"{API_BASE_URL}/ingest/save"
|
||||
HISTORY_FILE = Path("data/logs/search_history.jsonl")
|
||||
|
||||
# Timeout Strategy
|
||||
|
|
@ -21,7 +23,7 @@ timeout_setting = os.getenv("MINDNET_API_TIMEOUT") or os.getenv("MINDNET_LLM_TIM
|
|||
API_TIMEOUT = float(timeout_setting) if timeout_setting else 300.0
|
||||
|
||||
# --- PAGE SETUP ---
|
||||
st.set_page_config(page_title="mindnet v2.3.2", page_icon="🧠", layout="wide")
|
||||
st.set_page_config(page_title="mindnet v2.3.10", page_icon="🧠", layout="wide")
|
||||
|
||||
# --- CSS STYLING ---
|
||||
st.markdown("""
|
||||
|
|
@ -52,10 +54,13 @@ st.markdown("""
|
|||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
font-size: 0.7rem;
|
||||
color: #888;
|
||||
margin-bottom: 5px;
|
||||
.suggestion-card {
|
||||
border-left: 3px solid #1a73e8;
|
||||
background-color: #ffffff;
|
||||
padding: 10px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
|
@ -67,20 +72,14 @@ if "user_id" not in st.session_state: st.session_state.user_id = str(uuid.uuid4(
|
|||
# --- HELPER FUNCTIONS ---
|
||||
|
||||
def normalize_meta_and_body(meta, body):
|
||||
"""
|
||||
Sanitizer: Stellt sicher, dass nur erlaubte Felder im Frontmatter bleiben.
|
||||
Alles andere wird in den Body verschoben (Repair-Strategie).
|
||||
"""
|
||||
"""Sanitizer: Stellt sicher, dass nur erlaubte Felder im Frontmatter bleiben."""
|
||||
ALLOWED_KEYS = {"title", "type", "status", "tags", "id", "created", "updated", "aliases", "lang"}
|
||||
|
||||
clean_meta = {}
|
||||
extra_content = []
|
||||
|
||||
# 1. Title/Titel Normalisierung
|
||||
if "titel" in meta and "title" not in meta:
|
||||
meta["title"] = meta.pop("titel")
|
||||
|
||||
# 2. Tags Normalisierung (Synonyme)
|
||||
tag_candidates = ["tags", "emotionale_keywords", "keywords", "schluesselwoerter"]
|
||||
all_tags = []
|
||||
for key in tag_candidates:
|
||||
|
|
@ -89,14 +88,12 @@ def normalize_meta_and_body(meta, body):
|
|||
if isinstance(val, list): all_tags.extend(val)
|
||||
elif isinstance(val, str): all_tags.extend([t.strip() for t in val.split(",")])
|
||||
|
||||
# 3. Filterung und Verschiebung
|
||||
for key, val in meta.items():
|
||||
if key in ALLOWED_KEYS:
|
||||
clean_meta[key] = val
|
||||
elif key in tag_candidates:
|
||||
pass # Schon oben behandelt
|
||||
pass
|
||||
else:
|
||||
# Unerlaubtes Feld (z.B. 'situation') -> Ab in den Body!
|
||||
if val and isinstance(val, str):
|
||||
header = key.replace("_", " ").title()
|
||||
extra_content.append(f"## {header}\n{val}\n")
|
||||
|
|
@ -104,7 +101,6 @@ def normalize_meta_and_body(meta, body):
|
|||
if all_tags:
|
||||
clean_meta["tags"] = list(set(all_tags))
|
||||
|
||||
# 4. Body Zusammenbau
|
||||
if extra_content:
|
||||
new_section = "\n".join(extra_content)
|
||||
final_body = f"{new_section}\n{body}"
|
||||
|
|
@ -114,18 +110,14 @@ def normalize_meta_and_body(meta, body):
|
|||
return clean_meta, final_body
|
||||
|
||||
def parse_markdown_draft(full_text):
|
||||
"""
|
||||
Robustes Parsing + Sanitization.
|
||||
"""
|
||||
"""Robustes Parsing + Sanitization."""
|
||||
clean_text = full_text
|
||||
|
||||
# Codeblock entfernen
|
||||
pattern_block = r"```(?:markdown|md)?\s*(.*?)\s*```"
|
||||
match_block = re.search(pattern_block, full_text, re.DOTALL | re.IGNORECASE)
|
||||
if match_block:
|
||||
clean_text = match_block.group(1).strip()
|
||||
|
||||
# Frontmatter splitten
|
||||
parts = re.split(r"^---+\s*$", clean_text, maxsplit=2, flags=re.MULTILINE)
|
||||
|
||||
meta = {}
|
||||
|
|
@ -152,7 +144,6 @@ def build_markdown_doc(meta, body):
|
|||
|
||||
meta["updated"] = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# Sortierung für UX
|
||||
ordered_meta = {}
|
||||
prio_keys = ["id", "type", "title", "status", "tags"]
|
||||
for k in prio_keys:
|
||||
|
|
@ -183,6 +174,8 @@ def load_history_from_logs(limit=10):
|
|||
except: pass
|
||||
return queries
|
||||
|
||||
# --- API CLIENT ---
|
||||
|
||||
def send_chat_message(message: str, top_k: int, explain: bool):
|
||||
try:
|
||||
response = requests.post(
|
||||
|
|
@ -195,6 +188,32 @@ def send_chat_message(message: str, top_k: int, explain: bool):
|
|||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def analyze_draft_text(text: str, n_type: str):
|
||||
"""Ruft den neuen Intelligence-Service (WP-11) auf."""
|
||||
try:
|
||||
response = requests.post(
|
||||
INGEST_ANALYZE_ENDPOINT,
|
||||
json={"text": text, "type": n_type},
|
||||
timeout=15
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def save_draft_to_vault(markdown_content: str, filename: str = None):
|
||||
"""Ruft den neuen Persistence-Service (WP-11) auf."""
|
||||
try:
|
||||
response = requests.post(
|
||||
INGEST_SAVE_ENDPOINT,
|
||||
json={"markdown_content": markdown_content, "filename": filename},
|
||||
timeout=60
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def submit_feedback(query_id, node_id, score, comment=None):
|
||||
try:
|
||||
requests.post(FEEDBACK_ENDPOINT, json={"query_id": query_id, "node_id": node_id, "score": score, "comment": comment}, timeout=2)
|
||||
|
|
@ -206,7 +225,7 @@ def submit_feedback(query_id, node_id, score, comment=None):
|
|||
def render_sidebar():
|
||||
with st.sidebar:
|
||||
st.title("🧠 mindnet")
|
||||
st.caption("v2.3.2 | WP-10 UI")
|
||||
st.caption("v2.3.10 | Mode Switch Fix")
|
||||
mode = st.radio("Modus", ["💬 Chat", "📝 Manueller Editor"], index=0)
|
||||
st.divider()
|
||||
st.subheader("⚙️ Settings")
|
||||
|
|
@ -221,64 +240,157 @@ def render_sidebar():
|
|||
return mode, top_k, explain
|
||||
|
||||
def render_draft_editor(msg):
|
||||
qid = msg.get('query_id', str(uuid.uuid4()))
|
||||
# Ensure ID Stability
|
||||
if "query_id" not in msg or not msg["query_id"]:
|
||||
msg["query_id"] = str(uuid.uuid4())
|
||||
|
||||
qid = msg["query_id"]
|
||||
key_base = f"draft_{qid}"
|
||||
|
||||
# 1. Init
|
||||
# State Keys
|
||||
data_meta_key = f"{key_base}_data_meta"
|
||||
data_sugg_key = f"{key_base}_data_suggestions"
|
||||
widget_body_key = f"{key_base}_widget_body"
|
||||
data_body_key = f"{key_base}_data_body"
|
||||
|
||||
# --- 1. INIT STATE (Nur beim allerersten Laden der Message) ---
|
||||
if f"{key_base}_init" not in st.session_state:
|
||||
meta, body = parse_markdown_draft(msg["content"])
|
||||
if "type" not in meta: meta["type"] = "default"
|
||||
if "title" not in meta: meta["title"] = ""
|
||||
tags = meta.get("tags", [])
|
||||
meta["tags_str"] = ", ".join(tags) if isinstance(tags, list) else str(tags)
|
||||
|
||||
st.session_state[f"{key_base}_type"] = meta.get("type", "default")
|
||||
st.session_state[f"{key_base}_title"] = meta.get("title", "")
|
||||
# Persistent Data (Source of Truth)
|
||||
st.session_state[data_meta_key] = meta
|
||||
st.session_state[data_sugg_key] = []
|
||||
st.session_state[data_body_key] = body.strip()
|
||||
|
||||
tags_raw = meta.get("tags", [])
|
||||
st.session_state[f"{key_base}_tags"] = ", ".join(tags_raw) if isinstance(tags_raw, list) else str(tags_raw)
|
||||
|
||||
st.session_state[f"{key_base}_body"] = body.strip()
|
||||
st.session_state[f"{key_base}_meta"] = meta
|
||||
st.session_state[f"{key_base}_init"] = True
|
||||
|
||||
# 2. UI
|
||||
# --- 2. RESURRECTION FIX (WICHTIG!) ---
|
||||
# Wenn wir vom Manuellen Editor zurückkommen, wurde der widget_key von Streamlit gelöscht.
|
||||
# Wir müssen ihn aus dem persistenten data_body_key wiederherstellen.
|
||||
if widget_body_key not in st.session_state and data_body_key in st.session_state:
|
||||
st.session_state[widget_body_key] = st.session_state[data_body_key]
|
||||
|
||||
# --- CALLBACKS ---
|
||||
def _sync_body():
|
||||
# Sync Widget -> Data (Source of Truth)
|
||||
st.session_state[data_body_key] = st.session_state[widget_body_key]
|
||||
|
||||
def _insert_text(text_to_insert):
|
||||
# Insert in Widget Key und Sync Data
|
||||
current = st.session_state.get(widget_body_key, "")
|
||||
new_text = f"{current}\n\n{text_to_insert}"
|
||||
st.session_state[widget_body_key] = new_text
|
||||
st.session_state[data_body_key] = new_text
|
||||
|
||||
def _remove_text(text_to_remove):
|
||||
current = st.session_state.get(widget_body_key, "")
|
||||
new_text = current.replace(text_to_remove, "").strip()
|
||||
st.session_state[widget_body_key] = new_text
|
||||
st.session_state[data_body_key] = new_text
|
||||
|
||||
# --- UI LAYOUT ---
|
||||
st.markdown(f'<div class="draft-box">', unsafe_allow_html=True)
|
||||
st.markdown("### 📝 Entwurf bearbeiten")
|
||||
|
||||
# Metadata
|
||||
# Metadata Form
|
||||
meta_ref = st.session_state[data_meta_key]
|
||||
c1, c2 = st.columns([2, 1])
|
||||
with c1:
|
||||
new_title = st.text_input("Titel", value=st.session_state.get(f"{key_base}_title", ""), key=f"{key_base}_inp_title")
|
||||
with c2:
|
||||
known_types = ["concept", "project", "decision", "experience", "journal", "person", "value", "goal", "principle", "default"]
|
||||
curr_type = st.session_state.get(f"{key_base}_type", "default")
|
||||
if curr_type not in known_types: known_types.append(curr_type)
|
||||
new_type = st.selectbox("Typ", known_types, index=known_types.index(curr_type), key=f"{key_base}_sel_type")
|
||||
# Auch hier Keys für Widgets nutzen, um Resets zu vermeiden
|
||||
title_key = f"{key_base}_wdg_title"
|
||||
if title_key not in st.session_state: st.session_state[title_key] = meta_ref["title"]
|
||||
meta_ref["title"] = st.text_input("Titel", key=title_key)
|
||||
|
||||
new_tags = st.text_input("Tags (kommagetrennt)", value=st.session_state.get(f"{key_base}_tags", ""), key=f"{key_base}_inp_tags")
|
||||
with c2:
|
||||
known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle"]
|
||||
curr = meta_ref["type"]
|
||||
if curr not in known_types: known_types.append(curr)
|
||||
type_key = f"{key_base}_wdg_type"
|
||||
if type_key not in st.session_state: st.session_state[type_key] = meta_ref["type"]
|
||||
meta_ref["type"] = st.selectbox("Typ", known_types, index=known_types.index(curr) if curr in known_types else 0, key=type_key)
|
||||
|
||||
tags_key = f"{key_base}_wdg_tags"
|
||||
if tags_key not in st.session_state: st.session_state[tags_key] = meta_ref.get("tags_str", "")
|
||||
meta_ref["tags_str"] = st.text_input("Tags", key=tags_key)
|
||||
|
||||
# Tabs
|
||||
tab_edit, tab_view = st.tabs(["✏️ Inhalt", "👁️ Vorschau"])
|
||||
tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"])
|
||||
|
||||
# --- TAB 1: EDITOR ---
|
||||
with tab_edit:
|
||||
st.caption("Bearbeite hier den Inhalt. Metadaten (oben) werden automatisch hinzugefügt.")
|
||||
new_body = st.text_area(
|
||||
# Hier kein 'value' Argument mehr, da wir den Key oben (Resurrection) initialisiert haben.
|
||||
st.text_area(
|
||||
"Body",
|
||||
value=st.session_state.get(f"{key_base}_body", ""),
|
||||
key=widget_body_key,
|
||||
height=500,
|
||||
key=f"{key_base}_txt_body",
|
||||
on_change=_sync_body,
|
||||
label_visibility="collapsed"
|
||||
)
|
||||
|
||||
# Reassembly
|
||||
final_tags_list = [t.strip() for t in new_tags.split(",") if t.strip()]
|
||||
final_meta = st.session_state.get(f"{key_base}_meta", {}).copy()
|
||||
final_meta.update({
|
||||
"id": "generated_on_save",
|
||||
"type": new_type,
|
||||
"title": new_title,
|
||||
"status": "draft",
|
||||
"tags": final_tags_list
|
||||
})
|
||||
# --- TAB 2: INTELLIGENCE ---
|
||||
with tab_intel:
|
||||
st.info("Klicke auf 'Analysieren', um Verknüpfungen für den AKTUELLEN Text zu finden.")
|
||||
|
||||
final_doc = build_markdown_doc(final_meta, new_body)
|
||||
if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"):
|
||||
st.session_state[data_sugg_key] = []
|
||||
|
||||
# Lese vom Widget (aktuell) oder Data (Fallback)
|
||||
text_to_analyze = st.session_state.get(widget_body_key, st.session_state.get(data_body_key, ""))
|
||||
|
||||
with st.spinner("Analysiere..."):
|
||||
analysis = analyze_draft_text(text_to_analyze, meta_ref["type"])
|
||||
|
||||
if "error" in analysis:
|
||||
st.error(f"Fehler: {analysis['error']}")
|
||||
else:
|
||||
suggestions = analysis.get("suggestions", [])
|
||||
st.session_state[data_sugg_key] = suggestions
|
||||
if not suggestions:
|
||||
st.warning("Keine Vorschläge gefunden.")
|
||||
else:
|
||||
st.success(f"{len(suggestions)} Vorschläge gefunden.")
|
||||
|
||||
# Render List
|
||||
suggestions = st.session_state[data_sugg_key]
|
||||
if suggestions:
|
||||
current_text_state = st.session_state.get(widget_body_key, "")
|
||||
|
||||
for idx, sugg in enumerate(suggestions):
|
||||
link_text = sugg.get('suggested_markdown', '')
|
||||
is_inserted = link_text in current_text_state
|
||||
|
||||
bg_color = "#e6fffa" if is_inserted else "#ffffff"
|
||||
border = "3px solid #28a745" if is_inserted else "3px solid #1a73e8"
|
||||
|
||||
st.markdown(f"""
|
||||
<div style="border-left: {border}; background-color: {bg_color}; padding: 10px; margin-bottom: 8px; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
||||
<b>{sugg.get('target_title')}</b> <small>({sugg.get('type')})</small><br>
|
||||
<i>{sugg.get('reason')}</i><br>
|
||||
<code>{link_text}</code>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
if is_inserted:
|
||||
st.button("❌ Entfernen", key=f"del_{idx}_{key_base}", on_click=_remove_text, args=(link_text,))
|
||||
else:
|
||||
st.button("➕ Einfügen", key=f"add_{idx}_{key_base}", on_click=_insert_text, args=(link_text,))
|
||||
|
||||
# --- TAB 3: SAVE ---
|
||||
final_tags = [t.strip() for t in meta_ref["tags_str"].split(",") if t.strip()]
|
||||
final_meta = {
|
||||
"id": "generated_on_save",
|
||||
"type": meta_ref["type"],
|
||||
"title": meta_ref["title"],
|
||||
"status": "draft",
|
||||
"tags": final_tags
|
||||
}
|
||||
# Final Doc aus Data
|
||||
final_body = st.session_state.get(widget_body_key, st.session_state[data_body_key])
|
||||
final_doc = build_markdown_doc(final_meta, final_body)
|
||||
|
||||
with tab_view:
|
||||
st.markdown('<div class="preview-box">', unsafe_allow_html=True)
|
||||
|
|
@ -287,11 +399,19 @@ def render_draft_editor(msg):
|
|||
|
||||
st.markdown("---")
|
||||
|
||||
# Actions
|
||||
b1, b2 = st.columns([1, 1])
|
||||
with b1:
|
||||
fname = f"{datetime.now().strftime('%Y%m%d')}-{new_type}.md"
|
||||
st.download_button("💾 Download .md", data=final_doc, file_name=fname, mime="text/markdown")
|
||||
if st.button("💾 Speichern & Indizieren", type="primary", key=f"{key_base}_save"):
|
||||
with st.spinner("Speichere im Vault..."):
|
||||
safe_title = re.sub(r'[^a-zA-Z0-9]', '-', meta_ref["title"]).lower()[:30] or "draft"
|
||||
fname = f"{datetime.now().strftime('%Y%m%d')}-{safe_title}.md"
|
||||
|
||||
result = save_draft_to_vault(final_doc, filename=fname)
|
||||
if "error" in result:
|
||||
st.error(f"Fehler: {result['error']}")
|
||||
else:
|
||||
st.success(f"Gespeichert: {result.get('file_path')}")
|
||||
st.balloons()
|
||||
with b2:
|
||||
if st.button("📋 Code anzeigen", key=f"{key_base}_btn_copy"):
|
||||
st.code(final_doc, language="markdown")
|
||||
|
|
@ -303,13 +423,12 @@ def render_chat_interface(top_k, explain):
|
|||
for idx, msg in enumerate(st.session_state.messages):
|
||||
with st.chat_message(msg["role"]):
|
||||
if msg["role"] == "assistant":
|
||||
# Meta
|
||||
# Header
|
||||
intent = msg.get("intent", "UNKNOWN")
|
||||
src = msg.get("intent_source", "?")
|
||||
icon = {"EMPATHY":"❤️", "DECISION":"⚖️", "CODING":"💻", "FACT":"📚", "INTERVIEW":"📝"}.get(intent, "🧠")
|
||||
st.markdown(f'<div class="intent-badge">{icon} Intent: {intent} <span style="opacity:0.6; font-size:0.8em">({src})</span></div>', unsafe_allow_html=True)
|
||||
|
||||
# Debugging (Always visible for safety)
|
||||
with st.expander("🐞 Debug Raw Payload", expanded=False):
|
||||
st.json(msg)
|
||||
|
||||
|
|
@ -359,15 +478,13 @@ def render_chat_interface(top_k, explain):
|
|||
st.rerun()
|
||||
|
||||
def render_manual_editor():
|
||||
st.header("📝 Manueller Editor")
|
||||
c1, c2 = st.columns([1, 2])
|
||||
n_type = c1.selectbox("Typ", ["concept", "project", "decision", "experience", "value", "goal"])
|
||||
tags = c2.text_input("Tags")
|
||||
body = st.text_area("Inhalt", height=400, placeholder="# Titel\n\nText...")
|
||||
if st.button("Code anzeigen"):
|
||||
meta = {"type": n_type, "status": "draft", "tags": [t.strip() for t in tags.split(",")]}
|
||||
st.code(build_markdown_doc(meta, body), language="markdown")
|
||||
mock_msg = {
|
||||
"content": "---\ntype: concept\ntitle: Neue Notiz\nstatus: draft\ntags: []\n---\n# Titel\n",
|
||||
"query_id": "manual_mode_v2"
|
||||
}
|
||||
render_draft_editor(mock_msg)
|
||||
|
||||
# --- MAIN ---
|
||||
mode, top_k, explain = render_sidebar()
|
||||
if mode == "💬 Chat":
|
||||
render_chat_interface(top_k, explain)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ from .routers.tools import router as tools_router
|
|||
from .routers.feedback import router as feedback_router
|
||||
# NEU: Chat Router (WP-05)
|
||||
from .routers.chat import router as chat_router
|
||||
# NEU: Ingest Router (WP-11)
|
||||
from .routers.ingest import router as ingest_router
|
||||
|
||||
try:
|
||||
from .routers.admin import router as admin_router
|
||||
|
|
@ -20,7 +22,7 @@ except Exception:
|
|||
admin_router = None
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
app = FastAPI(title="mindnet API", version="0.5.0") # Version bump WP-05
|
||||
app = FastAPI(title="mindnet API", version="0.6.0") # Version bump WP-11
|
||||
s = get_settings()
|
||||
|
||||
@app.get("/healthz")
|
||||
|
|
@ -38,6 +40,9 @@ def create_app() -> FastAPI:
|
|||
# NEU: Chat Endpoint
|
||||
app.include_router(chat_router, prefix="/chat", tags=["chat"])
|
||||
|
||||
# NEU: Ingest Endpoint
|
||||
app.include_router(ingest_router, prefix="/ingest", tags=["ingest"])
|
||||
|
||||
if admin_router:
|
||||
app.include_router(admin_router, prefix="/admin", tags=["admin"])
|
||||
|
||||
|
|
|
|||
89
app/routers/ingest.py
Normal file
89
app/routers/ingest.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""
|
||||
app/routers/ingest.py
|
||||
API-Endpunkte für WP-11 (Discovery & Persistence).
|
||||
Delegiert an Services.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from app.core.ingestion import IngestionService
|
||||
from app.services.discovery import DiscoveryService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
# Services Init (Global oder via Dependency Injection)
|
||||
discovery_service = DiscoveryService()
|
||||
|
||||
class AnalyzeRequest(BaseModel):
|
||||
text: str
|
||||
type: str = "concept"
|
||||
|
||||
class SaveRequest(BaseModel):
|
||||
markdown_content: str
|
||||
filename: Optional[str] = None
|
||||
folder: str = "00_Inbox"
|
||||
|
||||
class SaveResponse(BaseModel):
|
||||
status: str
|
||||
file_path: str
|
||||
note_id: str
|
||||
stats: Dict[str, Any]
|
||||
|
||||
@router.post("/analyze")
|
||||
async def analyze_draft(req: AnalyzeRequest):
|
||||
"""
|
||||
WP-11 Intelligence: Liefert Link-Vorschläge via DiscoveryService.
|
||||
"""
|
||||
try:
|
||||
# Hier rufen wir jetzt den verbesserten Service auf
|
||||
result = await discovery_service.analyze_draft(req.text, req.type)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Analyze failed: {e}", exc_info=True)
|
||||
return {"suggestions": [], "error": str(e)}
|
||||
|
||||
@router.post("/save", response_model=SaveResponse)
|
||||
async def save_note(req: SaveRequest):
|
||||
"""
|
||||
WP-11 Persistence: Speichert und indiziert.
|
||||
"""
|
||||
try:
|
||||
vault_root = os.getenv("MINDNET_VAULT_ROOT", "./vault")
|
||||
abs_vault_root = os.path.abspath(vault_root)
|
||||
|
||||
if not os.path.exists(abs_vault_root):
|
||||
try: os.makedirs(abs_vault_root, exist_ok=True)
|
||||
except: pass
|
||||
|
||||
final_filename = req.filename or f"draft_{int(time.time())}.md"
|
||||
ingest_service = IngestionService()
|
||||
|
||||
# Async Call
|
||||
result = await ingest_service.create_from_text(
|
||||
markdown_content=req.markdown_content,
|
||||
filename=final_filename,
|
||||
vault_root=abs_vault_root,
|
||||
folder=req.folder
|
||||
)
|
||||
|
||||
if result.get("status") == "error":
|
||||
raise HTTPException(status_code=500, detail=result.get("error"))
|
||||
|
||||
return SaveResponse(
|
||||
status="success",
|
||||
file_path=result.get("path", "unknown"),
|
||||
note_id=result.get("note_id", "unknown"),
|
||||
stats={
|
||||
"chunks": result.get("chunks_count", 0),
|
||||
"edges": result.get("edges_count", 0)
|
||||
}
|
||||
)
|
||||
except HTTPException as he: raise he
|
||||
except Exception as e:
|
||||
logger.error(f"Save failed: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Save failed: {str(e)}")
|
||||
255
app/services/discovery.py
Normal file
255
app/services/discovery.py
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
"""
|
||||
app/services/discovery.py
|
||||
Service für Link-Vorschläge und Knowledge-Discovery (WP-11).
|
||||
|
||||
Features:
|
||||
- Sliding Window Analyse für lange Texte.
|
||||
- Footer-Scan für Projekt-Referenzen.
|
||||
- 'Matrix-Logic' für intelligente Kanten-Typen (Experience -> Value = based_on).
|
||||
- Async & Nomic-Embeddings kompatibel.
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import os
|
||||
from typing import List, Dict, Any, Optional, Set
|
||||
import yaml
|
||||
|
||||
from app.core.qdrant import QdrantConfig, get_client
|
||||
from app.models.dto import QueryRequest
|
||||
from app.core.retriever import hybrid_retrieve
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DiscoveryService:
|
||||
def __init__(self, collection_prefix: str = None):
|
||||
self.cfg = QdrantConfig.from_env()
|
||||
self.prefix = collection_prefix or self.cfg.prefix or "mindnet"
|
||||
self.client = get_client(self.cfg)
|
||||
self.registry = self._load_type_registry()
|
||||
|
||||
async def analyze_draft(self, text: str, current_type: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Analysiert den Text und liefert Vorschläge mit kontext-sensitiven Kanten-Typen.
|
||||
"""
|
||||
suggestions = []
|
||||
|
||||
# Fallback, falls keine spezielle Regel greift
|
||||
default_edge_type = self._get_default_edge_type(current_type)
|
||||
|
||||
# Tracking-Sets für Deduplizierung (Wir merken uns NOTE-IDs)
|
||||
seen_target_note_ids = set()
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 1. Exact Match: Titel/Aliases
|
||||
# ---------------------------------------------------------
|
||||
# Holt Titel, Aliases UND Typen aus dem Index
|
||||
known_entities = self._fetch_all_titles_and_aliases()
|
||||
found_entities = self._find_entities_in_text(text, known_entities)
|
||||
|
||||
for entity in found_entities:
|
||||
if entity["id"] in seen_target_note_ids:
|
||||
continue
|
||||
seen_target_note_ids.add(entity["id"])
|
||||
|
||||
# INTELLIGENTE KANTEN-LOGIK (MATRIX)
|
||||
target_type = entity.get("type", "concept")
|
||||
smart_edge = self._resolve_edge_type(current_type, target_type)
|
||||
|
||||
suggestions.append({
|
||||
"type": "exact_match",
|
||||
"text_found": entity["match"],
|
||||
"target_title": entity["title"],
|
||||
"target_id": entity["id"],
|
||||
"suggested_edge_type": smart_edge,
|
||||
"suggested_markdown": f"[[rel:{smart_edge} {entity['title']}]]",
|
||||
"confidence": 1.0,
|
||||
"reason": f"Exakter Treffer: '{entity['match']}' ({target_type})"
|
||||
})
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 2. Semantic Match: Sliding Window & Footer Focus
|
||||
# ---------------------------------------------------------
|
||||
search_queries = self._generate_search_queries(text)
|
||||
|
||||
# Async parallel abfragen
|
||||
tasks = [self._get_semantic_suggestions_async(q) for q in search_queries]
|
||||
results_list = await asyncio.gather(*tasks)
|
||||
|
||||
# Ergebnisse verarbeiten
|
||||
for hits in results_list:
|
||||
for hit in hits:
|
||||
note_id = hit.payload.get("note_id")
|
||||
if not note_id: continue
|
||||
|
||||
# Deduplizierung (Notiz-Ebene)
|
||||
if note_id in seen_target_note_ids:
|
||||
continue
|
||||
|
||||
# Score Check (Threshold 0.50 für nomic-embed-text)
|
||||
if hit.total_score > 0.50:
|
||||
seen_target_note_ids.add(note_id)
|
||||
|
||||
target_title = hit.payload.get("title") or "Unbekannt"
|
||||
|
||||
# INTELLIGENTE KANTEN-LOGIK (MATRIX)
|
||||
# Den Typ der gefundenen Notiz aus dem Payload lesen
|
||||
target_type = hit.payload.get("type", "concept")
|
||||
smart_edge = self._resolve_edge_type(current_type, target_type)
|
||||
|
||||
suggestions.append({
|
||||
"type": "semantic_match",
|
||||
"text_found": (hit.source.get("text") or "")[:60] + "...",
|
||||
"target_title": target_title,
|
||||
"target_id": note_id,
|
||||
"suggested_edge_type": smart_edge,
|
||||
"suggested_markdown": f"[[rel:{smart_edge} {target_title}]]",
|
||||
"confidence": round(hit.total_score, 2),
|
||||
"reason": f"Semantisch ähnlich zu {target_type} ({hit.total_score:.2f})"
|
||||
})
|
||||
|
||||
# Sortieren nach Confidence
|
||||
suggestions.sort(key=lambda x: x["confidence"], reverse=True)
|
||||
|
||||
return {
|
||||
"draft_length": len(text),
|
||||
"analyzed_windows": len(search_queries),
|
||||
"suggestions_count": len(suggestions),
|
||||
"suggestions": suggestions[:10]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Core Logic: Die Matrix
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def _resolve_edge_type(self, source_type: str, target_type: str) -> str:
|
||||
"""
|
||||
Entscheidungsmatrix für komplexe Verbindungen.
|
||||
Definiert, wie Typ A auf Typ B verlinken sollte.
|
||||
"""
|
||||
st = source_type.lower()
|
||||
tt = target_type.lower()
|
||||
|
||||
# Regeln für 'experience' (Erfahrungen)
|
||||
if st == "experience":
|
||||
if tt == "value": return "based_on"
|
||||
if tt == "principle": return "derived_from"
|
||||
if tt == "trip": return "part_of"
|
||||
if tt == "lesson": return "learned"
|
||||
if tt == "project": return "related_to" # oder belongs_to
|
||||
|
||||
# Regeln für 'project'
|
||||
if st == "project":
|
||||
if tt == "decision": return "depends_on"
|
||||
if tt == "concept": return "uses"
|
||||
if tt == "person": return "managed_by"
|
||||
|
||||
# Regeln für 'decision' (ADR)
|
||||
if st == "decision":
|
||||
if tt == "principle": return "compliant_with"
|
||||
if tt == "requirement": return "addresses"
|
||||
|
||||
# Fallback: Standard aus der types.yaml für den Source-Typ
|
||||
return self._get_default_edge_type(st)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Sliding Windows
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def _generate_search_queries(self, text: str) -> List[str]:
|
||||
"""
|
||||
Erzeugt intelligente Fenster + Footer Scan.
|
||||
"""
|
||||
text_len = len(text)
|
||||
if not text: return []
|
||||
|
||||
queries = []
|
||||
|
||||
# 1. Start / Gesamtkontext
|
||||
queries.append(text[:600])
|
||||
|
||||
# 2. Footer-Scan (Wichtig für "Projekt"-Referenzen am Ende)
|
||||
if text_len > 150:
|
||||
footer = text[-250:]
|
||||
if footer not in queries:
|
||||
queries.append(footer)
|
||||
|
||||
# 3. Sliding Window für lange Texte
|
||||
if text_len > 800:
|
||||
window_size = 500
|
||||
step = 1500
|
||||
for i in range(window_size, text_len - window_size, step):
|
||||
end_pos = min(i + window_size, text_len)
|
||||
chunk = text[i:end_pos]
|
||||
if len(chunk) > 100:
|
||||
queries.append(chunk)
|
||||
|
||||
return queries
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Standard Helpers
|
||||
# ---------------------------------------------------------
|
||||
|
||||
async def _get_semantic_suggestions_async(self, text: str):
|
||||
req = QueryRequest(query=text, top_k=5, explain=False)
|
||||
try:
|
||||
res = hybrid_retrieve(req)
|
||||
return res.results
|
||||
except Exception as e:
|
||||
logger.error(f"Semantic suggestion error: {e}")
|
||||
return []
|
||||
|
||||
def _load_type_registry(self) -> dict:
|
||||
path = os.getenv("MINDNET_TYPES_FILE", "config/types.yaml")
|
||||
if not os.path.exists(path):
|
||||
if os.path.exists("types.yaml"): path = "types.yaml"
|
||||
else: return {}
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {}
|
||||
except Exception: return {}
|
||||
|
||||
def _get_default_edge_type(self, note_type: str) -> str:
|
||||
types_cfg = self.registry.get("types", {})
|
||||
type_def = types_cfg.get(note_type, {})
|
||||
defaults = type_def.get("edge_defaults")
|
||||
return defaults[0] if defaults else "related_to"
|
||||
|
||||
def _fetch_all_titles_and_aliases(self) -> List[Dict]:
|
||||
notes = []
|
||||
next_page = None
|
||||
col = f"{self.prefix}_notes"
|
||||
try:
|
||||
while True:
|
||||
res, next_page = self.client.scroll(
|
||||
collection_name=col, limit=1000, offset=next_page,
|
||||
with_payload=True, with_vectors=False
|
||||
)
|
||||
for point in res:
|
||||
pl = point.payload or {}
|
||||
aliases = pl.get("aliases") or []
|
||||
if isinstance(aliases, str): aliases = [aliases]
|
||||
|
||||
notes.append({
|
||||
"id": pl.get("note_id"),
|
||||
"title": pl.get("title"),
|
||||
"aliases": aliases,
|
||||
"type": pl.get("type", "concept") # WICHTIG: Typ laden für Matrix
|
||||
})
|
||||
if next_page is None: break
|
||||
except Exception: pass
|
||||
return notes
|
||||
|
||||
def _find_entities_in_text(self, text: str, entities: List[Dict]) -> List[Dict]:
|
||||
found = []
|
||||
text_lower = text.lower()
|
||||
for entity in entities:
|
||||
# Title Check
|
||||
title = entity.get("title")
|
||||
if title and title.lower() in text_lower:
|
||||
found.append({"match": title, "title": title, "id": entity["id"], "type": entity["type"]})
|
||||
continue
|
||||
# Alias Check
|
||||
for alias in entity.get("aliases", []):
|
||||
if str(alias).lower() in text_lower:
|
||||
found.append({"match": alias, "title": title, "id": entity["id"], "type": entity["type"]})
|
||||
break
|
||||
return found
|
||||
|
|
@ -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 []
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Mindnet v2.4 – Admin Guide
|
||||
**Datei:** `docs/mindnet_admin_guide_v2.4.md`
|
||||
**Stand:** 2025-12-10
|
||||
**Status:** **FINAL** (Inkl. Frontend Deployment & Interview Config)
|
||||
**Stand:** 2025-12-11
|
||||
**Status:** **FINAL** (Inkl. Async Architecture & Nomic Model)
|
||||
**Quellen:** `Handbuch.md`, `mindnet_developer_guide_v2.4.md`.
|
||||
|
||||
> Dieses Handbuch richtet sich an **Administratoren**. Es beschreibt Installation, Konfiguration, Backup-Strategien, Monitoring und den sicheren Betrieb der Mindnet-Instanz (API + UI + DB).
|
||||
|
|
@ -23,7 +23,7 @@ Wir unterscheiden strikt zwischen:
|
|||
* **OS:** Linux (Ubuntu 22.04+ empfohlen) oder macOS.
|
||||
* **Runtime:** Python 3.10+, Docker (für Qdrant), Ollama (für LLM).
|
||||
* **Hardware:**
|
||||
* CPU: 4+ Cores empfohlen (für Import & Inference).
|
||||
* CPU: 4+ Cores empfohlen (für Async Import & Inference).
|
||||
* RAM: Min. 8GB empfohlen (4GB System + 4GB für Phi-3/Qdrant).
|
||||
* Disk: SSD empfohlen für Qdrant-Performance.
|
||||
|
||||
|
|
@ -37,11 +37,11 @@ Wir unterscheiden strikt zwischen:
|
|||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# 3. Dependencies installieren (inkl. Streamlit)
|
||||
# 3. Dependencies installieren (inkl. Streamlit, HTTPX)
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 4. Verzeichnisse anlegen
|
||||
mkdir -p logs qdrant_storage data/logs
|
||||
mkdir -p logs qdrant_storage data/logs vault
|
||||
|
||||
### 2.3 Qdrant Setup (Docker)
|
||||
Wir nutzen Qdrant als Vektor-Datenbank. Persistenz ist wichtig.
|
||||
|
|
@ -53,20 +53,25 @@ Wir nutzen Qdrant als Vektor-Datenbank. Persistenz ist wichtig.
|
|||
-v $(pwd)/qdrant_storage:/qdrant/storage \
|
||||
qdrant/qdrant
|
||||
|
||||
### 2.4 Ollama Setup (LLM Service)
|
||||
Mindnet benötigt einen lokalen LLM-Server für den Chat.
|
||||
### 2.4 Ollama Setup (LLM & Embeddings)
|
||||
Mindnet benötigt einen lokalen LLM-Server für Chat UND Embeddings.
|
||||
**WICHTIG (Update v2.3.10):** Es muss zwingend `nomic-embed-text` installiert sein, sonst startet der Import nicht.
|
||||
|
||||
# 1. Installieren (Linux Script)
|
||||
curl -fsSL [https://ollama.com/install.sh](https://ollama.com/install.sh) | sh
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# 2. Modell laden (Phi-3 Mini für CPU-Performance)
|
||||
ollama pull phi3:mini
|
||||
# 2. Modelle laden
|
||||
ollama pull phi3:mini # Für Chat/Reasoning
|
||||
ollama pull nomic-embed-text # Für Vektoren (768 Dim)
|
||||
|
||||
# 3. Testen
|
||||
curl http://localhost:11434/api/generate -d '{"model": "phi3:mini", "prompt":"Hi"}'
|
||||
|
||||
### 2.5 Konfiguration (ENV)
|
||||
Erstelle eine `.env` Datei im Root-Verzeichnis. Die neuen Settings für WP-06/WP-07 (Timeout, Decision Config) sind essenziell für stabilen Betrieb auf CPUs.
|
||||
Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM` und `MINDNET_EMBEDDING_MODEL`.
|
||||
|
||||
# Server Config
|
||||
UVICORN_HOST=0.0.0.0
|
||||
|
||||
# Qdrant Verbindung
|
||||
QDRANT_URL="http://localhost:6333"
|
||||
|
|
@ -74,15 +79,23 @@ Erstelle eine `.env` Datei im Root-Verzeichnis. Die neuen Settings für WP-06/WP
|
|||
# Mindnet Core Settings
|
||||
COLLECTION_PREFIX="mindnet"
|
||||
MINDNET_TYPES_FILE="./config/types.yaml"
|
||||
MINDNET_VAULT_ROOT="./vault"
|
||||
|
||||
# LLM / RAG Settings
|
||||
MINDNET_LLM_MODEL="phi3:mini"
|
||||
# WICHTIG: Dimension auf 768 setzen (für Nomic)
|
||||
VECTOR_DIM=768
|
||||
|
||||
# AI Modelle (Ollama)
|
||||
MINDNET_OLLAMA_URL="http://127.0.0.1:11434"
|
||||
MINDNET_LLM_MODEL="phi3:mini"
|
||||
MINDNET_EMBEDDING_MODEL="nomic-embed-text" # NEU
|
||||
|
||||
# Config & Timeouts
|
||||
# Timeouts (Erhöht für Async/Nomic)
|
||||
MINDNET_LLM_TIMEOUT=300.0
|
||||
MINDNET_API_TIMEOUT=60.0
|
||||
|
||||
# Configs
|
||||
MINDNET_PROMPTS_PATH="./config/prompts.yaml"
|
||||
MINDNET_DECISION_CONFIG="./config/decision_engine.yaml"
|
||||
MINDNET_LLM_TIMEOUT=300.0
|
||||
|
||||
### 2.6 Deployment via Systemd (Backend & Frontend)
|
||||
|
||||
|
|
@ -98,6 +111,7 @@ Mindnet benötigt zwei Services pro Umgebung: API (Uvicorn) und UI (Streamlit).
|
|||
User=llmadmin
|
||||
Group=llmadmin
|
||||
WorkingDirectory=/home/llmadmin/mindnet
|
||||
# Async Server Start
|
||||
ExecStart=/home/llmadmin/mindnet/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8001 --env-file .env
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
|
@ -141,11 +155,11 @@ Mindnet benötigt zwei Services pro Umgebung: API (Uvicorn) und UI (Streamlit).
|
|||
## 3. Betrieb im Alltag
|
||||
|
||||
### 3.1 Regelmäßige Importe
|
||||
Der Vault-Zustand sollte regelmäßig (z.B. stündlich per Cronjob) nach Qdrant synchronisiert werden.
|
||||
Der Vault-Zustand sollte regelmäßig (z.B. stündlich per Cronjob) nach Qdrant synchronisiert werden. Das Skript nutzt nun **AsyncIO** und eine Semaphore, um Ollama nicht zu überlasten.
|
||||
|
||||
**Cronjob-Beispiel (stündlich):**
|
||||
|
||||
0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault /path/to/vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1
|
||||
0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1
|
||||
|
||||
### 3.2 Health-Checks
|
||||
Prüfe regelmäßig, ob alle Komponenten laufen.
|
||||
|
|
@ -157,34 +171,28 @@ Prüfe regelmäßig, ob alle Komponenten laufen.
|
|||
### 3.3 Logs & Monitoring
|
||||
* **Backend Fehler:** `journalctl -u mindnet-prod -f`
|
||||
* **Frontend Fehler:** `journalctl -u mindnet-ui-prod -f`
|
||||
* Achte auf "Timeout"-Meldungen im Frontend, wenn das Backend zu langsam antwortet.
|
||||
* **LLM Fehler:** `journalctl -u ollama -f`
|
||||
* **Fachliche Logs:** `data/logs/search_history.jsonl`
|
||||
|
||||
---
|
||||
|
||||
## 4. Update-Prozess
|
||||
## 4. Troubleshooting (Update v2.4)
|
||||
|
||||
Wenn neue Versionen ausgerollt werden (Deployment):
|
||||
### "Vector dimension error: expected dim: 768, got 384"
|
||||
* **Ursache:** Du versuchst, in eine alte Qdrant-Collection (mit 384 Dim aus v2.2) neue Embeddings (mit 768 Dim von Nomic) zu schreiben.
|
||||
* **Lösung:** Full Reset erforderlich.
|
||||
1. `python -m scripts.reset_qdrant --mode wipe --prefix mindnet --yes` (Löscht DB).
|
||||
2. `python -m scripts.import_markdown ...` (Baut neu auf).
|
||||
|
||||
1. **Code aktualisieren:**
|
||||
### "500 Internal Server Error" beim Speichern
|
||||
* **Ursache:** Oft Timeout bei Ollama, wenn `nomic-embed-text` noch nicht im RAM geladen ist ("Cold Start").
|
||||
* **Lösung:**
|
||||
1. Sicherstellen, dass Modell existiert: `ollama list`.
|
||||
2. API neustarten (re-initialisiert Async Clients).
|
||||
|
||||
cd /home/llmadmin/mindnet
|
||||
git pull origin main
|
||||
|
||||
2. **Dependencies prüfen:**
|
||||
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
3. **Dienste neustarten (Zwingend!):**
|
||||
|
||||
sudo systemctl restart mindnet-prod
|
||||
sudo systemctl restart mindnet-ui-prod
|
||||
|
||||
4. **Schema-Migration (falls nötig):**
|
||||
|
||||
python3 -m scripts.import_markdown ... --apply
|
||||
### "NameError: name 'os' is not defined"
|
||||
* **Ursache:** Fehlender Import in Skripten nach Updates.
|
||||
* **Lösung:** `git pull` (Fix wurde in v2.3.10 deployed).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -202,16 +210,13 @@ Für schnelle Wiederherstellung des Suchindex.
|
|||
tar -czf qdrant_backup_$(date +%F).tar.gz ./qdrant_storage
|
||||
docker start mindnet_qdrant
|
||||
|
||||
### 5.3 Log-Daten (Priorität 3)
|
||||
Sichere den Ordner `data/logs/`. Verlust dieser Daten bedeutet Verlust des Trainingsmaterials für Self-Tuning.
|
||||
|
||||
### 5.4 Notfall-Wiederherstellung (Rebuild)
|
||||
Wenn die Datenbank korrupt ist:
|
||||
### 5.3 Notfall-Wiederherstellung (Rebuild)
|
||||
Wenn die Datenbank korrupt ist oder Modelle gewechselt werden:
|
||||
|
||||
# 1. DB komplett leeren (Wipe)
|
||||
python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes
|
||||
# 2. Alles neu importieren
|
||||
python3 -m scripts.import_markdown --vault /path/to/vault --prefix "mindnet" --apply
|
||||
python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -222,6 +227,3 @@ Mindnet hat aktuell **keine integrierte Authentifizierung**.
|
|||
* **Frontend:** Streamlit auf Port 8501 ist offen. Nutze Nginx Basic Auth oder VPN.
|
||||
* **API:** Sollte nicht direkt im öffentlichen Netz stehen.
|
||||
* **Qdrant:** Auf `127.0.0.1` beschränken.
|
||||
|
||||
### 6.2 Typen-Governance
|
||||
Änderungen an der `types.yaml` (z.B. neue Gewichte) wirken global und erfordern Tests.
|
||||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -147,3 +156,4 @@ Aktueller Implementierungsstand der Module.
|
|||
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
|
||||
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
|
||||
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
|
||||
| **WP11** | Backend Intelligence | 🟢 Live | **Async Core, Nomic, Matrix.** |
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Mindnet v2.4 – Entwickler-Workflow
|
||||
**Datei:** `docs/DEV_WORKFLOW.md`
|
||||
**Stand:** 2025-12-10 (Aktualisiert: Inkl. Interview-Tests WP07)
|
||||
**Stand:** 2025-12-11 (Aktualisiert: Inkl. Async Intelligence & Nomic)
|
||||
|
||||
Dieses Handbuch beschreibt den Entwicklungszyklus zwischen **Windows PC** (IDE), **Raspberry Pi** (Gitea) und **Beelink** (Runtime/Server).
|
||||
|
||||
|
|
@ -35,14 +35,14 @@ Hier erstellst du die neue Funktion in einer sicheren Umgebung.
|
|||
2. **Branch erstellen:**
|
||||
* Klicke wieder unten links auf `main`.
|
||||
* Wähle `+ Create new branch...`.
|
||||
* Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp07-interview`).
|
||||
* Gib den Namen ein: `feature/was-ich-tue` (z.B. `feature/wp11-async-fix`).
|
||||
* Drücke **Enter**.
|
||||
|
||||
3. **Sicherheits-Check:**
|
||||
* Steht unten links jetzt dein Feature-Branch? **Nur dann darfst du Code ändern!**
|
||||
|
||||
4. **Coden:**
|
||||
* Nimm deine Änderungen vor (z.B. neue Schemas in `decision_engine.yaml`).
|
||||
* Nimm deine Änderungen vor (z.B. neue Schemas in `decision_engine.yaml` oder Async-Logik in `ingestion.py`).
|
||||
|
||||
5. **Sichern & Hochladen:**
|
||||
* **Source Control** Icon (Gabel-Symbol) -> Nachricht eingeben -> **Commit**.
|
||||
|
|
@ -64,14 +64,16 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft.
|
|||
```bash
|
||||
git fetch
|
||||
# Tipp: 'git branch -r' zeigt alle verfügbaren Branches an
|
||||
git checkout feature/wp07-interview
|
||||
git checkout feature/wp11-async-fix
|
||||
git pull
|
||||
```
|
||||
|
||||
4. **Umgebung vorbereiten (bei Bedarf):**
|
||||
4. **Umgebung vorbereiten (WICHTIG für v2.4):**
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt # Nur nötig bei neuen Paketen
|
||||
pip install -r requirements.txt # HTTPX usw.
|
||||
# Sicherstellen, dass das neue Embedding-Modell da ist:
|
||||
ollama pull nomic-embed-text
|
||||
```
|
||||
|
||||
5. **Test-Server aktualisieren (WICHTIG):**
|
||||
|
|
@ -87,8 +89,6 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft.
|
|||
|
||||
# Logs prüfen (um Fehler zu sehen):
|
||||
journalctl -u mindnet-dev -f
|
||||
# Oder Frontend Logs:
|
||||
journalctl -u mindnet-ui-dev -f
|
||||
```
|
||||
|
||||
**Option B: Manuell Debuggen (Direct Output)**
|
||||
|
|
@ -100,13 +100,6 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft.
|
|||
|
||||
# 2. Manuell starten (z.B. API)
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 8002 --env-file .env
|
||||
|
||||
# ... Testen ...
|
||||
|
||||
# 3. Wenn fertig: Services wieder anschalten (Optional)
|
||||
# Strg+C drücken
|
||||
sudo systemctl start mindnet-dev
|
||||
sudo systemctl start mindnet-ui-dev
|
||||
```
|
||||
|
||||
6. **Validieren (Smoke Tests):**
|
||||
|
|
@ -114,16 +107,15 @@ Hier prüfst du, ob dein neuer Code auf dem echten Server läuft.
|
|||
* **Browser:** Öffne `http://<IP>:8502` um die UI zu testen (Intent Badge prüfen!).
|
||||
* **CLI:** Führe Testskripte in einem **zweiten Terminal** aus:
|
||||
|
||||
**Test A: Decision Engine**
|
||||
**Test A: Intelligence / Aliases (Neu in WP11)**
|
||||
```bash
|
||||
python tests/test_wp06_decision.py -p 8002 -q "Soll ich Qdrant nutzen?"
|
||||
# Erwartung: Intent DECISION
|
||||
python debug_analysis.py
|
||||
# Erwartung: "✅ ALIAS GEFUNDEN"
|
||||
```
|
||||
|
||||
**Test B: Interview Modus (Neu!)**
|
||||
**Test B: API Check**
|
||||
```bash
|
||||
python tests/test_wp06_decision.py -p 8002 -q "Ich will ein neues Projekt starten"
|
||||
# Erwartung: Intent INTERVIEW, Output ist Markdown Codeblock
|
||||
curl -X POST "http://localhost:8002/ingest/analyze" -d '{"text": "mindnet", "type": "journal"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -150,16 +142,18 @@ Jetzt bringen wir die Änderung in das Live-System (Port 8001 / 8501).
|
|||
cd /home/llmadmin/mindnet
|
||||
git pull origin main
|
||||
|
||||
# Dependencies updaten
|
||||
# Dependencies updaten & Modelle checken
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
ollama pull nomic-embed-text
|
||||
|
||||
# Falls sich die Vektor-Dimension geändert hat (v2.4 Upgrade):
|
||||
# python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes
|
||||
# python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
|
||||
|
||||
# Produktions-Services neustarten
|
||||
sudo systemctl restart mindnet-prod
|
||||
sudo systemctl restart mindnet-ui-prod
|
||||
|
||||
# Kurz prüfen, ob er läuft
|
||||
sudo systemctl status mindnet-prod
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -174,7 +168,7 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch.
|
|||
cd ~/mindnet_dev
|
||||
git checkout main
|
||||
git pull
|
||||
git branch -d feature/wp07-interview
|
||||
git branch -d feature/wp11-async-fix
|
||||
```
|
||||
3. **VS Code:**
|
||||
* Auf `main` wechseln.
|
||||
|
|
@ -187,27 +181,25 @@ Damit das Chaos nicht wächst, löschen wir den fertigen Branch.
|
|||
|
||||
| Wo? | Befehl | Was tut es? |
|
||||
| :--- | :--- | :--- |
|
||||
| **VS Code** | `Sync (auf main)` | **WICHTIG:** Holt neuesten Code vom Server. |
|
||||
| **Beelink** | `git fetch` | Aktualisiert Liste der Remote-Branches. |
|
||||
| **Beelink** | `sudo systemctl restart mindnet-dev` | **Neustart Dev-Backend (Port 8002).** |
|
||||
| **Beelink** | `sudo systemctl restart mindnet-ui-dev` | **Neustart Dev-Frontend (Port 8502).** |
|
||||
| **Beelink** | `journalctl -u mindnet-dev -f` | **Live-Logs Backend.** |
|
||||
| **Beelink** | `journalctl -u mindnet-ui-dev -f` | **Live-Logs Frontend.** |
|
||||
| **Beelink** | `python debug_analysis.py` | **Prüft Aliases & Scores.** |
|
||||
| **Beelink** | `python -m scripts.reset_qdrant ...` | **Löscht & Repariert DB.** |
|
||||
|
||||
---
|
||||
|
||||
## 4. Troubleshooting
|
||||
|
||||
**"Vector dimension error: expected 768, got 384"**
|
||||
* **Ursache:** Du hast `nomic-embed-text` (768) aktiviert, aber die DB ist noch alt (384).
|
||||
* **Lösung:** `scripts.reset_qdrant` ausführen und neu importieren.
|
||||
|
||||
**"Read timed out (300s)" / 500 Error beim Interview**
|
||||
* **Ursache:** Das LLM (Ollama) braucht für den One-Shot Draft länger als das Timeout erlaubt.
|
||||
* **Lösung:**
|
||||
1. Erhöhe in `.env` den Wert: `MINDNET_LLM_TIMEOUT=300.0`.
|
||||
2. Starte die Server neu.
|
||||
|
||||
**"Port 8002 / 8502 already in use"**
|
||||
* **Ursache:** Du willst `uvicorn` oder `streamlit` manuell starten, aber der Service läuft noch.
|
||||
* **Lösung:** `sudo systemctl stop mindnet-dev` bzw. `mindnet-ui-dev`.
|
||||
|
||||
**"UnicodeDecodeError in .env"**
|
||||
* **Ursache:** Umlaute oder Sonderzeichen in der `.env` Datei.
|
||||
* **Lösung:** `.env` bereinigen (nur ASCII nutzen) und sicherstellen, dass sie UTF-8 ohne BOM ist.
|
||||
|
|
@ -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):**
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
---
|
||||
<details>
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
- [2.1 Struktur-Kanten (Das Skelett)](#21-struktur-kanten-das-skelett)
|
||||
- [2.2 Inhalts-Kanten (explizit)](#22-inhalts-kanten-explizit)
|
||||
- [2.3 Typ-basierte Default-Kanten (Regelbasiert)](#23-typ-basierte-default-kanten-regelbasiert)
|
||||
- [2.4 Matrix-Logik (Kontextsensitive Kanten) – Neu in v2.4](#24-matrix-logik-kontextsensitive-kanten--neu-in-v24)
|
||||
- [3) Edge-Payload – Felder \& Semantik](#3-edge-payload--felder--semantik)
|
||||
- [4) Typ-Registry (`config/types.yaml`)](#4-typ-registry-configtypesyaml)
|
||||
- [4.1 Zweck](#41-zweck)
|
||||
|
|
@ -26,12 +27,13 @@
|
|||
- [5) Der Retriever (Funktionaler Layer)](#5-der-retriever-funktionaler-layer)
|
||||
- [5.1 Scoring-Modell](#51-scoring-modell)
|
||||
- [5.2 Erklärbarkeit (Explainability) – WP04b](#52-erklärbarkeit-explainability--wp04b)
|
||||
- [6) Context Intelligence \& Intent Router (WP06/WP07)](#6-context-intelligence--intent-router-wp06wp07)
|
||||
- [6) Context Intelligence \& Intent Router (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.** |
|
||||
| **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.** |
|
||||
|
|
@ -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 (`<prefix>_chunks`)
|
||||
Die atomaren Sucheinheiten.
|
||||
* **Zweck:** Vektorsuche (Embeddings), Granulares Ergebnis.
|
||||
* **Update v2.3.10:** Vektor-Dimension ist jetzt **768** (für `nomic-embed-text`).
|
||||
* **Schema (Payload):**
|
||||
|
||||
| Feld | Datentyp | Beschreibung |
|
||||
|
|
@ -204,37 +209,40 @@ Steuert die LLM-Persönlichkeit und Templates.
|
|||
* Enthält Templates für alle Strategien inkl. `interview_template` mit One-Shot Logik.
|
||||
|
||||
### 3.5 Environment (`.env`)
|
||||
Erweiterung für LLM-Steuerung:
|
||||
Erweiterung für LLM-Steuerung und Embedding-Modell:
|
||||
|
||||
MINDNET_LLM_MODEL=phi3:mini
|
||||
MINDNET_EMBEDDING_MODEL=nomic-embed-text # NEU in v2.3.10
|
||||
MINDNET_OLLAMA_URL=http://127.0.0.1:11434
|
||||
MINDNET_LLM_TIMEOUT=300.0 # Neu: Erhöht für CPU-Inference Cold-Starts
|
||||
MINDNET_API_TIMEOUT=60.0 # Neu: Timeout für Frontend-API Calls
|
||||
MINDNET_DECISION_CONFIG="config/decision_engine.yaml"
|
||||
MINDNET_VAULT_ROOT="./vault" # Neu: Pfad für Write-Back
|
||||
|
||||
---
|
||||
|
||||
## 4. Import-Pipeline (Markdown → Qdrant)
|
||||
|
||||
Das Skript `scripts/import_markdown.py` orchestriert den Prozess.
|
||||
**Neu in v2.3.10:** Der Import nutzt `asyncio` und eine **Semaphore**, um Ollama nicht zu überlasten.
|
||||
|
||||
### 4.1 Verarbeitungsschritte
|
||||
### 4.1 Verarbeitungsschritte (Async)
|
||||
|
||||
1. **Discovery & Parsing:**
|
||||
* Einlesen der `.md` Dateien. Hash-Vergleich (Body/Frontmatter) zur Erkennung von Änderungen.
|
||||
2. **Typauflösung:**
|
||||
* Laden der `types.yaml`. Bestimmen des effektiven Typs und der `edge_defaults`.
|
||||
* Bestimmung des `type` via `types.yaml`.
|
||||
3. **Chunking:**
|
||||
* Zerlegung via `chunker.py` basierend auf `chunk_profile` (z.B. `by_heading`, `short`, `long`).
|
||||
* Trennung von `text` (Kern) und `window` (Embedding-Kontext).
|
||||
4. **Kantenableitung (Edge Derivation):**
|
||||
Die `derive_edges.py` erzeugt Kanten in strikter Reihenfolge:
|
||||
1. **Inline-Edges:** `[[rel:depends_on X]]` → `kind=depends_on`, `rule_id=inline:rel`, `conf=0.95`.
|
||||
2. **Callout-Edges:** `> [!edge] related_to: [[X]]` → `kind=related_to`, `rule_id=callout:edge`, `conf=0.90`.
|
||||
3. **Explizite Referenzen:** `[[X]]` → `kind=references`, `rule_id=explicit:wikilink`, `conf=1.0`.
|
||||
4. **Typ-Defaults:** Für jede Referenz werden Zusatzkanten gemäß `edge_defaults` erzeugt (z.B. `project` -> `depends_on`). `rule_id=edge_defaults:...`, `conf=0.7`.
|
||||
5. **Struktur:** `belongs_to`, `next`, `prev` (automatisch).
|
||||
5. **Upsert:**
|
||||
* Schreiben in Qdrant. Nutzung von `--purge-before-upsert` für saubere Updates.
|
||||
* Zerlegung via `chunker.py` basierend auf `chunk_profile`.
|
||||
4. **Embedding (Async):**
|
||||
* Der `EmbeddingsClient` (`app/services/embeddings_client.py`) sendet Text-Chunks asynchron an Ollama.
|
||||
* Modell: `nomic-embed-text` (768d).
|
||||
* Semaphore: Max. 5 gleichzeitige Files, um OOM (Out-of-Memory) zu verhindern.
|
||||
5. **Kantenableitung (Edge Derivation):**
|
||||
* `derive_edges.py` erzeugt Inline-, Callout- und Default-Edges.
|
||||
6. **Upsert:**
|
||||
* Schreiben in Qdrant. Nutzung von `--purge-before-upsert`.
|
||||
* **Strict Mode:** Der Prozess bricht ab, wenn Embeddings leer sind oder Dimension `0` haben.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -243,7 +251,7 @@ Das Skript `scripts/import_markdown.py` orchestriert den Prozess.
|
|||
Der Retriever (`app/core/retriever.py`) unterstützt zwei Modi. Für den Chat wird **zwingend** der Hybrid-Modus genutzt.
|
||||
|
||||
### 5.1 Betriebsmodi
|
||||
* **Semantic:** Reine Vektorsuche. Schnell.
|
||||
* **Semantic:** Reine Vektorsuche (768d).
|
||||
* **Hybrid:** Vektorsuche + Graph-Expansion (Tiefe N) + Re-Ranking.
|
||||
|
||||
### 5.2 Scoring-Formel (WP04a)
|
||||
|
|
@ -274,7 +282,7 @@ Der Hybrid-Modus lädt dynamisch die Nachbarschaft der Top-K Vektor-Treffer ("Se
|
|||
|
||||
---
|
||||
|
||||
## 6. RAG & Chat Architektur (WP06 Hybrid Router + WP07 Interview)
|
||||
## 6. RAG \& Chat Architektur (WP06 Hybrid Router + WP07 Interview)
|
||||
|
||||
Der Flow für eine Chat-Anfrage (`/chat`) wurde in WP06 auf eine **Configuration-Driven Architecture** umgestellt. Der `ChatRouter` (`app/routers/chat.py`) fungiert als zentraler Dispatcher.
|
||||
|
||||
|
|
@ -329,7 +337,7 @@ Das Frontend ist eine **Streamlit-Anwendung** (`app/frontend/ui.py`), die als se
|
|||
|
||||
### 7.1 Kommunikation
|
||||
* **Backend-URL:** Konfiguriert via `MINDNET_API_URL` (Default: `http://localhost:8002`).
|
||||
* **Endpoints:** Nutzt `/chat` für Interaktion und `/feedback` für Bewertungen.
|
||||
* **Endpoints:** Nutzt `/chat` für Interaktion, `/feedback` für Bewertungen und `/ingest/analyze` für Intelligence.
|
||||
* **Resilienz:** Das Frontend implementiert eigene Timeouts (`MINDNET_API_TIMEOUT`, Default 300s).
|
||||
|
||||
### 7.2 Features & UI-Logik
|
||||
|
|
@ -350,7 +358,14 @@ Wenn der Intent `INTERVIEW` ist, rendert die UI statt einer Textblase den **Draf
|
|||
3. **Editor Widget:** `st.text_area` erlaubt das Bearbeiten des Inhalts vor dem Speichern.
|
||||
4. **Action:** Buttons zum Download oder Kopieren des fertigen Markdowns.
|
||||
|
||||
### 7.4 Deployment Ports
|
||||
### 7.4 State Management (Resurrection Pattern)
|
||||
Um Datenverlust bei Tab-Wechseln (Chat <-> Editor) zu verhindern, nutzt `ui.py` ein Persistenz-Muster:
|
||||
* Daten liegen in `st.session_state[data_key]`.
|
||||
* Widgets liegen in `st.session_state[widget_key]`.
|
||||
* Callbacks (`on_change`) synchronisieren Widget -> Data.
|
||||
* Beim Neu-Rendern wird Widget-State aus Data-State wiederhergestellt.
|
||||
|
||||
### 7.5 Deployment Ports
|
||||
Zur sauberen Trennung von Prod und Dev laufen Frontend und Backend auf dedizierten Ports:
|
||||
|
||||
| Umgebung | Backend (FastAPI) | Frontend (Streamlit) |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -251,3 +270,4 @@ Aktueller Implementierungsstand der Module.
|
|||
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
|
||||
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
|
||||
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
|
||||
| **WP11** | Backend Intelligence | 🟢 Live | **Async Ingestion, Nomic Embeddings, Matrix Logic.** |
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Mindnet v2.4 – User Guide
|
||||
**Datei:** `docs/mindnet_user_guide_v2.4.md`
|
||||
**Stand:** 2025-12-10
|
||||
**Status:** **FINAL** (Inkl. RAG, Web-Interface & Interview-Assistent)
|
||||
**Stand:** 2025-12-11
|
||||
**Status:** **FINAL** (Inkl. RAG, Web-Interface & Interview-Assistent & Intelligence)
|
||||
**Quellen:** `knowledge_design.md`, `wp04_retriever_scoring.md`, `Programmplan_V2.2.md`, `Handbuch.md`.
|
||||
|
||||
> **Willkommen bei Mindnet.**
|
||||
|
|
@ -42,6 +42,7 @@ Seit Version 2.3.1 bedienst du Mindnet über eine grafische Oberfläche im Brows
|
|||
|
||||
### 2.2 Die Sidebar (Einstellungen & Verlauf)
|
||||
* **Modus-Wahl:** Umschalten zwischen "💬 Chat" und "📝 Manueller Editor".
|
||||
* *Neu in v2.4:* Der manuelle Editor speichert deine Eingaben auch beim Wechseln der Tabs ("State Resurrection").
|
||||
* **Verlauf:** Die letzten Suchanfragen sind hier gelistet. Ein Klick führt die Suche erneut aus.
|
||||
* **Settings:**
|
||||
* **Top-K:** Wie viele Quellen sollen gelesen werden? (Standard: 5).
|
||||
|
|
@ -68,7 +69,7 @@ Wenn du frustriert bist oder reflektieren willst, wechselt Mindnet in den "Ich"-
|
|||
* **Auslöser (Keywords & Semantik):** "Ich fühle mich...", "Traurig", "Gestresst", "Alles ist sinnlos", "Ich bin überfordert".
|
||||
* **Was passiert:** Mindnet lädt deine **Erfahrungen** (`type: experience`) und **Glaubenssätze** (`type: belief`). Es antwortet verständnisvoll und zitiert deine eigenen Lektionen.
|
||||
|
||||
### 3.3 Modus: Interview ("Der Analyst") – Neu!
|
||||
### 3.3 Modus: Interview ("Der Analyst")
|
||||
Wenn du Wissen festhalten willst, statt zu suchen.
|
||||
|
||||
* **Auslöser:** "Neues Projekt", "Notiz erstellen", "Ich will etwas festhalten", "Neue Entscheidung dokumentieren".
|
||||
|
|
@ -129,3 +130,14 @@ Mindnet kann dir helfen, Markdown-Notizen zu schreiben.
|
|||
* Du siehst den Body-Text mit Platzhaltern (`[TODO]`), wo Infos fehlten (z.B. Stakeholder).
|
||||
4. **Finalisierung:** Ergänze die fehlenden Infos direkt im Editor und klicke auf **Download** oder **Kopieren**.
|
||||
5. **Speichern:** Speichere die Datei in deinen Obsidian Vault. Beim nächsten Import ist sie im System.
|
||||
|
||||
### 6.4 Der Intelligence-Workflow (Neu in v2.4)
|
||||
Wenn du Texte im **manuellen Editor** schreibst, unterstützt dich Mindnet aktiv bei der Vernetzung:
|
||||
|
||||
1. **Schreiben:** Tippe deinen Text im Tab **"✏️ Inhalt"**.
|
||||
2. **Analysieren:** Wechsle zum Tab **"🧠 Intelligence"** und klicke auf **"🔍 Analyse starten"**. Das System scannt deinen Text (Vektor-Suche & Exact Match).
|
||||
3. **Vorschläge nutzen:**
|
||||
* **Exakte Treffer:** Das System erkennt Begriffe wie "KI-Gedächtnis" automatisch als Alias für "Mindnet (System)".
|
||||
* **Semantische Treffer:** Das System findet inhaltlich verwandte Notizen.
|
||||
* **Klick auf "➕ Einfügen":** Fügt den Link (z.B. `[[rel:related_to Mindnet]]`) an der Cursor-Position oder am Ende ein.
|
||||
4. **Speichern:** Klicke auf "💾 Speichern & Indizieren". Der Text wird sofort in den Vault geschrieben und in Qdrant indiziert.
|
||||
|
|
@ -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)
|
||||
|
||||
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 {}
|
||||
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()
|
||||
|
||||
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)
|
||||
logger.info(f"Found {len(files)} markdown files.")
|
||||
|
||||
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 {}
|
||||
stats = {"processed": 0, "skipped": 0, "errors": 0}
|
||||
|
||||
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")
|
||||
# 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 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)}...")
|
||||
|
||||
# --- Writes ---
|
||||
if not args.apply:
|
||||
continue
|
||||
tasks = [process_with_limit(f) for f in batch]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
# Wenn nichts geändert und keine Artefakte fehlen → nichts zu tun
|
||||
if not changed and not (chunks_missing or edges_missing):
|
||||
continue
|
||||
logger.info(f"Done. Stats: {stats}")
|
||||
if not args.apply:
|
||||
logger.info("DRY RUN. Use --apply to write to DB.")
|
||||
|
||||
# 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}"}))
|
||||
def main():
|
||||
load_dotenv()
|
||||
default_prefix = os.getenv("COLLECTION_PREFIX", "mindnet")
|
||||
|
||||
# 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)
|
||||
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")
|
||||
|
||||
# Chunks schreiben, wenn geändert ODER vorher fehlend
|
||||
if chunk_pls and (changed or chunks_missing):
|
||||
chunks_name, chunk_pts = points_for_chunks(cfg.prefix, chunk_pls, vecs)
|
||||
upsert_batch(client, chunks_name, chunk_pts)
|
||||
|
||||
# Edges schreiben, wenn vorhanden und (geändert ODER vorher fehlend)
|
||||
if edges and (changed or edges_missing):
|
||||
edges_name, edge_pts = points_for_edges(cfg.prefix, edges)
|
||||
upsert_batch(client, edges_name, edge_pts)
|
||||
|
||||
print(f"Done. Processed notes: {processed}")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Starte den Async Loop
|
||||
asyncio.run(main_async(args))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -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.")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user