From c33b1c644a27cb12c6a3711aeb8e4444e838da85 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 10 Jan 2026 10:58:44 +0100 Subject: [PATCH] Update graph_utils.py to version 1.6.1: Restore '_edge' function to address ImportError, revert to UUIDv5 for Qdrant compatibility, and maintain section logic in ID generation. Enhance documentation for clarity and refine edge ID generation process. --- app/core/graph/graph_utils.py | 124 ++++++++++++++++++---------------- 1 file changed, 65 insertions(+), 59 deletions(-) diff --git a/app/core/graph/graph_utils.py b/app/core/graph/graph_utils.py index 457d1db..a05982b 100644 --- a/app/core/graph/graph_utils.py +++ b/app/core/graph/graph_utils.py @@ -1,11 +1,12 @@ """ 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) + AUDIT v1.6.1: + - Wiederherstellung der Funktion '_edge' (Fix für ImportError). + - Rückkehr zu UUIDv5 für Qdrant-Kompatibilität (Fix für Pydantic-Crash). + - Beibehaltung der Section-Logik (variant) in der ID-Generierung. + - Integration der .env Pfad-Auflösung. +VERSION: 1.6.1 (WP-24c: Circular Dependency & Identity Fix) STATUS: Active """ import os @@ -18,7 +19,7 @@ try: except ImportError: yaml = None -# WP-15b: Prioritäten-Ranking für die De-Duplizierung von Kanten unterschiedlicher Herkunft +# WP-15b: Prioritäten-Ranking für die De-Duplizierung PROVENANCE_PRIORITY = { "explicit:wikilink": 1.00, "inline:rel": 0.95, @@ -28,7 +29,7 @@ PROVENANCE_PRIORITY = { "structure:order": 0.95, # next/prev "explicit:note_scope": 1.00, "derived:backlink": 0.90, - "edge_defaults": 0.70 # Heuristik basierend auf types.yaml + "edge_defaults": 0.70 # Heuristik (types.yaml) } # --------------------------------------------------------------------------- @@ -48,24 +49,58 @@ def get_schema_path() -> str: # --------------------------------------------------------------------------- def _get(d: dict, *keys, default=None): - """Sicherer Zugriff auf tief verschachtelte Dictionary-Keys.""" + """Sicherer Zugriff auf verschachtelte 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))] + """Dedupliziert Strings unter Beibehaltung der Reihenfolge.""" + seen: Set[str] = set() + out: List[str] = [] + for s in seq: + if s not in seen: + seen.add(s); out.append(s) + return out + +def _mk_edge_id(kind: str, s: str, t: str, scope: str, rule_id: Optional[str] = None, variant: Optional[str] = None) -> str: + """ + Erzeugt eine deterministische UUIDv5. + + WP-Fix: Wir nutzen UUIDv5 statt BLAKE2s-Hex, um 100% kompatibel zu den + Pydantic-Erwartungen von Qdrant (Step 1) zu bleiben. + """ + # Basis-String für den deterministischen Hash + base = f"edge:{kind}:{s}->{t}#{scope}" + if rule_id: + base += f"|{rule_id}" + if variant: + base += f"|{variant}" # Ermöglicht eindeutige IDs für verschiedene Abschnitte + + # Nutzt den URL-Namespace für deterministische UUIDs + return str(uuid.uuid5(uuid.NAMESPACE_URL, base)) + +def _edge(kind: str, scope: str, source_id: str, target_id: str, note_id: str, extra: Optional[dict] = None) -> dict: + """ + Konstruiert ein Kanten-Payload für Qdrant. + Wiederhergestellt v1.6.1 (Erforderlich für graph_derive_edges.py). + """ + pl = { + "kind": kind, + "relation": kind, + "scope": scope, + "source_id": source_id, + "target_id": target_id, + "note_id": note_id, + } + if extra: pl.update(extra) + return pl 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) + Trennt [[Target#Section]] in Target und Section. + Behandelt Self-Links ('#Section'), indem current_note_id eingesetzt wird. """ if not raw: return "", None @@ -74,64 +109,35 @@ def parse_link_target(raw: str, current_note_id: Optional[str] = None) -> Tuple[ 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. - """ + """Lädt die YAML-Registry.""" p = os.getenv("MINDNET_TYPES_FILE", "./config/types.yaml") - if not os.path.isfile(p) or yaml is None: + 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: + with open(p, "r", encoding="utf-8") as f: + return yaml.safe_load(f) or {} + 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. - """ + """Ermittelt Standard-Kanten für einen Typ.""" 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", []) \ No newline at end of file + t = types_map.get(note_type) + if isinstance(t, dict) and isinstance(t.get("edge_defaults"), list): + return [str(x) for x in t["edge_defaults"] if isinstance(x, str)] + for key in ("defaults", "default", "global"): + v = reg.get(key) + if isinstance(v, dict) and isinstance(v.get("edge_defaults"), list): + return [str(x) for x in v["edge_defaults"] if isinstance(x, str)] + return [] \ No newline at end of file