mindnet/app/core/derive_edges.py
Lars a14d0bb7cb
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 2s
app/core/derive_edges.py aktualisiert
2025-11-08 14:25:01 +01:00

131 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
derive_edges.py v1.9.0
Zweck:
- Ableiten von Edges aus Note + Chunks.
- Rückwärtskompatibel zu bisherigen Funktionen, inkl. Parametern:
build_edges_for_note(parsed, note_id, chunks, note_scope_refs=False, ...)
- Neu: Type-Registry wird optional benutzt, um fehlende Standardkanten je 'type'
(edge_defaults) zu ergänzen, ohne vorhandene explizite Kanten zu überschreiben.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Set, Tuple
# Annahme: parser liefert wikilinks und frontmatter; unverändert zum Stand vor der Erweiterung
try:
from app.core.parser import extract_wikilinks # type: ignore
except Exception:
def extract_wikilinks(text: str) -> List[str]:
# Minimaler Fallback im Produktivcode steht eure echte Implementierung.
import re
return re.findall(r"\[\[([^\]]+)\]\]", text or "")
# Optional: Registry
try:
from app.core.type_registry import get_edge_defaults_for_type
except Exception:
def get_edge_defaults_for_type(note_type: str) -> List[str]:
return []
# Edge-Helper (UUIDv5, etc.) werden hier nicht neu definiert; wir liefern nur die
# Edge-Strukturen zurück. Upsert passiert über eure bestehenden qdrant_points-Utilities.
def _edge(
edge_type: str,
src_scope: str,
src_id: str,
dst_scope: str,
dst_id: str,
note_id: str,
extra: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
e = {
"type": edge_type,
"src_scope": src_scope,
"src_id": src_id,
"dst_scope": dst_scope,
"dst_id": dst_id,
"note_id": note_id,
}
if extra:
e.update(extra)
return e
def build_edges_for_note(
*,
note_id: str,
note_type: Optional[str],
chunks: List[Dict[str, Any]],
frontmatter: Dict[str, Any],
body_text: str,
note_scope_refs: bool = False,
) -> List[Dict[str, Any]]:
"""
Erzeugt die Standardkanten:
- belongs_to (Chunk→Note)
- prev/next (Chunk-Sequenz)
- references (aus [[...]]; optional: note_scope_refs=True → an Note statt an Chunk hängen)
- backlink (inverse der references)
- depends_on (aus FM 'depends_on': [...])
- assigned_to (aus FM 'assigned_to': [...])
Zusätzlich:
- Type-Registry edge_defaults ergänzt fehlende Kanten als 'type_default'
(z. B. 'related_to' ohne Ziel bleibt ungenutzt nur, wenn interpretierbar).
Rückgabewert:
Liste von Edge-Dicts. Upsert übernimmt euer Qdrant-Utility.
"""
edges: List[Dict[str, Any]] = []
# belongs_to + prev/next
for i, ch in enumerate(chunks):
cid = ch.get("chunk_id") or f"{note_id}#{i+1}"
edges.append(_edge("belongs_to", "chunk", cid, "note", note_id, note_id))
if i > 0:
prev_cid = chunks[i - 1].get("chunk_id") or f"{note_id}#{i}"
edges.append(_edge("prev", "chunk", cid, "chunk", prev_cid, note_id))
edges.append(_edge("next", "chunk", prev_cid, "chunk", cid, note_id))
# references/backlink
refs: Set[str] = set(extract_wikilinks(body_text or ""))
if note_scope_refs:
# Referenzen auf Note-Ebene
for ref in sorted(refs):
edges.append(_edge("references", "note", note_id, "note", ref, note_id))
edges.append(_edge("backlink", "note", ref, "note", note_id, note_id))
else:
# Referenzen pro Chunk (wenn sinnvoll)
for ch in chunks:
cid = ch.get("chunk_id")
ctext = (ch.get("text") or "") + "\n" + (ch.get("window") or "")
cset: Set[str] = set(extract_wikilinks(ctext))
for ref in sorted(cset):
edges.append(_edge("references", "chunk", cid, "note", ref, note_id))
# Backlink auf Note-Ebene
edges.append(_edge("backlink", "note", ref, "note", note_id, note_id))
# depends_on / assigned_to aus Frontmatter
fm_dep = frontmatter.get("depends_on") or []
if isinstance(fm_dep, list):
for d in fm_dep:
edges.append(_edge("depends_on", "note", note_id, "note", str(d), note_id))
fm_ass = frontmatter.get("assigned_to") or []
if isinstance(fm_ass, list):
for a in fm_ass:
edges.append(_edge("assigned_to", "note", note_id, "person", str(a), note_id))
# Type-Defaults (nur ergänzend, wenn interpretierbar)
if note_type:
defaults = get_edge_defaults_for_type(note_type)
# Aus defaults lassen sich ohne konkretes Ziel nur symbolische Kanten ableiten.
# Wir fügen hier KEINE künstlichen Zielnoten hinzu; Registry ist vor allem
# für spätere WP-04-Gewichtungen gedacht. Wenn ihr konkrete Regeln definieren
# wollt (z. B. „task → belongs_to project aus frontmatter.project_id“),
# kann dies hier ergänzt werden (mit echten Ziel-IDs).
# Aktuell also: keine „leeren“ Kanten ohne Ziel hinzufügen.
return edges