letzte anpassungen

This commit is contained in:
Lars 2025-12-27 20:30:24 +01:00
parent 8490911958
commit 7fa9ce81bd

View File

@ -1,16 +1,25 @@
""" """
FILE: app/core/graph/graph_subgraph.py FILE: app/core/graph/graph_subgraph.py
DESCRIPTION: In-Memory Repräsentation eines Graphen für Scoring und Analyse. DESCRIPTION: In-Memory Repräsentation eines Graphen für Scoring und Analyse.
Zentrale Komponente für die Graph-Expansion (BFS) und Bonus-Berechnung.
MODULARISIERUNG: Teil des graph-Pakets (WP-14).
VERSION: 1.1.0
STATUS: Active
""" """
import math import math
from collections import defaultdict from collections import defaultdict
from typing import Dict, List, Optional, DefaultDict, Any, Set from typing import Dict, List, Optional, DefaultDict, Any, Set
from qdrant_client import QdrantClient from qdrant_client import QdrantClient
# Lokale Paket-Imports
from .graph_weights import EDGE_BASE_WEIGHTS, calculate_edge_weight from .graph_weights import EDGE_BASE_WEIGHTS, calculate_edge_weight
from .graph_db_adapter import fetch_edges_from_qdrant from .graph_db_adapter import fetch_edges_from_qdrant
class Subgraph: class Subgraph:
"""Leichtgewichtiger Subgraph mit Adjazenzlisten & Kennzahlen.""" """
Leichtgewichtiger Subgraph mit Adjazenzlisten & Kennzahlen.
Wird für die Berechnung von Graph-Boni im Retriever genutzt.
"""
def __init__(self) -> None: def __init__(self) -> None:
self.adj: DefaultDict[str, List[Dict]] = defaultdict(list) self.adj: DefaultDict[str, List[Dict]] = defaultdict(list)
@ -19,7 +28,10 @@ class Subgraph:
self.out_degree: DefaultDict[str, int] = defaultdict(int) self.out_degree: DefaultDict[str, int] = defaultdict(int)
def add_edge(self, e: Dict) -> None: def add_edge(self, e: Dict) -> None:
"""Fügt eine Kante hinzu und aktualisiert Indizes.""" """
Fügt eine Kante hinzu und aktualisiert Indizes.
Unterstützt Kontext-Notes für verbesserte Graph-Konnektivität.
"""
src = e.get("source") src = e.get("source")
tgt = e.get("target") tgt = e.get("target")
kind = e.get("kind") kind = e.get("kind")
@ -29,15 +41,15 @@ class Subgraph:
if not src or not tgt: if not src or not tgt:
return return
# 1. Forward # 1. Forward-Kante
self.adj[src].append({"target": tgt, "kind": kind, "weight": weight}) self.adj[src].append({"target": tgt, "kind": kind, "weight": weight})
self.out_degree[src] += 1 self.out_degree[src] += 1
self.in_degree[tgt] += 1 self.in_degree[tgt] += 1
# 2. Reverse (WP-04b Explanation) # 2. Reverse-Kante (für WP-04b Explanation Layer)
self.reverse_adj[tgt].append({"source": src, "kind": kind, "weight": weight}) self.reverse_adj[tgt].append({"source": src, "kind": kind, "weight": weight})
# 3. Kontext-Note Handling # 3. Kontext-Note Handling (erhöht die Zentralität der Parent-Note)
if owner and owner != src: if owner and owner != src:
self.adj[owner].append({"target": tgt, "kind": kind, "weight": weight}) self.adj[owner].append({"target": tgt, "kind": kind, "weight": weight})
self.out_degree[owner] += 1 self.out_degree[owner] += 1
@ -54,16 +66,21 @@ class Subgraph:
return self.aggregate_edge_bonus(node_id) return self.aggregate_edge_bonus(node_id)
def centrality_bonus(self, node_id: str) -> float: def centrality_bonus(self, node_id: str) -> float:
"""Log-gedämpfte Zentralität (In-Degree).""" """
Log-gedämpfte Zentralität basierend auf dem In-Degree.
Begrenzt auf einen maximalen Boost von 0.15.
"""
indeg = self.in_degree.get(node_id, 0) indeg = self.in_degree.get(node_id, 0)
if indeg <= 0: if indeg <= 0:
return 0.0 return 0.0
return min(math.log1p(indeg) / 10.0, 0.15) return min(math.log1p(indeg) / 10.0, 0.15)
def get_outgoing_edges(self, node_id: str) -> List[Dict[str, Any]]: def get_outgoing_edges(self, node_id: str) -> List[Dict[str, Any]]:
"""Gibt alle ausgehenden Kanten einer Node zurück."""
return self.adj.get(node_id, []) return self.adj.get(node_id, [])
def get_incoming_edges(self, node_id: str) -> List[Dict[str, Any]]: def get_incoming_edges(self, node_id: str) -> List[Dict[str, Any]]:
"""Gibt alle eingehenden Kanten einer Node zurück."""
return self.reverse_adj.get(node_id, []) return self.reverse_adj.get(node_id, [])
@ -74,7 +91,10 @@ def expand(
depth: int = 1, depth: int = 1,
edge_types: Optional[List[str]] = None, edge_types: Optional[List[str]] = None,
) -> Subgraph: ) -> Subgraph:
"""Expandiert ab Seeds entlang von Edges bis zu einer bestimmten Tiefe.""" """
Expandiert ab Seeds entlang von Edges bis zu einer bestimmten Tiefe.
Nutzt fetch_edges_from_qdrant für den Datenbankzugriff.
"""
sg = Subgraph() sg = Subgraph()
frontier = set(seeds) frontier = set(seeds)
visited = set() visited = set()
@ -83,6 +103,7 @@ def expand(
if not frontier: if not frontier:
break break
# Batch-Abfrage der Kanten für die aktuelle Ebene
payloads = fetch_edges_from_qdrant(client, prefix, list(frontier), edge_types) payloads = fetch_edges_from_qdrant(client, prefix, list(frontier), edge_types)
next_frontier: Set[str] = set() next_frontier: Set[str] = set()
@ -91,12 +112,14 @@ def expand(
if not src or not tgt: continue if not src or not tgt: continue
sg.add_edge({ sg.add_edge({
"source": src, "target": tgt, "source": src,
"target": tgt,
"kind": pl.get("kind", "edge"), "kind": pl.get("kind", "edge"),
"weight": calculate_edge_weight(pl), "weight": calculate_edge_weight(pl),
"note_id": pl.get("note_id"), "note_id": pl.get("note_id"),
}) })
# BFS Logik: Neue Ziele in die nächste Frontier aufnehmen
if tgt not in visited: if tgt not in visited:
next_frontier.add(str(tgt)) next_frontier.add(str(tgt))