Update qdrant_points.py, graph_utils.py, ingestion_db.py, ingestion_processor.py, and import_markdown.py: Enhance UUID generation for edge IDs, improve error handling, and refine documentation for clarity. Implement atomic consistency in batch upserts and ensure strict phase separation in the ingestion workflow. Update versioning to reflect changes in functionality and maintain compatibility with the ingestion service.
137 lines
5.4 KiB
Python
137 lines
5.4 KiB
Python
"""
|
|
FILE: app/core/graph/graph_utils.py
|
|
DESCRIPTION: Basale Werkzeuge, ID-Generierung und Provenance-Konfiguration für den Graphen.
|
|
AUDIT v1.6.0:
|
|
- Erweitert um parse_link_target für sauberes Section-Splitting.
|
|
- Einführung einer gehärteten, deterministischen ID-Berechnung für Kanten (WP-24c).
|
|
- Integration der .env-gesteuerten Pfadauflösung für Schema und Vokabular.
|
|
VERSION: 1.6.0 (WP-24c: Identity & Path Enforcement)
|
|
STATUS: Active
|
|
"""
|
|
import os
|
|
import uuid
|
|
import hashlib
|
|
from typing import Iterable, List, Optional, Set, Any, Tuple
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
yaml = None
|
|
|
|
# WP-15b: Prioritäten-Ranking für die De-Duplizierung von Kanten unterschiedlicher Herkunft
|
|
PROVENANCE_PRIORITY = {
|
|
"explicit:wikilink": 1.00,
|
|
"inline:rel": 0.95,
|
|
"callout:edge": 0.90,
|
|
"semantic_ai": 0.90, # Validierte KI-Kanten
|
|
"structure:belongs_to": 1.00,
|
|
"structure:order": 0.95, # next/prev
|
|
"explicit:note_scope": 1.00,
|
|
"derived:backlink": 0.90,
|
|
"edge_defaults": 0.70 # Heuristik basierend auf types.yaml
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pfad-Auflösung (Integration der .env Umgebungsvariablen)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def get_vocab_path() -> str:
|
|
"""Liefert den Pfad zum Edge-Vokabular aus der .env oder den Default."""
|
|
return os.getenv("MINDNET_VOCAB_PATH", "/mindnet/vault/mindnet/_system/dictionary/edge_vocabulary.md")
|
|
|
|
def get_schema_path() -> str:
|
|
"""Liefert den Pfad zum Graph-Schema aus der .env oder den Default."""
|
|
return os.getenv("MINDNET_SCHEMA_PATH", "/mindnet/vault/mindnet/_system/dictionary/graph_schema.md")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ID & String Helper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _get(d: dict, *keys, default=None):
|
|
"""Sicherer Zugriff auf tief verschachtelte Dictionary-Keys."""
|
|
for k in keys:
|
|
if isinstance(d, dict) and k in d and d[k] is not None:
|
|
return d[k]
|
|
return default
|
|
|
|
def _dedupe_seq(seq: Iterable[str]) -> List[str]:
|
|
"""Dedupliziert eine Sequenz von Strings unter Beibehaltung der Reihenfolge."""
|
|
seen = set()
|
|
return [x for x in seq if not (x in seen or seen.add(x))]
|
|
|
|
def parse_link_target(raw: str, current_note_id: Optional[str] = None) -> Tuple[str, Optional[str]]:
|
|
"""
|
|
Trennt einen Obsidian-Link [[Target#Section]] in seine Bestandteile Target und Section.
|
|
Behandelt Self-Links (z.B. [[#Ziele]]), indem die aktuelle note_id eingesetzt wird.
|
|
|
|
Returns:
|
|
Tuple (target_id, target_section)
|
|
"""
|
|
if not raw:
|
|
return "", None
|
|
|
|
parts = raw.split("#", 1)
|
|
target = parts[0].strip()
|
|
section = parts[1].strip() if len(parts) > 1 else None
|
|
|
|
# Spezialfall: Self-Link innerhalb derselben Datei
|
|
if not target and section and current_note_id:
|
|
target = current_note_id
|
|
|
|
return target, section
|
|
|
|
def _mk_edge_id(kind: str, source_id: str, target_id: str, scope: str = "note") -> str:
|
|
"""
|
|
WP-24c: Erzeugt eine deterministische UUIDv5 für eine Kante.
|
|
Garantiert, dass explizite Links und systemgenerierte Symmetrien dieselbe Point-ID
|
|
erzeugen, sofern Quelle und Ziel identisch aufgelöst wurden.
|
|
|
|
Args:
|
|
kind: Typ der Relation (z.B. 'references')
|
|
source_id: Kanonische ID der Quell-Note
|
|
target_id: Kanonische ID der Ziel-Note
|
|
scope: Granularität (z.B. 'note' oder 'chunk')
|
|
"""
|
|
# Hard-Guard gegen None-Werte zur Vermeidung von Pydantic-Validierungsfehlern
|
|
if not all([kind, source_id, target_id]):
|
|
raise ValueError(f"Incomplete data for edge ID: kind={kind}, src={source_id}, tgt={target_id}")
|
|
|
|
# Stabiler Schlüssel für die Kollisions-Strategie (Authority-First)
|
|
stable_key = f"edge:{kind}:{source_id}:{target_id}:{scope}"
|
|
|
|
# Nutzt den URL-Namespace für deterministische Reproduzierbarkeit
|
|
return str(uuid.uuid5(uuid.NAMESPACE_URL, stable_key))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registry Operations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def load_types_registry() -> dict:
|
|
"""
|
|
Lädt die zentrale YAML-Registry (types.yaml).
|
|
Pfad wird über die Umgebungsvariable MINDNET_TYPES_FILE gesteuert.
|
|
"""
|
|
p = os.getenv("MINDNET_TYPES_FILE", "./config/types.yaml")
|
|
if not os.path.isfile(p) or yaml is None:
|
|
return {}
|
|
try:
|
|
with open(p, "r", encoding="utf-8") as f:
|
|
data = yaml.safe_load(f)
|
|
return data if data is not None else {}
|
|
except Exception:
|
|
return {}
|
|
|
|
def get_edge_defaults_for(note_type: Optional[str], reg: dict) -> List[str]:
|
|
"""
|
|
Ermittelt die konfigurierten Standard-Kanten für einen Note-Typ.
|
|
Greift bei Bedarf auf die globalen ingestion_settings zurück.
|
|
"""
|
|
types_map = reg.get("types", reg) if isinstance(reg, dict) else {}
|
|
if note_type and isinstance(types_map, dict):
|
|
t_cfg = types_map.get(note_type)
|
|
if isinstance(t_cfg, dict) and isinstance(t_cfg.get("edge_defaults"), list):
|
|
return [str(x) for x in t_cfg["edge_defaults"]]
|
|
|
|
# Fallback auf die globalen Standardwerte der Ingestion
|
|
cfg_def = reg.get("ingestion_settings", {})
|
|
return cfg_def.get("edge_defaults", []) |