mindnet/scripts/import_markdown.py
Lars de5db09b51 Update logging levels in ingestion_processor.py and import_markdown.py for improved visibility
Change debug logs to info and warning levels in ingestion_processor.py to enhance the visibility of change detection processes, including hash comparisons and artifact checks. Additionally, ensure .env is loaded before logging setup in import_markdown.py to correctly read the DEBUG environment variable. These adjustments aim to improve traceability and debugging during ingestion workflows.
2026-01-12 08:13:26 +01:00

265 lines
12 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
FILE: scripts/import_markdown.py
VERSION: 2.6.2 (WP-24c: Gold-Standard v4.1.0)
STATUS: Active (Core)
COMPATIBILITY: IngestionProcessor v4.0.0+, graph_utils v4.1.0+
Zweck:
-------
Hauptwerkzeug zum Importieren von Markdown-Dateien aus einem lokalen Obsidian-Vault in die
Qdrant Vektor-Datenbank. Das Script ist darauf optimiert, die strukturelle Integrität des
Wissensgraphen zu wahren und die manuelle Nutzer-Autorität vor automatisierten System-Eingriffen
zu schützen.
Hintergrund der 2-Phasen-Schreibstrategie (Authority-First):
------------------------------------------------------------
Um das Problem der "Ghost-IDs" (Links auf Titel statt IDs) und der asynchronen Überschreibungen
(Symmetrien löschen manuelle Kanten) zu lösen, implementiert dieses Script eine strikte
Trennung der Arbeitsabläufe:
1. PASS 1: Global Context Discovery (Pre-Scan)
- Scannt den gesamten Vault, um ein Mapping von Titeln/Dateinamen zu Note-IDs aufzubauen.
- Dieser Cache wird dem IngestionService übergeben, damit Wikilinks wie [[Klaus]]
während der Verarbeitung sofort in die korrekte Zeitstempel-ID (z.B. 202601031726-klaus)
aufgelöst werden können.
- Dies verhindert die Erzeugung falscher UUIDs durch unaufgelöste Bezeichnungen.
2. PHASE 1: Authority Processing (Schreib-Durchlauf)
- Alle validen Dateien werden in Batches verarbeitet.
- Notizen, Chunks und explizite (vom Nutzer manuell gesetzte) Kanten werden sofort geschrieben.
- Durch die Verwendung von 'wait=True' in der Datenbank-Layer (qdrant_points) wird
sichergestellt, dass diese Informationen physisch indiziert sind, bevor Phase 2 startet.
- Symmetrische Gegenkanten werden während dieser Phase lediglich im Speicher gepuffert.
3. PHASE 2: Global Symmetry Commitment (Integritäts-Sicherung)
- Erst nach Abschluss aller Batches wird die Methode commit_vault_symmetries() aufgerufen.
- Diese prüft die gepufferten Symmetrie-Vorschläge gegen die bereits existierende
Nutzer-Autorität in der Datenbank.
- Dank der in graph_utils v1.6.2 zentralisierten ID-Logik (_mk_edge_id) erkennt das
System Kollisionen hunderprozentig: Existiert bereits eine manuelle Kante für dieselbe
Verbindung, wird die automatische Symmetrie unterdrückt.
Detaillierte Funktionsweise:
----------------------------
- Ordner-Filter: Schließt System-Ordner wie .trash, .obsidian, .sync sowie Vorlagen konsequent aus.
- Cloud-Resilienz: Implementiert Semaphoren zur Begrenzung paralleler API-Zugriffe (max. 5).
- Mixture of Experts (MoE): Nutzt LLM-Validierung zur intelligenten Zuweisung von Kanten.
- Change Detection: Vergleicht Hashes, um redundante Schreibvorgänge zu vermeiden.
Ergebnis-Interpretation:
------------------------
- Log-Ausgabe: Zeigt detailliert den Fortschritt, LLM-Entscheidungen (✅ OK / ❌ SKIP)
und den Status der Symmetrie-Injektion.
- Statistiken: Gibt am Ende eine Zusammenfassung über Erfolg, Übersprungene (Hash identisch)
und Fehler (z.B. fehlendes Frontmatter).
Verwendung:
-----------
- Initialer Aufbau: python3 -m scripts.import_markdown --vault /pfad/zum/vault --apply
- Update-Lauf: Das Script erkennt Änderungen automatisch via Change Detection.
- Erzwingung: Mit --force wird die Hash-Prüfung ignoriert und alles neu indiziert.
"""
import asyncio
import os
import argparse
import logging
import sys
from pathlib import Path
from typing import List, Dict, Any
from dotenv import load_dotenv
# WP-24c v4.5.9: Lade .env VOR dem Logging-Setup, damit DEBUG=true korrekt gelesen wird
load_dotenv()
# Root Logger Setup: Nutzt zentrale setup_logging() Funktion
# WP-24c v4.4.0-DEBUG: Aktiviert DEBUG-Level für End-to-End Tracing
# Kann auch über Umgebungsvariable DEBUG=true gesteuert werden
from app.core.logging_setup import setup_logging
# Bestimme Log-Level basierend auf DEBUG Umgebungsvariable (nach load_dotenv!)
debug_mode = os.getenv("DEBUG", "false").lower() == "true"
log_level = logging.DEBUG if debug_mode else logging.INFO
# Nutze zentrale Logging-Konfiguration (File + Console)
setup_logging(log_level=log_level)
# Sicherstellung, dass das Root-Verzeichnis im Python-Pfad liegt
sys.path.append(os.getcwd())
# App-spezifische Imports
from app.core.ingestion import IngestionService
from app.core.parser import pre_scan_markdown
logger = logging.getLogger("importer")
async def main_async(args):
"""
Haupt-Workflow der Ingestion. Koordiniert die zwei Durchläufe (Pass 1/2)
und die zwei Schreibphasen (Phase 1/2).
"""
vault_path = Path(args.vault).resolve()
if not vault_path.exists():
logger.error(f"Vault-Pfad existiert nicht: {vault_path}")
return
# 1. Initialisierung des zentralen Ingestion-Services
# Nutzt IngestionProcessor v3.4.2 (initialisiert Registry mit .env Pfaden)
logger.info(f"Initializing IngestionService (Prefix: {args.prefix})")
service = IngestionService(collection_prefix=args.prefix)
logger.info(f"Scanning {vault_path}...")
all_files_raw = list(vault_path.rglob("*.md"))
# --- GLOBALER ORDNER-FILTER ---
# Diese Liste stellt sicher, dass keine System-Leichen oder temporäre Dateien
# den Graphen korrumpieren oder zu ID-Kollisionen führen.
files = []
# WP-24c v4.1.0: MINDNET_IGNORE_FOLDERS aus Umgebungsvariable
# Format: Komma-separierte Liste von Ordnernamen (z.B. "trash,temp,archive")
env_ignore = os.getenv("MINDNET_IGNORE_FOLDERS", "")
env_ignore_list = [f.strip() for f in env_ignore.split(",") if f.strip()] if env_ignore else []
# Standard-Ignore-Liste (System-Ordner)
default_ignore_list = [".trash", ".obsidian", ".sync", "templates", "_system", ".git"]
# Kombinierte Ignore-Liste (Umgebungsvariable hat Priorität, wird mit Defaults kombiniert)
ignore_list = list(set(default_ignore_list + env_ignore_list))
logger.info(f"📁 Ignore-Liste: {ignore_list}")
for f in all_files_raw:
f_str = str(f)
# Filtert Ordner aus der ignore_list und versteckte Verzeichnisse
if not any(folder in f_str for folder in ignore_list) and not "/." in f_str:
files.append(f)
files.sort()
logger.info(f"Found {len(files)} relevant markdown files (filtered trash/system/hidden).")
# =========================================================================
# PASS 1: Global Pre-Scan
# Ziel: Aufbau eines vollständigen Mappings von Bezeichnungen zu stabilen IDs.
# WICHTIG: Dies ist die Voraussetzung für die korrekte ID-Generierung in Phase 1.
# =========================================================================
logger.info(f"🔍 [Pass 1] Global Pre-Scan: Building context cache for {len(files)} files...")
for f_path in files:
try:
# Extrahiert Frontmatter und Metadaten ohne DB-Last
# Nutzt service.registry zur Typ-Auflösung
ctx = pre_scan_markdown(str(f_path), registry=service.registry)
if ctx:
# Mehrfache Indizierung für maximale Trefferrate bei Wikilinks
service.batch_cache[ctx.note_id] = ctx
service.batch_cache[ctx.title] = ctx
# Auch den Dateinamen ohne Endung als Alias hinterlegen
service.batch_cache[os.path.splitext(f_path.name)[0]] = ctx
except Exception as e:
logger.warning(f"⚠️ Pre-scan fehlgeschlagen für {f_path.name}: {e}")
# =========================================================================
# PHASE 1: Authority Processing (Batch-Lauf)
# Ziel: Verarbeitung der Dateiinhalte und Speicherung der Nutzer-Autorität.
# =========================================================================
stats = {"processed": 0, "skipped": 0, "errors": 0}
# Semaphore begrenzt die Parallelität zum Schutz der lokalen oder Cloud-API
sem = asyncio.Semaphore(5)
async def process_with_limit(f_path):
"""Kapselt den Prozess-Aufruf mit Ressourcen-Limitierung."""
async with sem:
try:
# Verwendet process_file (v3.4.2), das explizite Kanten sofort schreibt.
# Symmetrien werden im Service-Puffer gesammelt und NICHT sofort geschrieben.
return await service.process_file(
file_path=str(f_path),
vault_root=str(vault_path),
force_replace=args.force,
apply=args.apply,
purge_before=True
)
except Exception as e:
return {"status": "error", "error": str(e), "path": str(f_path)}
logger.info(f"🚀 [Phase 1] Starting semantic processing in batches...")
batch_size = 20
for i in range(0, len(files), batch_size):
batch = files[i:i+batch_size]
logger.info(f"--- Processing Batch {i//batch_size + 1} ({len(batch)} files) ---")
# Parallelisierung innerhalb des Batches (begrenzt durch sem)
tasks = [process_with_limit(f) for f in batch]
results = await asyncio.gather(*tasks)
for res in results:
# Robuste Auswertung der Rückgabe-Dictionaries
if not isinstance(res, dict):
stats["errors"] += 1
continue
status = res.get("status")
if status == "success":
stats["processed"] += 1
elif status == "error":
stats["errors"] += 1
logger.error(f"❌ Fehler in {res.get('path')}: {res.get('error')}")
elif status == "unchanged":
stats["skipped"] += 1
else:
stats["skipped"] += 1
# =========================================================================
# PHASE 2: Global Symmetry Commitment
# Ziel: Finale Integrität. Triggert erst, wenn Phase 1 komplett indiziert ist.
# Verwendet die identische ID-Logik aus graph_utils v1.6.2.
# =========================================================================
if args.apply:
logger.info(f"🔄 [Phase 2] Starting global symmetry injection for the entire vault...")
try:
# Diese Methode prüft den Puffer gegen die nun vollständige Datenbank.
# Verhindert Duplikate bei der 'Steinzeitaxt' durch Authority-Lookup.
sym_res = await service.commit_vault_symmetries()
if sym_res.get("status") == "success":
logger.info(f"✅ Phase 2 abgeschlossen. Hinzugefügt: {sym_res.get('added', 0)} geschützte Symmetrien.")
else:
logger.info(f"⏭️ Phase 2 übersprungen: {sym_res.get('reason', 'Keine Daten oder bereits vorhanden')}")
except Exception as e:
logger.error(f"❌ Fehler in Phase 2: {e}")
else:
logger.info("⏭️ [Phase 2] Dry-Run: Keine Symmetrie-Injektion durchgeführt.")
logger.info(f"--- Import beendet ---")
logger.info(f"Statistiken: {stats}")
def main():
"""Einstiegspunkt und Argument-Parsing."""
# WP-24c v4.5.9: load_dotenv() wurde bereits beim Modul-Import aufgerufen
# (oben, vor dem Logging-Setup, damit DEBUG=true korrekt gelesen wird)
# Standard-Präfix aus Umgebungsvariable oder Fallback
default_prefix = os.getenv("COLLECTION_PREFIX", "mindnet")
# Optionaler Vault-Root aus .env
default_vault = os.getenv("MINDNET_VAULT_ROOT", "./vault")
parser = argparse.ArgumentParser(description="Mindnet Ingester: Two-Phase Markdown Import")
parser.add_argument("--vault", default=default_vault, help="Pfad zum Obsidian Vault")
parser.add_argument("--prefix", default=default_prefix, help="Qdrant Collection Präfix")
parser.add_argument("--force", action="store_true", help="Erzwingt Neu-Indizierung aller Dateien")
parser.add_argument("--apply", action="store_true", help="Schreibt physisch in die Datenbank")
args = parser.parse_args()
try:
asyncio.run(main_async(args))
except KeyboardInterrupt:
logger.info("Import durch Nutzer abgebrochen.")
except Exception as e:
logger.critical(f"FATALER FEHLER: {e}", exc_info=True)
sys.exit(1)
if __name__ == "__main__":
main()