Merge pull request 'WP11' (#8) from WP11 into main
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 3s
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 3s
Reviewed-on: #8 # Merge Request: WP-11 Backend Intelligence & Async Core (v2.4.0) **Branch:** `feature/wp11-backend-intelligence` → `main` **Reviewer:** @Mindmaster **Status:** ✅ Ready to Merge ## 🎯 Zielsetzung Dieses Update implementiert die "Active Intelligence" Architektur. Das System wechselt von einer reaktiven Suche zu einem proaktiven Assistenten, der während des Schreibens im Editor semantische Verknüpfungen vorschlägt. Zudem wurde der gesamte Core auf `asyncio` umgestellt, um Timeouts bei der Generierung zu verhindern. ## 🛠 Technische Änderungen ### 1. Async & Performance * **Ingestion:** `scripts/import_markdown.py` und `app/core/ingestion.py` laufen nun asynchron. * **Embeddings:** Neuer `EmbeddingsClient` nutzt `httpx` statt `requests` (Non-blocking). * **Semaphore:** Import nutzt max. 5 parallele Tasks, um Ollama-Überlastung zu verhindern. ### 2. Quality Upgrade (Nomic) * **Modell:** Wechsel von `sentence-transformers` (384 dim) auf `nomic-embed-text` (768 dim). * **Effekt:** Massiv verbesserte semantische Trefferquote (siehe "Italien-Test"). ### 3. Intelligence Features * **Active Intelligence:** Neuer Endpoint `/ingest/analyze` analysiert Drafts mittels "Sliding Window". * **Exact Match:** Erkennt Aliases (z.B. "KI-Gedächtnis") zuverlässig. * **Matrix Logic:** `DiscoveryService` wählt Kanten-Typen kontextsensitiv (z.B. `experience` + `value` -> `based_on`). ### 4. Frontend Integration * **UI:** Neuer Tab "🧠 Intelligence" im manuellen Editor. * **State:** "Resurrection Pattern" verhindert Datenverlust beim Tab-Wechsel. ## ⚠️ Breaking Changes (WICHTIG für Deployment) Dieses Release ist **nicht abwärtskompatibel** zur Datenbank von v2.3! 1. **Vektor-Dimension:** Geändert von 384 auf 768. 2. **Ollama:** Modell `nomic-embed-text` ist PFLICHT. 3. **Config:** `.env` benötigt `VECTOR_DIM=768` und `MINDNET_EMBEDDING_MODEL`. ## 🧪 Test-Protokoll | Test | Befehl | Status | | :--- | :--- | :--- | | **Alias Lookup** | `python debug_analysis.py` | ✅ Pass | | **Async Import** | `python -m scripts.import_markdown ...` | ✅ Pass | | **API Intelligence** | `curl ... /ingest/analyze` | ✅ Pass | | **UI Interaction** | Editor öffnen -> Analyse -> Link einfügen | ✅ Pass | ## 🔄 Deployment Schritte Nach dem Merge auf dem Server ausführen: 1. `git pull` 2. `pip install -r requirements.txt` 3. `ollama pull nomic-embed-text` 4. **DB Reset:** `python -m scripts.reset_qdrant --mode wipe --prefix mindnet --yes` 5. **Re-Import:** `python -m scripts.import_markdown --vault ./vault --prefix mindnet --apply --force` 6. `sudo systemctl restart mindnet-prod mindnet-ui-prod`
This commit is contained in:
commit
f016a16c68
|
|
@ -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,
|
||||
|
|
@ -126,4 +133,4 @@ def make_chunk_payloads(note: Dict[str, Any],
|
|||
|
||||
out.append(pl)
|
||||
|
||||
return out
|
||||
return out
|
||||
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,13 +215,22 @@ def make_note_payload(note: Any, *args, **kwargs) -> Dict[str, Any]:
|
|||
if tags:
|
||||
payload["tags"] = _ensure_list(tags)
|
||||
|
||||
# WP-11: Aliases übernehmen (für Discovery Service)
|
||||
aliases = fm.get("aliases")
|
||||
if aliases:
|
||||
payload["aliases"] = _ensure_list(aliases)
|
||||
|
||||
# Zeitliche Metadaten (sofern vorhanden)
|
||||
for k in ("created", "modified", "date"):
|
||||
v = fm.get(k) or n.get(k)
|
||||
if v:
|
||||
payload[k] = str(v)
|
||||
|
||||
# Fulltext (Fallback, falls body im Input)
|
||||
if "body" in n and n["body"]:
|
||||
payload["fulltext"] = str(n["body"])
|
||||
|
||||
# JSON-Roundtrip zur harten Validierung (ASCII beibehalten)
|
||||
json.loads(json.dumps(payload, ensure_ascii=False))
|
||||
|
||||
return payload
|
||||
return payload
|
||||
|
|
@ -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)
|
||||
|
||||
# Persistent Data (Source of Truth)
|
||||
st.session_state[data_meta_key] = meta
|
||||
st.session_state[data_sugg_key] = []
|
||||
st.session_state[data_body_key] = body.strip()
|
||||
|
||||
st.session_state[f"{key_base}_type"] = meta.get("type", "default")
|
||||
st.session_state[f"{key_base}_title"] = meta.get("title", "")
|
||||
|
||||
tags_raw = meta.get("tags", [])
|
||||
st.session_state[f"{key_base}_tags"] = ", ".join(tags_raw) if isinstance(tags_raw, list) else str(tags_raw)
|
||||
|
||||
st.session_state[f"{key_base}_body"] = body.strip()
|
||||
st.session_state[f"{key_base}_meta"] = meta
|
||||
st.session_state[f"{key_base}_init"] = True
|
||||
|
||||
# 2. UI
|
||||
# --- 2. RESURRECTION FIX (WICHTIG!) ---
|
||||
# Wenn wir vom Manuellen Editor zurückkommen, wurde der widget_key von Streamlit gelöscht.
|
||||
# Wir müssen ihn aus dem persistenten data_body_key wiederherstellen.
|
||||
if widget_body_key not in st.session_state and data_body_key in st.session_state:
|
||||
st.session_state[widget_body_key] = st.session_state[data_body_key]
|
||||
|
||||
# --- CALLBACKS ---
|
||||
def _sync_body():
|
||||
# Sync Widget -> Data (Source of Truth)
|
||||
st.session_state[data_body_key] = st.session_state[widget_body_key]
|
||||
|
||||
def _insert_text(text_to_insert):
|
||||
# Insert in Widget Key und Sync Data
|
||||
current = st.session_state.get(widget_body_key, "")
|
||||
new_text = f"{current}\n\n{text_to_insert}"
|
||||
st.session_state[widget_body_key] = new_text
|
||||
st.session_state[data_body_key] = new_text
|
||||
|
||||
def _remove_text(text_to_remove):
|
||||
current = st.session_state.get(widget_body_key, "")
|
||||
new_text = current.replace(text_to_remove, "").strip()
|
||||
st.session_state[widget_body_key] = new_text
|
||||
st.session_state[data_body_key] = new_text
|
||||
|
||||
# --- UI LAYOUT ---
|
||||
st.markdown(f'<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")
|
||||
# Auch hier Keys für Widgets nutzen, um Resets zu vermeiden
|
||||
title_key = f"{key_base}_wdg_title"
|
||||
if title_key not in st.session_state: st.session_state[title_key] = meta_ref["title"]
|
||||
meta_ref["title"] = st.text_input("Titel", key=title_key)
|
||||
|
||||
with c2:
|
||||
known_types = ["concept", "project", "decision", "experience", "journal", "person", "value", "goal", "principle", "default"]
|
||||
curr_type = st.session_state.get(f"{key_base}_type", "default")
|
||||
if curr_type not in known_types: known_types.append(curr_type)
|
||||
new_type = st.selectbox("Typ", known_types, index=known_types.index(curr_type), key=f"{key_base}_sel_type")
|
||||
known_types = ["concept", "project", "decision", "experience", "journal", "value", "goal", "principle"]
|
||||
curr = meta_ref["type"]
|
||||
if curr not in known_types: known_types.append(curr)
|
||||
type_key = f"{key_base}_wdg_type"
|
||||
if type_key not in st.session_state: st.session_state[type_key] = meta_ref["type"]
|
||||
meta_ref["type"] = st.selectbox("Typ", known_types, index=known_types.index(curr) if curr in known_types else 0, key=type_key)
|
||||
|
||||
new_tags = st.text_input("Tags (kommagetrennt)", value=st.session_state.get(f"{key_base}_tags", ""), key=f"{key_base}_inp_tags")
|
||||
tags_key = f"{key_base}_wdg_tags"
|
||||
if tags_key not in st.session_state: st.session_state[tags_key] = meta_ref.get("tags_str", "")
|
||||
meta_ref["tags_str"] = st.text_input("Tags", key=tags_key)
|
||||
|
||||
# Tabs
|
||||
tab_edit, tab_view = st.tabs(["✏️ Inhalt", "👁️ Vorschau"])
|
||||
tab_edit, tab_intel, tab_view = st.tabs(["✏️ Inhalt", "🧠 Intelligence", "👁️ Vorschau"])
|
||||
|
||||
# --- TAB 1: EDITOR ---
|
||||
with tab_edit:
|
||||
st.caption("Bearbeite hier den Inhalt. Metadaten (oben) werden automatisch hinzugefügt.")
|
||||
new_body = st.text_area(
|
||||
# Hier kein 'value' Argument mehr, da wir den Key oben (Resurrection) initialisiert haben.
|
||||
st.text_area(
|
||||
"Body",
|
||||
value=st.session_state.get(f"{key_base}_body", ""),
|
||||
key=widget_body_key,
|
||||
height=500,
|
||||
key=f"{key_base}_txt_body",
|
||||
on_change=_sync_body,
|
||||
label_visibility="collapsed"
|
||||
)
|
||||
|
||||
# Reassembly
|
||||
final_tags_list = [t.strip() for t in new_tags.split(",") if t.strip()]
|
||||
final_meta = st.session_state.get(f"{key_base}_meta", {}).copy()
|
||||
final_meta.update({
|
||||
|
||||
# --- TAB 2: INTELLIGENCE ---
|
||||
with tab_intel:
|
||||
st.info("Klicke auf 'Analysieren', um Verknüpfungen für den AKTUELLEN Text zu finden.")
|
||||
|
||||
if st.button("🔍 Analyse starten", key=f"{key_base}_analyze"):
|
||||
st.session_state[data_sugg_key] = []
|
||||
|
||||
# Lese vom Widget (aktuell) oder Data (Fallback)
|
||||
text_to_analyze = st.session_state.get(widget_body_key, st.session_state.get(data_body_key, ""))
|
||||
|
||||
with st.spinner("Analysiere..."):
|
||||
analysis = analyze_draft_text(text_to_analyze, meta_ref["type"])
|
||||
|
||||
if "error" in analysis:
|
||||
st.error(f"Fehler: {analysis['error']}")
|
||||
else:
|
||||
suggestions = analysis.get("suggestions", [])
|
||||
st.session_state[data_sugg_key] = suggestions
|
||||
if not suggestions:
|
||||
st.warning("Keine Vorschläge gefunden.")
|
||||
else:
|
||||
st.success(f"{len(suggestions)} Vorschläge gefunden.")
|
||||
|
||||
# Render List
|
||||
suggestions = st.session_state[data_sugg_key]
|
||||
if suggestions:
|
||||
current_text_state = st.session_state.get(widget_body_key, "")
|
||||
|
||||
for idx, sugg in enumerate(suggestions):
|
||||
link_text = sugg.get('suggested_markdown', '')
|
||||
is_inserted = link_text in current_text_state
|
||||
|
||||
bg_color = "#e6fffa" if is_inserted else "#ffffff"
|
||||
border = "3px solid #28a745" if is_inserted else "3px solid #1a73e8"
|
||||
|
||||
st.markdown(f"""
|
||||
<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": new_type,
|
||||
"title": new_title,
|
||||
"type": meta_ref["type"],
|
||||
"title": meta_ref["title"],
|
||||
"status": "draft",
|
||||
"tags": final_tags_list
|
||||
})
|
||||
|
||||
final_doc = build_markdown_doc(final_meta, new_body)
|
||||
"tags": final_tags
|
||||
}
|
||||
# Final Doc aus Data
|
||||
final_body = st.session_state.get(widget_body_key, st.session_state[data_body_key])
|
||||
final_doc = build_markdown_doc(final_meta, final_body)
|
||||
|
||||
with tab_view:
|
||||
st.markdown('<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,36 +53,49 @@ Wir nutzen Qdrant als Vektor-Datenbank. Persistenz ist wichtig.
|
|||
-v $(pwd)/qdrant_storage:/qdrant/storage \
|
||||
qdrant/qdrant
|
||||
|
||||
### 2.4 Ollama Setup (LLM Service)
|
||||
Mindnet benötigt einen lokalen LLM-Server für den Chat.
|
||||
### 2.4 Ollama Setup (LLM & Embeddings)
|
||||
Mindnet benötigt einen lokalen LLM-Server für Chat UND Embeddings.
|
||||
**WICHTIG (Update v2.3.10):** Es muss zwingend `nomic-embed-text` installiert sein, sonst startet der Import nicht.
|
||||
|
||||
# 1. Installieren (Linux Script)
|
||||
curl -fsSL [https://ollama.com/install.sh](https://ollama.com/install.sh) | sh
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# 2. Modell laden (Phi-3 Mini für CPU-Performance)
|
||||
ollama pull phi3:mini
|
||||
# 2. Modelle laden
|
||||
ollama pull phi3:mini # Für Chat/Reasoning
|
||||
ollama pull nomic-embed-text # Für Vektoren (768 Dim)
|
||||
|
||||
# 3. Testen
|
||||
curl http://localhost:11434/api/generate -d '{"model": "phi3:mini", "prompt":"Hi"}'
|
||||
|
||||
### 2.5 Konfiguration (ENV)
|
||||
Erstelle eine `.env` Datei im Root-Verzeichnis. Die neuen Settings für WP-06/WP-07 (Timeout, Decision Config) sind essenziell für stabilen Betrieb auf CPUs.
|
||||
Erstelle eine `.env` Datei im Root-Verzeichnis. Achte besonders auf `VECTOR_DIM` und `MINDNET_EMBEDDING_MODEL`.
|
||||
|
||||
# Server Config
|
||||
UVICORN_HOST=0.0.0.0
|
||||
|
||||
# Qdrant Verbindung
|
||||
QDRANT_URL="http://localhost:6333"
|
||||
|
||||
# Mindnet Core Settings
|
||||
COLLECTION_PREFIX="mindnet"
|
||||
MINDNET_TYPES_FILE="./config/types.yaml"
|
||||
MINDNET_VAULT_ROOT="./vault"
|
||||
|
||||
# LLM / RAG Settings
|
||||
MINDNET_LLM_MODEL="phi3:mini"
|
||||
# WICHTIG: Dimension auf 768 setzen (für Nomic)
|
||||
VECTOR_DIM=768
|
||||
|
||||
# AI Modelle (Ollama)
|
||||
MINDNET_OLLAMA_URL="http://127.0.0.1:11434"
|
||||
MINDNET_LLM_MODEL="phi3:mini"
|
||||
MINDNET_EMBEDDING_MODEL="nomic-embed-text" # NEU
|
||||
|
||||
# Config & Timeouts
|
||||
# Timeouts (Erhöht für Async/Nomic)
|
||||
MINDNET_LLM_TIMEOUT=300.0
|
||||
MINDNET_API_TIMEOUT=60.0
|
||||
|
||||
# Configs
|
||||
MINDNET_PROMPTS_PATH="./config/prompts.yaml"
|
||||
MINDNET_DECISION_CONFIG="./config/decision_engine.yaml"
|
||||
MINDNET_LLM_TIMEOUT=300.0
|
||||
|
||||
### 2.6 Deployment via Systemd (Backend & Frontend)
|
||||
|
||||
|
|
@ -98,6 +111,7 @@ Mindnet benötigt zwei Services pro Umgebung: API (Uvicorn) und UI (Streamlit).
|
|||
User=llmadmin
|
||||
Group=llmadmin
|
||||
WorkingDirectory=/home/llmadmin/mindnet
|
||||
# Async Server Start
|
||||
ExecStart=/home/llmadmin/mindnet/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8001 --env-file .env
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
|
@ -141,11 +155,11 @@ Mindnet benötigt zwei Services pro Umgebung: API (Uvicorn) und UI (Streamlit).
|
|||
## 3. Betrieb im Alltag
|
||||
|
||||
### 3.1 Regelmäßige Importe
|
||||
Der Vault-Zustand sollte regelmäßig (z.B. stündlich per Cronjob) nach Qdrant synchronisiert werden.
|
||||
Der Vault-Zustand sollte regelmäßig (z.B. stündlich per Cronjob) nach Qdrant synchronisiert werden. Das Skript nutzt nun **AsyncIO** und eine Semaphore, um Ollama nicht zu überlasten.
|
||||
|
||||
**Cronjob-Beispiel (stündlich):**
|
||||
|
||||
0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault /path/to/vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1
|
||||
0 * * * * cd /home/llmadmin/mindnet && .venv/bin/python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --purge-before-upsert --sync-deletes >> ./logs/import.log 2>&1
|
||||
|
||||
### 3.2 Health-Checks
|
||||
Prüfe regelmäßig, ob alle Komponenten laufen.
|
||||
|
|
@ -157,34 +171,28 @@ Prüfe regelmäßig, ob alle Komponenten laufen.
|
|||
### 3.3 Logs & Monitoring
|
||||
* **Backend Fehler:** `journalctl -u mindnet-prod -f`
|
||||
* **Frontend Fehler:** `journalctl -u mindnet-ui-prod -f`
|
||||
* Achte auf "Timeout"-Meldungen im Frontend, wenn das Backend zu langsam antwortet.
|
||||
* **LLM Fehler:** `journalctl -u ollama -f`
|
||||
* **Fachliche Logs:** `data/logs/search_history.jsonl`
|
||||
|
||||
---
|
||||
|
||||
## 4. Update-Prozess
|
||||
## 4. Troubleshooting (Update v2.4)
|
||||
|
||||
Wenn neue Versionen ausgerollt werden (Deployment):
|
||||
### "Vector dimension error: expected dim: 768, got 384"
|
||||
* **Ursache:** Du versuchst, in eine alte Qdrant-Collection (mit 384 Dim aus v2.2) neue Embeddings (mit 768 Dim von Nomic) zu schreiben.
|
||||
* **Lösung:** Full Reset erforderlich.
|
||||
1. `python -m scripts.reset_qdrant --mode wipe --prefix mindnet --yes` (Löscht DB).
|
||||
2. `python -m scripts.import_markdown ...` (Baut neu auf).
|
||||
|
||||
1. **Code aktualisieren:**
|
||||
|
||||
cd /home/llmadmin/mindnet
|
||||
git pull origin main
|
||||
### "500 Internal Server Error" beim Speichern
|
||||
* **Ursache:** Oft Timeout bei Ollama, wenn `nomic-embed-text` noch nicht im RAM geladen ist ("Cold Start").
|
||||
* **Lösung:**
|
||||
1. Sicherstellen, dass Modell existiert: `ollama list`.
|
||||
2. API neustarten (re-initialisiert Async Clients).
|
||||
|
||||
2. **Dependencies prüfen:**
|
||||
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
3. **Dienste neustarten (Zwingend!):**
|
||||
|
||||
sudo systemctl restart mindnet-prod
|
||||
sudo systemctl restart mindnet-ui-prod
|
||||
|
||||
4. **Schema-Migration (falls nötig):**
|
||||
|
||||
python3 -m scripts.import_markdown ... --apply
|
||||
### "NameError: name 'os' is not defined"
|
||||
* **Ursache:** Fehlender Import in Skripten nach Updates.
|
||||
* **Lösung:** `git pull` (Fix wurde in v2.3.10 deployed).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -202,16 +210,13 @@ Für schnelle Wiederherstellung des Suchindex.
|
|||
tar -czf qdrant_backup_$(date +%F).tar.gz ./qdrant_storage
|
||||
docker start mindnet_qdrant
|
||||
|
||||
### 5.3 Log-Daten (Priorität 3)
|
||||
Sichere den Ordner `data/logs/`. Verlust dieser Daten bedeutet Verlust des Trainingsmaterials für Self-Tuning.
|
||||
|
||||
### 5.4 Notfall-Wiederherstellung (Rebuild)
|
||||
Wenn die Datenbank korrupt ist:
|
||||
### 5.3 Notfall-Wiederherstellung (Rebuild)
|
||||
Wenn die Datenbank korrupt ist oder Modelle gewechselt werden:
|
||||
|
||||
# 1. DB komplett leeren (Wipe)
|
||||
python3 -m scripts.reset_qdrant --mode wipe --prefix "mindnet" --yes
|
||||
# 2. Alles neu importieren
|
||||
python3 -m scripts.import_markdown --vault /path/to/vault --prefix "mindnet" --apply
|
||||
python3 -m scripts.import_markdown --vault ./vault --prefix "mindnet" --apply --force
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -221,7 +226,4 @@ Wenn die Datenbank korrupt ist:
|
|||
Mindnet hat aktuell **keine integrierte Authentifizierung**.
|
||||
* **Frontend:** Streamlit auf Port 8501 ist offen. Nutze Nginx Basic Auth oder VPN.
|
||||
* **API:** Sollte nicht direkt im öffentlichen Netz stehen.
|
||||
* **Qdrant:** Auf `127.0.0.1` beschränken.
|
||||
|
||||
### 6.2 Typen-Governance
|
||||
Änderungen an der `types.yaml` (z.B. neue Gewichte) wirken global und erfordern Tests.
|
||||
* **Qdrant:** Auf `127.0.0.1` beschränken.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Mindnet v2.4 – Appendices & Referenzen
|
||||
**Datei:** `docs/mindnet_appendices_v2.4.md`
|
||||
**Stand:** 2025-12-10
|
||||
**Status:** **FINAL** (Integrierter Stand WP01–WP10a)
|
||||
**Stand:** 2025-12-11
|
||||
**Status:** **FINAL** (Integrierter Stand WP01–WP11)
|
||||
**Quellen:** `TYPE_REGISTRY_MANUAL.md`, `chunking_strategy.md`, `mindnet_technical_architecture.md`, `Handbuch.md`.
|
||||
|
||||
> Dieses Dokument bündelt Tabellen, Schemata und technische Referenzen, die in den Prozess-Dokumenten (Playbook, Guides) den Lesefluss stören würden.
|
||||
|
|
@ -43,7 +43,9 @@ Referenz aller implementierten Kantenarten (`kind`).
|
|||
| `similar_to` | Inline | Ja | Inhaltliche Ähnlichkeit. "Ist wie X". |
|
||||
| `caused_by` | Inline | Nein | Kausalität. "X ist der Grund für Y". |
|
||||
| `solves` | Inline | Nein | Lösung. "Tool X löst Problem Y". |
|
||||
| `derived_from` | Default (Exp) | Nein | Herkunft. "Erkenntnis stammt aus Quelle X". |
|
||||
| `derived_from` | Matrix / Default | Nein | Herkunft. "Erkenntnis stammt aus Prinzip X". |
|
||||
| `based_on` | Matrix | Nein | Fundament. "Erfahrung basiert auf Wert Y". |
|
||||
| `uses` | Matrix | Nein | Nutzung. "Projekt nutzt Konzept Z". |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -104,28 +106,35 @@ Diese Variablen steuern das Verhalten der Skripte und Container.
|
|||
| `QDRANT_URL` | `http://localhost:6333` | URL zur Vektor-DB. |
|
||||
| `QDRANT_API_KEY` | *(leer)* | API-Key für Absicherung (optional). |
|
||||
| `COLLECTION_PREFIX` | `mindnet` | Namensraum für Collections (`{prefix}_notes` etc). |
|
||||
| `VECTOR_DIM` | `768` | **NEU:** Dimension für Embeddings (für Nomic). |
|
||||
| `MINDNET_TYPES_FILE` | `config/types.yaml` | Pfad zur Typ-Registry. |
|
||||
| `MINDNET_RETRIEVER_CONFIG`| `config/retriever.yaml`| Pfad zur Scoring-Konfiguration. |
|
||||
| `MINDNET_PROMPTS_PATH` | `config/prompts.yaml` | Pfad zu LLM-Prompts (Neu in v2.2). |
|
||||
| `MINDNET_DECISION_CONFIG` | `config/decision_engine.yaml` | Router & Interview Config (Neu in v2.3). |
|
||||
| `MINDNET_LLM_MODEL` | `phi3:mini` | Name des Ollama-Modells (Neu in v2.2). |
|
||||
| `MINDNET_LLM_MODEL` | `phi3:mini` | Name des Chat-Modells. |
|
||||
| `MINDNET_EMBEDDING_MODEL` | `nomic-embed-text` | **NEU:** Name des Vektor-Modells. |
|
||||
| `MINDNET_OLLAMA_URL` | `http://127.0.0.1:11434`| URL zum LLM-Server (Neu in v2.2). |
|
||||
| `MINDNET_LLM_TIMEOUT` | `300.0` | Timeout für Ollama (Erhöht für CPU-Inference). |
|
||||
| `MINDNET_API_TIMEOUT` | `300.0` | Frontend Timeout (Streamlit). |
|
||||
| `MINDNET_API_TIMEOUT` | `60.0` | **NEU:** Frontend Timeout (Streamlit). |
|
||||
| `MINDNET_VAULT_ROOT` | `./vault` | **NEU:** Pfad für Write-Back Operationen. |
|
||||
| `MINDNET_HASH_COMPARE` | `Body` | Vergleichsmodus für Import (`Body`, `Frontmatter`, `Full`). |
|
||||
| `MINDNET_HASH_SOURCE` | `parsed` | Quelle für Hash (`parsed`, `raw`, `file`). |
|
||||
| `VECTOR_DIM` | `384` | Dimension der Embeddings (Modellabhängig). |
|
||||
|
||||
---
|
||||
|
||||
## Anhang E: Glossar
|
||||
|
||||
* **Active Intelligence:** Feature, das während des Schreibens Links vorschlägt.
|
||||
* **Async Ingestion:** Non-blocking Import-Prozess zur Vermeidung von Timeouts.
|
||||
* **Decision Engine:** Komponente, die den Intent prüft und Strategien wählt (WP06).
|
||||
* **Draft Editor:** Web-Komponente zur Bearbeitung generierter Notizen (WP10a).
|
||||
* **Explanation Layer:** Komponente, die Scores und Graphen als Begründung liefert.
|
||||
* **Hybrid Router:** Kombination aus Keyword-Matching und LLM-Klassifizierung für Intents.
|
||||
* **Matrix Logic:** Regelwerk, das Kanten-Typen basierend auf Quell- und Ziel-Typ bestimmt.
|
||||
* **Nomic:** Das neue, hochpräzise Embedding-Modell (768 Dim).
|
||||
* **One-Shot Extractor:** LLM-Strategie zur sofortigen Generierung von Drafts ohne Rückfragen (WP07).
|
||||
* **RAG (Retrieval Augmented Generation):** Kombination aus Suche und Text-Generierung.
|
||||
* **Resurrection Pattern:** UI-Technik, um Eingaben bei Tab-Wechseln zu erhalten.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -146,4 +155,5 @@ Aktueller Implementierungsstand der Module.
|
|||
| **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** |
|
||||
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
|
||||
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
|
||||
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
|
||||
| **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
|
||||
|
||||
|
|
@ -250,4 +269,5 @@ Aktueller Implementierungsstand der Module.
|
|||
| **WP07** | Interview Assistent | 🟢 Live | **One-Shot Extractor & Schemas aktiv.** |
|
||||
| **WP08** | Self-Tuning | 🔴 Geplant | Auto-Adjustment der Gewichte. |
|
||||
| **WP10** | Chat Interface | 🟢 Live | Web-Interface (Streamlit). |
|
||||
| **WP10a**| Draft Editor | 🟢 Live | **Interaktives UI für WP07 Drafts.** |
|
||||
| **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".
|
||||
|
|
@ -128,4 +129,15 @@ Mindnet kann dir helfen, Markdown-Notizen zu schreiben.
|
|||
* Du siehst das generierte Frontmatter (`type: project`, `status: draft`).
|
||||
* Du siehst den Body-Text mit Platzhaltern (`[TODO]`), wo Infos fehlten (z.B. Stakeholder).
|
||||
4. **Finalisierung:** Ergänze die fehlenden Infos direkt im Editor und klicke auf **Download** oder **Kopieren**.
|
||||
5. **Speichern:** Speichere die Datei in deinen Obsidian Vault. Beim nächsten Import ist sie im System.
|
||||
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)
|
||||
|
||||
logger.info(f"Scanning {vault_path}...")
|
||||
files = list(vault_path.rglob("*.md"))
|
||||
# Exclude .obsidian folder if present
|
||||
files = [f for f in files if ".obsidian" not in str(f)]
|
||||
files.sort()
|
||||
|
||||
logger.info(f"Found {len(files)} markdown files.")
|
||||
|
||||
def _load_json_or_yaml(path: str) -> dict:
|
||||
import io
|
||||
data: dict = {}
|
||||
if not path or not os.path.exists(path):
|
||||
return data
|
||||
try:
|
||||
import yaml # type: ignore
|
||||
with io.open(path, "r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
return data
|
||||
except Exception:
|
||||
# YAML evtl. nicht installiert – versuche JSON
|
||||
try:
|
||||
with io.open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
return data
|
||||
except Exception:
|
||||
return {}
|
||||
stats = {"processed": 0, "skipped": 0, "errors": 0}
|
||||
|
||||
def load_type_registry() -> dict:
|
||||
# Reihenfolge: ENV > ./types.yaml (im aktuellen Arbeitsverzeichnis)
|
||||
p = _env("MINDNET_TYPES_FILE", None)
|
||||
if p and os.path.exists(p):
|
||||
return _load_json_or_yaml(p)
|
||||
fallback = os.path.abspath("./config/types.yaml") if os.path.exists("./config/types.yaml") else os.path.abspath("./types.yaml")
|
||||
return _load_json_or_yaml(fallback)
|
||||
# Wir nutzen eine Semaphore, um nicht zu viele Files gleichzeitig zu öffnen/embedden
|
||||
sem = asyncio.Semaphore(5) # Max 5 concurrent files to avoid OOM or Rate Limit
|
||||
|
||||
def get_type_config(note_type: Optional[str], reg: dict) -> dict:
|
||||
if not reg or not isinstance(reg, dict):
|
||||
return {}
|
||||
types = reg.get("types", {}) if isinstance(reg.get("types"), dict) else {}
|
||||
if note_type and isinstance(note_type, str) and note_type in types:
|
||||
return types[note_type] or {}
|
||||
# Fallback: concept
|
||||
return types.get("concept", {}) or {}
|
||||
|
||||
def resolve_note_type(requested: Optional[str], reg: dict) -> str:
|
||||
if requested and isinstance(requested, str):
|
||||
return requested
|
||||
# Fallback wenn nichts gesetzt ist
|
||||
types = reg.get("types", {}) if isinstance(reg.get("types"), dict) else {}
|
||||
return "concept" if "concept" in types else (requested or "concept")
|
||||
|
||||
def effective_chunk_profile(note_type: str, reg: dict) -> Optional[str]:
|
||||
"""Resolve chunk_profile for type or from defaults/global.
|
||||
Accepts symbolic profiles: short|medium|long|default.
|
||||
"""
|
||||
cfg = get_type_config(note_type, reg)
|
||||
prof = (cfg.get("chunk_profile") if isinstance(cfg, dict) else None)
|
||||
if isinstance(prof, str) and prof:
|
||||
return prof
|
||||
# defaults fallbacks
|
||||
for key in ("defaults", "default", "global"):
|
||||
dcfg = reg.get(key) if isinstance(reg, dict) else None
|
||||
if isinstance(dcfg, dict):
|
||||
dprof = dcfg.get("chunk_profile")
|
||||
if isinstance(dprof, str) and dprof:
|
||||
return dprof
|
||||
return "default"
|
||||
|
||||
def effective_retriever_weight(note_type: str, reg: dict) -> Optional[float]:
|
||||
"""Resolve retriever_weight for type or defaults; returns float.
|
||||
"""
|
||||
cfg = get_type_config(note_type, reg)
|
||||
w = (cfg.get("retriever_weight") if isinstance(cfg, dict) else None)
|
||||
try:
|
||||
if w is not None:
|
||||
return float(w)
|
||||
except Exception:
|
||||
pass
|
||||
# defaults fallbacks
|
||||
for key in ("defaults", "default", "global"):
|
||||
dcfg = reg.get(key) if isinstance(reg, dict) else None
|
||||
if isinstance(dcfg, dict):
|
||||
dw = dcfg.get("retriever_weight")
|
||||
async def process_with_limit(f_path):
|
||||
async with sem:
|
||||
try:
|
||||
if dw is not None:
|
||||
return float(dw)
|
||||
except Exception:
|
||||
pass
|
||||
return 1.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Sonstige Helper
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
def iter_md(root: str) -> List[str]:
|
||||
out: List[str] = []
|
||||
for dirpath, _, filenames in os.walk(root):
|
||||
for fn in filenames:
|
||||
if not fn.lower().endswith(".md"):
|
||||
continue
|
||||
p = os.path.join(dirpath, fn)
|
||||
pn = p.replace("\\", "/")
|
||||
if any(ex in pn for ex in ["/.obsidian/", "/_backup_frontmatter/", "/_imported/"]):
|
||||
continue
|
||||
out.append(p)
|
||||
return sorted(out)
|
||||
|
||||
def collections(prefix: str) -> Tuple[str, str, str]:
|
||||
return f"{prefix}_notes", f"{prefix}_chunks", f"{prefix}_edges"
|
||||
|
||||
def fetch_existing_note_payload(client, prefix: str, note_id: str) -> Optional[Dict]:
|
||||
notes_col, _, _ = collections(prefix)
|
||||
f = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
|
||||
points, _ = client.scroll(
|
||||
collection_name=notes_col,
|
||||
scroll_filter=f, # wichtig: scroll_filter (nicht: filter)
|
||||
with_payload=True,
|
||||
with_vectors=False,
|
||||
limit=1,
|
||||
)
|
||||
if not points:
|
||||
return None
|
||||
return points[0].payload or {}
|
||||
|
||||
def list_qdrant_note_ids(client, prefix: str) -> Set[str]:
|
||||
notes_col, _, _ = collections(prefix)
|
||||
out: Set[str] = set()
|
||||
next_page = None
|
||||
while True:
|
||||
pts, next_page = client.scroll(
|
||||
collection_name=notes_col,
|
||||
with_payload=True,
|
||||
with_vectors=False,
|
||||
limit=256,
|
||||
offset=next_page,
|
||||
)
|
||||
if not pts:
|
||||
break
|
||||
for p in pts:
|
||||
pl = p.payload or {}
|
||||
nid = pl.get("note_id")
|
||||
if isinstance(nid, str):
|
||||
out.add(nid)
|
||||
if next_page is None:
|
||||
break
|
||||
return out
|
||||
|
||||
def purge_note_artifacts(client, prefix: str, note_id: str) -> None:
|
||||
_, chunks_col, edges_col = collections(prefix)
|
||||
filt = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
|
||||
for col in (chunks_col, edges_col):
|
||||
try:
|
||||
client.delete(
|
||||
collection_name=col,
|
||||
points_selector=rest.FilterSelector(filter=filt),
|
||||
wait=True
|
||||
)
|
||||
except Exception as e:
|
||||
print(json.dumps({"note_id": note_id, "warn": f"delete in {col} via filter failed: {e}"}))
|
||||
|
||||
def delete_note_everywhere(client, prefix: str, note_id: str) -> None:
|
||||
notes_col, chunks_col, edges_col = collections(prefix)
|
||||
filt = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
|
||||
for col in (edges_col, chunks_col, notes_col):
|
||||
try:
|
||||
client.delete(
|
||||
collection_name=col,
|
||||
points_selector=rest.FilterSelector(filter=filt),
|
||||
wait=True
|
||||
)
|
||||
except Exception as e:
|
||||
print(json.dumps({"note_id": note_id, "warn": f"delete in {col} failed: {e}"}))
|
||||
|
||||
|
||||
# --- Neu: Existenz-Checks für Artefakte (fehlertoleranter Rebuild) ---
|
||||
|
||||
def _has_any_point(client, collection: str, note_id: str) -> bool:
|
||||
"""Prüft, ob es mind. einen Punkt mit note_id in der Collection gibt."""
|
||||
filt = rest.Filter(must=[rest.FieldCondition(key="note_id", match=rest.MatchValue(value=note_id))])
|
||||
pts, _ = client.scroll(
|
||||
collection_name=collection,
|
||||
scroll_filter=filt,
|
||||
with_payload=False,
|
||||
with_vectors=False,
|
||||
limit=1,
|
||||
)
|
||||
return bool(pts)
|
||||
|
||||
def artifacts_missing(client, prefix: str, note_id: str) -> Tuple[bool, bool]:
|
||||
"""Gibt (chunks_missing, edges_missing) zurück."""
|
||||
_, chunks_col, edges_col = collections(prefix)
|
||||
chunks_missing = not _has_any_point(client, chunks_col, note_id)
|
||||
edges_missing = not _has_any_point(client, edges_col, note_id)
|
||||
return chunks_missing, edges_missing
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
def _resolve_mode(m: Optional[str]) -> str:
|
||||
m = (m or "body").strip().lower()
|
||||
return m if m in {"body", "frontmatter", "full"} else "body"
|
||||
|
||||
def main() -> None:
|
||||
load_dotenv()
|
||||
|
||||
ap = argparse.ArgumentParser(
|
||||
prog="scripts.import_markdown",
|
||||
description="Importiert Markdown-Notizen in Qdrant (Notes/Chunks/Edges)."
|
||||
)
|
||||
ap.add_argument("--vault", required=True, help="Pfad zum Vault (Ordner mit .md-Dateien)")
|
||||
ap.add_argument("--only-path", help="Nur diese Datei verarbeiten (absolut oder relativ)")
|
||||
ap.add_argument("--apply", action="store_true", help="Schreibt nach Qdrant (sonst Dry-Run)")
|
||||
ap.add_argument("--purge-before-upsert", action="store_true", help="Alte Chunks/Edges der Note vorher löschen")
|
||||
ap.add_argument("--force-replace", action="store_true", help="Note/Chunks/Edges unabhängig von Hash neu schreiben")
|
||||
ap.add_argument("--note-id", help="Nur Notes mit dieser ID verarbeiten (Filter)")
|
||||
ap.add_argument("--note-scope-refs", action="store_true", help="Note-scope References/Backlinks erzeugen")
|
||||
ap.add_argument("--hash-mode", help="body|frontmatter|full (Default body)")
|
||||
ap.add_argument("--hash-source", help="parsed|raw (Default parsed)")
|
||||
ap.add_argument("--hash-normalize", help="canonical|none (Default canonical)")
|
||||
ap.add_argument("--compare-text", action="store_true", help="Parsed fulltext zusätzlich direkt vergleichen")
|
||||
ap.add_argument("--baseline-modes", action="store_true", help="Fehlende Hash-Varianten still nachtragen (Notes)")
|
||||
ap.add_argument("--sync-deletes", action="store_true", help="Qdrant->Vault Lösch-Sync (Dry-Run; mit --apply ausführen)")
|
||||
ap.add_argument("--prefix", help="Collection-Prefix (überschreibt ENV COLLECTION_PREFIX)")
|
||||
args = ap.parse_args()
|
||||
|
||||
mode = _resolve_mode(args.hash_mode) # body|frontmatter|full
|
||||
src = _env("MINDNET_HASH_SOURCE", args.hash_source or "parsed") # parsed|raw
|
||||
norm = _env("MINDNET_HASH_NORMALIZE", args.hash_normalize or "canonical") # canonical|none
|
||||
note_scope_refs_env = (_env("MINDNET_NOTE_SCOPE_REFS", "false") == "true")
|
||||
note_scope_refs = args.note_scope_refs or note_scope_refs_env
|
||||
compare_text = args.compare_text or (_env("MINDNET_COMPARE_TEXT", "false") == "true")
|
||||
|
||||
# Qdrant
|
||||
cfg = QdrantConfig.from_env()
|
||||
if args.prefix:
|
||||
cfg.prefix = args.prefix.strip()
|
||||
client = get_client(cfg)
|
||||
ensure_collections(client, cfg.prefix, cfg.dim)
|
||||
ensure_payload_indexes(client, cfg.prefix)
|
||||
|
||||
# Type-Registry laden (optional)
|
||||
reg = load_type_registry()
|
||||
|
||||
root = os.path.abspath(args.vault)
|
||||
|
||||
# Dateiliste
|
||||
if args.only_path:
|
||||
only = os.path.abspath(args.only_path)
|
||||
files = [only]
|
||||
else:
|
||||
files = iter_md(root)
|
||||
if not files:
|
||||
print("Keine Markdown-Dateien gefunden.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# Optional: Sync-Deletes vorab
|
||||
if args.sync_deletes:
|
||||
vault_note_ids: Set[str] = set()
|
||||
for path in files:
|
||||
try:
|
||||
parsed = read_markdown(path)
|
||||
if not parsed:
|
||||
continue
|
||||
fm = normalize_frontmatter(parsed.frontmatter)
|
||||
nid = fm.get("id")
|
||||
if isinstance(nid, str):
|
||||
vault_note_ids.add(nid)
|
||||
except Exception:
|
||||
continue
|
||||
qdrant_note_ids = list_qdrant_note_ids(client, cfg.prefix)
|
||||
to_delete = sorted(qdrant_note_ids - vault_note_ids)
|
||||
print(json.dumps({
|
||||
"action": "sync-deletes",
|
||||
"prefix": cfg.prefix,
|
||||
"qdrant_total": len(qdrant_note_ids),
|
||||
"vault_total": len(vault_note_ids),
|
||||
"to_delete_count": len(to_delete),
|
||||
"to_delete": to_delete[:50] + (["…"] if len(to_delete) > 50 else [])
|
||||
}, ensure_ascii=False))
|
||||
if args.apply and to_delete:
|
||||
for nid in to_delete:
|
||||
print(json.dumps({"action": "delete", "note_id": nid, "decision": "apply"}))
|
||||
delete_note_everywhere(client, cfg.prefix, nid)
|
||||
|
||||
key_current = f"{mode}:{src}:{norm}"
|
||||
|
||||
processed = 0
|
||||
for path in files:
|
||||
try:
|
||||
parsed = read_markdown(path)
|
||||
if not parsed:
|
||||
continue
|
||||
except Exception as e:
|
||||
print(json.dumps({"path": path, "error": f"read_markdown failed: {type(e).__name__}: {e}"}))
|
||||
continue
|
||||
|
||||
# --- Frontmatter prüfen ---
|
||||
try:
|
||||
fm = normalize_frontmatter(parsed.frontmatter)
|
||||
validate_required_frontmatter(fm)
|
||||
except Exception as e:
|
||||
print(json.dumps({"path": path, "error": f"Frontmatter invalid: {type(e).__name__}: {e}"}))
|
||||
continue
|
||||
|
||||
if args.note_id and not args.only_path and fm.get("id") != args.note_id:
|
||||
continue
|
||||
|
||||
processed += 1
|
||||
|
||||
# --- Type-Registry anwenden (chunk_profile / retriever_weight) ---
|
||||
try:
|
||||
note_type = resolve_note_type(fm.get("type"), reg)
|
||||
except Exception:
|
||||
note_type = (fm.get("type") or "concept")
|
||||
fm["type"] = note_type or fm.get("type") or "concept"
|
||||
|
||||
prof = effective_chunk_profile(note_type, reg)
|
||||
if prof:
|
||||
fm["chunk_profile"] = prof
|
||||
|
||||
weight = effective_retriever_weight(note_type, reg)
|
||||
if weight is not None:
|
||||
try:
|
||||
fm["retriever_weight"] = float(weight)
|
||||
except Exception:
|
||||
pass # falls FM string-inkonsistent ist
|
||||
|
||||
# --- Payload aufbauen (inkl. Hashes) ---
|
||||
try:
|
||||
note_pl = make_note_payload(
|
||||
parsed,
|
||||
vault_root=root,
|
||||
hash_mode=mode,
|
||||
hash_normalize=norm,
|
||||
hash_source=src,
|
||||
file_path=path,
|
||||
)
|
||||
except Exception as e:
|
||||
print(json.dumps({"path": path, "error": f"make_note_payload failed: {type(e).__name__}: {e}"}))
|
||||
continue
|
||||
|
||||
if not note_pl.get("fulltext"):
|
||||
note_pl["fulltext"] = getattr(parsed, "body", "") or ""
|
||||
|
||||
# retriever_weight sicher in Note-Payload spiegeln (für spätere Filter)
|
||||
if "retriever_weight" not in note_pl and fm.get("retriever_weight") is not None:
|
||||
try:
|
||||
note_pl["retriever_weight"] = float(fm.get("retriever_weight"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
note_id = note_pl.get("note_id") or fm.get("id")
|
||||
if not note_id:
|
||||
print(json.dumps({"path": path, "error": "Missing note_id after payload build"}))
|
||||
continue
|
||||
|
||||
# --- bestehenden Payload laden (zum Diff) ---
|
||||
old_payload = None if args.force_replace else fetch_existing_note_payload(client, cfg.prefix, note_id)
|
||||
has_old = old_payload is not None
|
||||
|
||||
old_hashes = (old_payload or {}).get("hashes") or {}
|
||||
old_hash_exact = old_hashes.get(key_current)
|
||||
new_hash_exact = (note_pl.get("hashes") or {}).get(key_current)
|
||||
needs_baseline = (old_hash_exact is None)
|
||||
|
||||
hash_changed = (old_hash_exact is not None and new_hash_exact is not None and old_hash_exact != new_hash_exact)
|
||||
|
||||
text_changed = False
|
||||
if compare_text:
|
||||
old_text = (old_payload or {}).get("fulltext") or ""
|
||||
new_text = note_pl.get("fulltext") or ""
|
||||
text_changed = (old_text != new_text)
|
||||
|
||||
changed = args.force_replace or (not has_old) or hash_changed or text_changed
|
||||
do_baseline_only = (args.baseline_modes and has_old and needs_baseline and not changed)
|
||||
|
||||
# --- Chunks + Embeddings vorbereiten ---
|
||||
try:
|
||||
body_text = getattr(parsed, "body", "") or ""
|
||||
chunks = assemble_chunks(fm["id"], body_text, fm.get("type", "concept"))
|
||||
chunk_pls: List[Dict[str, Any]] = make_chunk_payloads(fm, note_pl["path"], chunks, note_text=body_text)
|
||||
except Exception as e:
|
||||
print(json.dumps({"path": path, "note_id": note_id, "error": f"chunk build failed: {type(e).__name__}: {e}"}))
|
||||
continue
|
||||
|
||||
# retriever_weight auf Chunk-Payload spiegeln
|
||||
if fm.get("retriever_weight") is not None:
|
||||
try:
|
||||
rw = float(fm.get("retriever_weight"))
|
||||
for pl in chunk_pls:
|
||||
# Feld nur setzen, wenn noch nicht vorhanden
|
||||
if "retriever_weight" not in pl:
|
||||
pl["retriever_weight"] = rw
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Embeddings (fallback: Nullvektoren)
|
||||
vecs: List[List[float]] = [[0.0] * int(cfg.dim) for _ in chunk_pls]
|
||||
if embed_texts and chunk_pls:
|
||||
try:
|
||||
texts_for_embed = [(pl.get("window") or pl.get("text") or "") for pl in chunk_pls]
|
||||
vecs = embed_texts(texts_for_embed)
|
||||
except Exception as e:
|
||||
print(json.dumps({"path": path, "note_id": note_id, "warn": f"embed_texts failed, using zeros: {e}"}))
|
||||
|
||||
# --- Fehlende Artefakte in Qdrant ermitteln ---
|
||||
chunks_missing, edges_missing = artifacts_missing(client, cfg.prefix, note_id)
|
||||
|
||||
# --- Edges (robust) ---
|
||||
edges: List[Dict[str, Any]] = []
|
||||
edges_failed = False
|
||||
should_build_edges = (changed and (not do_baseline_only)) or edges_missing
|
||||
if should_build_edges:
|
||||
try:
|
||||
note_refs = note_pl.get("references") or []
|
||||
edges = build_edges_for_note(
|
||||
note_id,
|
||||
chunk_pls,
|
||||
note_level_references=note_refs,
|
||||
include_note_scope_refs=note_scope_refs,
|
||||
res = await service.process_file(
|
||||
file_path=str(f_path),
|
||||
vault_root=str(vault_path),
|
||||
force_replace=args.force,
|
||||
apply=args.apply,
|
||||
purge_before=True
|
||||
)
|
||||
return res
|
||||
except Exception as e:
|
||||
edges_failed = True
|
||||
edges = []
|
||||
print(json.dumps({"path": path, "note_id": note_id, "warn": f"build_edges_for_note failed, skipping edges: {type(e).__name__}: {e}"}))
|
||||
return {"status": "error", "error": str(e), "path": str(f_path)}
|
||||
|
||||
# --- Summary (stdout) ---
|
||||
summary = {
|
||||
"note_id": note_id,
|
||||
"title": fm.get("title"),
|
||||
"chunks": len(chunk_pls),
|
||||
"edges": len(edges),
|
||||
"edges_failed": edges_failed,
|
||||
"changed": changed,
|
||||
"chunks_missing": chunks_missing,
|
||||
"edges_missing": edges_missing,
|
||||
"needs_baseline_for_mode": needs_baseline,
|
||||
"decision": ("baseline-only" if args.apply and do_baseline_only else
|
||||
"apply" if args.apply and (changed or chunks_missing or edges_missing) else
|
||||
"apply-skip-unchanged" if args.apply and not (changed or chunks_missing or edges_missing) else
|
||||
"dry-run"),
|
||||
"path": note_pl["path"],
|
||||
"hash_mode": mode,
|
||||
"hash_normalize": norm,
|
||||
"hash_source": src,
|
||||
"prefix": cfg.prefix,
|
||||
}
|
||||
print(json.dumps(summary, ensure_ascii=False))
|
||||
# Batch Processing
|
||||
# Wir verarbeiten in Chunks, um den Progress zu sehen
|
||||
batch_size = 20
|
||||
for i in range(0, len(files), batch_size):
|
||||
batch = files[i:i+batch_size]
|
||||
logger.info(f"Processing batch {i} to {i+len(batch)}...")
|
||||
|
||||
tasks = [process_with_limit(f) for f in batch]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
for res in results:
|
||||
if res.get("status") == "success":
|
||||
stats["processed"] += 1
|
||||
elif res.get("status") == "error":
|
||||
stats["errors"] += 1
|
||||
logger.error(f"Error in {res.get('path')}: {res.get('error')}")
|
||||
else:
|
||||
stats["skipped"] += 1
|
||||
|
||||
# --- Writes ---
|
||||
if not args.apply:
|
||||
continue
|
||||
logger.info(f"Done. Stats: {stats}")
|
||||
if not args.apply:
|
||||
logger.info("DRY RUN. Use --apply to write to DB.")
|
||||
|
||||
if do_baseline_only:
|
||||
merged_hashes = {}
|
||||
merged_hashes.update(old_hashes)
|
||||
merged_hashes.update(note_pl.get("hashes") or {})
|
||||
if old_payload:
|
||||
note_pl["hash_fulltext"] = old_payload.get("hash_fulltext", note_pl.get("hash_fulltext"))
|
||||
note_pl["hash_signature"] = old_payload.get("hash_signature", note_pl.get("hash_signature"))
|
||||
note_pl["hashes"] = merged_hashes
|
||||
notes_name, note_pts = points_for_note(cfg.prefix, note_pl, None, cfg.dim)
|
||||
upsert_batch(client, notes_name, note_pts)
|
||||
continue
|
||||
|
||||
# Wenn nichts geändert und keine Artefakte fehlen → nichts zu tun
|
||||
if not changed and not (chunks_missing or edges_missing):
|
||||
continue
|
||||
|
||||
# Purge nur bei echten Änderungen (unverändert + fehlende Artefakte ≠ Purge)
|
||||
if args.purge_before_upsert and has_old and changed:
|
||||
try:
|
||||
purge_note_artifacts(client, cfg.prefix, note_id)
|
||||
except Exception as e:
|
||||
print(json.dumps({"path": path, "note_id": note_id, "warn": f"purge failed: {e}"}))
|
||||
|
||||
# Note nur bei Änderungen neu schreiben
|
||||
if changed:
|
||||
notes_name, note_pts = points_for_note(cfg.prefix, note_pl, None, cfg.dim)
|
||||
upsert_batch(client, notes_name, note_pts)
|
||||
|
||||
# Chunks schreiben, wenn geändert ODER vorher fehlend
|
||||
if chunk_pls and (changed or chunks_missing):
|
||||
chunks_name, chunk_pts = points_for_chunks(cfg.prefix, chunk_pls, vecs)
|
||||
upsert_batch(client, chunks_name, chunk_pts)
|
||||
|
||||
# Edges schreiben, wenn vorhanden und (geändert ODER vorher fehlend)
|
||||
if edges and (changed or edges_missing):
|
||||
edges_name, edge_pts = points_for_edges(cfg.prefix, edges)
|
||||
upsert_batch(client, edges_name, edge_pts)
|
||||
|
||||
print(f"Done. Processed notes: {processed}")
|
||||
def main():
|
||||
load_dotenv()
|
||||
default_prefix = os.getenv("COLLECTION_PREFIX", "mindnet")
|
||||
|
||||
parser = argparse.ArgumentParser(description="Import Vault to Qdrant (Async)")
|
||||
parser.add_argument("--vault", default="./vault", help="Path to vault root")
|
||||
parser.add_argument("--prefix", default=default_prefix, help="Collection prefix")
|
||||
parser.add_argument("--force", action="store_true", help="Force re-index all files")
|
||||
parser.add_argument("--apply", action="store_true", help="Perform writes to Qdrant")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Starte den Async Loop
|
||||
asyncio.run(main_async(args))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Name: scripts/reset_qdrant.py
|
||||
Version: v1.2.0 (2025-11-11)
|
||||
Version: v1.2.1 (2025-12-11)
|
||||
Kurzbeschreibung:
|
||||
Sicheres Zurücksetzen der Qdrant-Collections für EIN Projektpräfix. Das Skript
|
||||
ermittelt zunächst die tatsächlich betroffenen Collections und zeigt eine
|
||||
|
|
@ -39,6 +39,7 @@ Exitcodes:
|
|||
0 = OK, 1 = abgebrochen/keine Aktion, 2 = Verbindungs-/Konfigurationsfehler
|
||||
|
||||
Changelog:
|
||||
v1.2.1: Fix: load_dotenv() hinzugefügt, damit VECTOR_DIM aus .env gelesen wird.
|
||||
v1.2.0: ensure_payload_indexes() nach wipe/truncate standardmäßig ausführen (idempotent); --no-indexes Flag ergänzt.
|
||||
v1.1.1: Stabilisierung & Preview (2025-09-05).
|
||||
v1.1.0: Interaktive Bestätigung, --yes/--dry-run hinzugefügt, Preview der betroffenen Collections.
|
||||
|
|
@ -50,6 +51,9 @@ import os
|
|||
import sys
|
||||
from typing import List
|
||||
|
||||
# FIX: Dotenv laden
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from qdrant_client import QdrantClient
|
||||
from qdrant_client.http import models as rest
|
||||
|
||||
|
|
@ -124,6 +128,9 @@ def wipe_collections(client: QdrantClient, all_col_names: List[str], existing: L
|
|||
|
||||
|
||||
def main():
|
||||
# FIX: Umgebungsvariablen aus .env laden
|
||||
load_dotenv()
|
||||
|
||||
ap = argparse.ArgumentParser(description="Wipe oder truncate mindnet-Collections in Qdrant (mit Bestätigung & Index-Setup).")
|
||||
ap.add_argument("--mode", choices=["wipe", "truncate"], required=True,
|
||||
help="wipe = Collections löschen & neu anlegen; truncate = nur Inhalte löschen")
|
||||
|
|
@ -135,6 +142,7 @@ def main():
|
|||
|
||||
# Qdrant-Konfiguration
|
||||
try:
|
||||
# Hier wird jetzt VECTOR_DIM=768 korrekt berücksichtigt
|
||||
cfg = QdrantConfig.from_env()
|
||||
except Exception as e:
|
||||
print(f"Konfigurationsfehler: {e}", file=sys.stderr)
|
||||
|
|
@ -156,6 +164,9 @@ def main():
|
|||
existing = resolve_existing_collections(client, cfg.prefix)
|
||||
nonexisting = [c for c in all_col_names if c not in existing]
|
||||
|
||||
# Debug-Info zur Dimension
|
||||
print(f"Info: Nutze Vektor-Dimension: {cfg.dim}")
|
||||
|
||||
# Preview & Bestätigung
|
||||
if not confirm_or_abort(args.mode, existing, nonexisting, args.yes):
|
||||
print("Abgebrochen – keine Änderungen vorgenommen.")
|
||||
|
|
@ -188,4 +199,4 @@ def main():
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user