From a852975811d00edac37d2ad386aadfb161f3451a Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 10 Jan 2026 15:19:46 +0100 Subject: [PATCH] Update qdrant_points.py, graph_utils.py, graph_derive_edges.py, and ingestion_processor.py to version 4.0.0: Implement GOLD-STANDARD identity with strict 4-parameter ID generation, eliminating rule_id and variant from ID calculations. Enhance documentation for clarity and consistency across modules, addressing ID drift and ensuring compatibility in the ingestion workflow. --- app/core/database/qdrant_points.py | 19 ++++++------ app/core/graph/graph_derive_edges.py | 38 ++++++++++++++--------- app/core/graph/graph_utils.py | 31 +++++++++--------- app/core/ingestion/ingestion_processor.py | 13 +++++--- 4 files changed, 58 insertions(+), 43 deletions(-) diff --git a/app/core/database/qdrant_points.py b/app/core/database/qdrant_points.py index c943f36..6cefd1c 100644 --- a/app/core/database/qdrant_points.py +++ b/app/core/database/qdrant_points.py @@ -2,7 +2,7 @@ FILE: app/core/database/qdrant_points.py DESCRIPTION: Object-Mapper für Qdrant. Konvertiert JSON-Payloads (Notes, Chunks, Edges) in PointStructs und generiert deterministische UUIDs. -VERSION: 1.5.3 (WP-Fix: Centralized Identity Enforcement) +VERSION: 4.0.0 (WP-24c: Gold-Standard Identity - 4-Parameter-ID) STATUS: Active DEPENDENCIES: qdrant_client, uuid, os, app.core.graph.graph_utils LAST_ANALYSIS: 2026-01-10 @@ -95,8 +95,11 @@ def _normalize_edge_payload(pl: dict) -> dict: def points_for_edges(prefix: str, edge_payloads: List[dict]) -> Tuple[str, List[rest.PointStruct]]: """ Konvertiert Kanten-Payloads in PointStructs. - WP-24c Audit v1.5.3: Nutzt die zentrale _mk_edge_id Funktion aus graph_utils. + WP-24c v4.0.0: Nutzt die zentrale _mk_edge_id Funktion aus graph_utils. Dies eliminiert den ID-Drift zwischen manuellen und virtuellen Kanten. + + GOLD-STANDARD v4.0.0: Die ID-Generierung verwendet STRICT nur die 4 Parameter + (kind, source_id, target_id, scope). rule_id und variant werden ignoriert. """ _, _, edges_col = _names(prefix) points: List[rest.PointStruct] = [] @@ -104,25 +107,23 @@ def points_for_edges(prefix: str, edge_payloads: List[dict]) -> Tuple[str, List[ for raw in edge_payloads: pl = _normalize_edge_payload(raw) - # Extraktion der Identitäts-Parameter + # Extraktion der Identitäts-Parameter (GOLD-STANDARD v4.0.0: nur 4 Parameter) kind = pl.get("kind", "edge") s = pl.get("source_id", "unknown-src") t = pl.get("target_id", "unknown-tgt") scope = pl.get("scope", "note") - # Optionale Differenzierung (falls von graph_derive_edges gesetzt) - rule_id = pl.get("rule_id") - variant = pl.get("variant") + # Hinweis: rule_id und variant werden im Payload gespeichert, + # fließen aber NICHT in die ID-Generierung ein (v4.0.0 Standard) try: # Aufruf der Single-Source-of-Truth für IDs + # GOLD-STANDARD v4.0.0: Nur 4 Parameter werden verwendet point_id = _mk_edge_id( kind=kind, s=s, t=t, - scope=scope, - rule_id=rule_id, - variant=variant + scope=scope ) # Synchronisierung des Payloads mit der berechneten ID diff --git a/app/core/graph/graph_derive_edges.py b/app/core/graph/graph_derive_edges.py index faa18b1..0579ad2 100644 --- a/app/core/graph/graph_derive_edges.py +++ b/app/core/graph/graph_derive_edges.py @@ -36,9 +36,10 @@ def build_edges_for_note( if not cid: continue # Verbindung Chunk -> Note + # WP-24c v4.0.0: rule_id wird nur im Payload gespeichert, fließt nicht in die ID ein edges.append(_edge("belongs_to", "chunk", cid, note_id, note_id, { "chunk_id": cid, - "edge_id": _mk_edge_id("belongs_to", cid, note_id, "chunk", "structure:belongs_to"), + "edge_id": _mk_edge_id("belongs_to", cid, note_id, "chunk"), "provenance": "structure", "rule_id": "structure:belongs_to", "confidence": PROVENANCE_PRIORITY["structure:belongs_to"] @@ -48,14 +49,15 @@ def build_edges_for_note( if idx < len(chunks) - 1: next_id = _get(chunks[idx+1], "chunk_id", "id") if next_id: + # WP-24c v4.0.0: rule_id wird nur im Payload gespeichert, fließt nicht in die ID ein edges.append(_edge("next", "chunk", cid, next_id, note_id, { "chunk_id": cid, - "edge_id": _mk_edge_id("next", cid, next_id, "chunk", "structure:order"), + "edge_id": _mk_edge_id("next", cid, next_id, "chunk"), "provenance": "structure", "rule_id": "structure:order", "confidence": PROVENANCE_PRIORITY["structure:order"] })) edges.append(_edge("prev", "chunk", next_id, cid, note_id, { "chunk_id": next_id, - "edge_id": _mk_edge_id("prev", next_id, cid, "chunk", "structure:order"), + "edge_id": _mk_edge_id("prev", next_id, cid, "chunk"), "provenance": "structure", "rule_id": "structure:order", "confidence": PROVENANCE_PRIORITY["structure:order"] })) @@ -77,8 +79,8 @@ def build_edges_for_note( payload = { "chunk_id": cid, - # WP-Fix: Variant=sec sorgt für eindeutige ID pro Sektion - "edge_id": _mk_edge_id(k, cid, t, "chunk", "inline:rel", variant=sec), + # WP-24c v4.0.0: variant wird nur im Payload gespeichert (target_section), fließt nicht in die ID ein + "edge_id": _mk_edge_id(k, cid, t, "chunk"), "provenance": "explicit", "rule_id": "inline:rel", "confidence": PROVENANCE_PRIORITY["inline:rel"] } if sec: payload["target_section"] = sec @@ -90,9 +92,10 @@ def build_edges_for_note( raw_t, k, p = cand.get("to"), cand.get("kind", "related_to"), cand.get("provenance", "semantic_ai") t, sec = parse_link_target(raw_t, note_id) if t: + # WP-24c v4.0.0: rule_id und variant werden nur im Payload gespeichert, fließen nicht in die ID ein payload = { "chunk_id": cid, - "edge_id": _mk_edge_id(k, cid, t, "chunk", f"candidate:{p}", variant=sec), + "edge_id": _mk_edge_id(k, cid, t, "chunk"), "provenance": p, "rule_id": f"candidate:{p}", "confidence": PROVENANCE_PRIORITY.get(p, 0.90) } if sec: payload["target_section"] = sec @@ -104,9 +107,10 @@ def build_edges_for_note( t, sec = parse_link_target(raw_t, note_id) if not t: continue + # WP-24c v4.0.0: rule_id und variant werden nur im Payload gespeichert, fließen nicht in die ID ein payload = { "chunk_id": cid, - "edge_id": _mk_edge_id(k, cid, t, "chunk", "callout:edge", variant=sec), + "edge_id": _mk_edge_id(k, cid, t, "chunk"), "provenance": "explicit", "rule_id": "callout:edge", "confidence": PROVENANCE_PRIORITY["callout:edge"] } if sec: payload["target_section"] = sec @@ -118,9 +122,10 @@ def build_edges_for_note( r, sec = parse_link_target(raw_r, note_id) if not r: continue + # WP-24c v4.0.0: rule_id und variant werden nur im Payload gespeichert, fließen nicht in die ID ein payload = { "chunk_id": cid, "ref_text": raw_r, - "edge_id": _mk_edge_id("references", cid, r, "chunk", "explicit:wikilink", variant=sec), + "edge_id": _mk_edge_id("references", cid, r, "chunk"), "provenance": "explicit", "rule_id": "explicit:wikilink", "confidence": PROVENANCE_PRIORITY["explicit:wikilink"] } if sec: payload["target_section"] = sec @@ -129,9 +134,10 @@ def build_edges_for_note( # Automatische Kanten-Vererbung aus types.yaml for rel in defaults: if rel != "references": + # WP-24c v4.0.0: rule_id und variant werden nur im Payload gespeichert, fließen nicht in die ID ein def_payload = { "chunk_id": cid, - "edge_id": _mk_edge_id(rel, cid, r, "chunk", f"edge_defaults:{rel}", variant=sec), + "edge_id": _mk_edge_id(rel, cid, r, "chunk"), "provenance": "rule", "rule_id": f"edge_defaults:{rel}", "confidence": PROVENANCE_PRIORITY["edge_defaults"] } if sec: def_payload["target_section"] = sec @@ -146,19 +152,21 @@ def build_edges_for_note( for r in refs_note: if not r: continue + # WP-24c v4.0.0: rule_id wird nur im Payload gespeichert, fließt nicht in die ID ein edges.append(_edge("references", "note", note_id, r, note_id, { - "edge_id": _mk_edge_id("references", note_id, r, "note", "explicit:note_scope"), - "provenance": "explicit", "confidence": PROVENANCE_PRIORITY["explicit:note_scope"] + "edge_id": _mk_edge_id("references", note_id, r, "note"), + "provenance": "explicit", "rule_id": "explicit:note_scope", "confidence": PROVENANCE_PRIORITY["explicit:note_scope"] })) # Backlinks zur Stärkung der Bidirektionalität edges.append(_edge("backlink", "note", r, note_id, note_id, { - "edge_id": _mk_edge_id("backlink", r, note_id, "note", "derived:backlink"), - "provenance": "rule", "confidence": PROVENANCE_PRIORITY["derived:backlink"] + "edge_id": _mk_edge_id("backlink", r, note_id, "note"), + "provenance": "rule", "rule_id": "derived:backlink", "confidence": PROVENANCE_PRIORITY["derived:backlink"] })) # 4) De-Duplizierung (In-Place) - # Da die EDGE-ID nun die Sektion (variant) enthält, bleiben Links auf - # unterschiedliche Abschnitte derselben Note erhalten. + # WP-24c v4.0.0: Da die EDGE-ID nur auf 4 Parametern basiert (kind, source, target, scope), + # werden Links auf unterschiedliche Abschnitte derselben Note durch die De-Duplizierung + # konsolidiert. Die Sektion-Information bleibt im Payload (target_section) erhalten. unique_map: Dict[str, dict] = {} for e in edges: eid = e["edge_id"] diff --git a/app/core/graph/graph_utils.py b/app/core/graph/graph_utils.py index cb0d371..131328e 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.2: - - Festlegung des globalen Standards für Kanten-IDs (WP-24c). - - Fix für ImportError (_edge Funktion wiederhergestellt). - - Integration der .env Pfad-Auflösung für Schema und Vokabular. -VERSION: 1.6.2 (WP-24c: Global Identity Standard) + AUDIT v4.0.0: + - GOLD-STANDARD v4.0.0: Strikte 4-Parameter-ID für Kanten (kind, source, target, scope). + - Eliminiert ID-Inkonsistenz zwischen Phase 1 (Autorität) und Phase 2 (Symmetrie). + - rule_id und variant werden ignoriert in der ID-Generierung (nur im Payload gespeichert). + - Fix für das "Steinzeitaxt"-Problem durch konsistente ID-Generierung. +VERSION: 4.0.0 (WP-24c: Gold-Standard Identity) STATUS: Active """ import os @@ -87,29 +88,31 @@ def parse_link_target(raw: str, current_note_id: Optional[str] = None) -> Tuple[ def _mk_edge_id(kind: str, s: str, t: str, scope: str, rule_id: Optional[str] = None, variant: Optional[str] = None) -> str: """ - WP-24c: DER GLOBALE STANDARD für Kanten-IDs. + WP-24c v4.0.0: DER GLOBALE STANDARD für Kanten-IDs. Erzeugt eine deterministische UUIDv5. Dies stellt sicher, dass manuelle Links und systemgenerierte Symmetrien dieselbe Point-ID in Qdrant erhalten. + GOLD-STANDARD v4.0.0: Die ID basiert STRICT auf vier Parametern: + f"edge:{kind}:{source}:{target}:{scope}" + + Die Parameter rule_id und variant werden IGNORIERT und fließen NICHT in die ID ein. + Sie können weiterhin im Payload gespeichert werden, haben aber keinen Einfluss auf die Identität. + Args: kind: Typ der Relation (z.B. 'mastered_by') s: Kanonische ID der Quell-Note t: Kanonische ID der Ziel-Note scope: Granularität (Standard: 'note') - rule_id: Optionale ID der Regel (aus graph_derive_edges) - variant: Optionale Variante für multiple Links zum selben Ziel + rule_id: Optionale ID der Regel (aus graph_derive_edges) - IGNORIERT in ID-Generierung + variant: Optionale Variante für multiple Links zum selben Ziel - IGNORIERT in ID-Generierung """ if not all([kind, s, t]): raise ValueError(f"Incomplete data for edge ID: kind={kind}, src={s}, tgt={t}") - # STRENGER STANDARD: Nutzt Doppelpunkte als Trenner. + # GOLD-STANDARD v4.0.0: STRICT 4-Parameter-ID + # Keine Suffixe für rule_id oder variant im Hash-String! # Jede manuelle Änderung an diesem String-Format führt zu doppelten Kanten in der DB! base = f"edge:{kind}:{s}:{t}:{scope}" - - if rule_id: - base += f":{rule_id}" - if variant: - base += f":{variant}" # Nutzt den URL-Namespace für deterministische Reproduzierbarkeit return str(uuid.uuid5(uuid.NAMESPACE_URL, base)) diff --git a/app/core/ingestion/ingestion_processor.py b/app/core/ingestion/ingestion_processor.py index 9977179..07df591 100644 --- a/app/core/ingestion/ingestion_processor.py +++ b/app/core/ingestion/ingestion_processor.py @@ -4,11 +4,12 @@ DESCRIPTION: Der zentrale IngestionService (Orchestrator). WP-25a: Integration der Mixture of Experts (MoE) Architektur. WP-15b: Two-Pass Workflow mit globalem Kontext-Cache. WP-20/22: Cloud-Resilienz und Content-Lifecycle integriert. - AUDIT v3.4.3: - - Entfernung des inkompatiblen edge_registry.initialize Aufrufs. + AUDIT v4.0.0: + - GOLD-STANDARD v4.0.0: Phase 2 verwendet exakt dieselbe 4-Parameter-ID wie Phase 1. + - Authority-Check in Phase 2 prüft mit konsistenter ID-Generierung. + - Eliminiert Duplikate durch inkonsistente ID-Generierung (Steinzeitaxt-Problem). - Beibehaltung der strikten 2-Phasen-Strategie (Authority-First). - - Fix für das Steinzeitaxt-Problem via zentralisierter ID-Logik. -VERSION: 3.4.3 (WP-24c: Compatibility Fix) +VERSION: 4.0.0 (WP-24c: Gold-Standard Identity) STATUS: Active """ import logging @@ -148,13 +149,15 @@ class IngestionService: src, tgt, kind = v_edge.get("note_id"), v_edge.get("target_id"), v_edge.get("kind") if not src or not tgt: continue - # WP-Fix: Nutzung der zentralisierten ID-Logik aus graph_utils + # WP-24c v4.0.0: Nutzung der zentralisierten ID-Logik aus graph_utils + # GOLD-STANDARD: Exakt 4 Parameter (kind, source, target, scope) try: v_id = _mk_edge_id(kind, src, tgt, "note") except ValueError: continue # AUTHORITY-CHECK: Nur schreiben, wenn keine manuelle Kante existiert + # Prüft mit exakt derselben 4-Parameter-ID, die in Phase 1 verwendet wurde if not is_explicit_edge_present(self.client, self.prefix, v_id): final_virtuals.append(v_edge) logger.info(f" 🔄 [SYMMETRY] Add inverse: {src} --({kind})--> {tgt}")