problem fix

ausgehende Kanten
This commit is contained in:
Lars 2025-12-14 14:45:50 +01:00
parent 3861246ac6
commit f7a4dab707
2 changed files with 75 additions and 48 deletions

View File

@ -8,24 +8,25 @@ def render_graph_explorer(graph_service):
st.header("🕸️ Graph Explorer") st.header("🕸️ Graph Explorer")
# Session State initialisieren # Session State initialisieren
if "graph_center_id" not in st.session_state: st.session_state.graph_center_id = None if "graph_center_id" not in st.session_state:
st.session_state.graph_center_id = None
# Defaults speichern für Persistenz # Defaults für View & Physik setzen
st.session_state.setdefault("graph_depth", 2) st.session_state.setdefault("graph_depth", 2)
st.session_state.setdefault("graph_show_labels", True) st.session_state.setdefault("graph_show_labels", True)
# Defaults angepasst für BarnesHut (andere Skala!) st.session_state.setdefault("graph_spacing", 250)
st.session_state.setdefault("graph_spacing", 150) st.session_state.setdefault("graph_gravity", -4000)
st.session_state.setdefault("graph_gravity", -3000)
col_ctrl, col_graph = st.columns([1, 4]) col_ctrl, col_graph = st.columns([1, 4])
# --- LINKE SPALTE: CONTROLS ---
with col_ctrl: with col_ctrl:
st.subheader("Fokus") st.subheader("Fokus")
# Suche # Sucheingabe
search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...") search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...")
options = {} # Suchlogik Qdrant
if search_term: if search_term:
hits, _ = graph_service.client.scroll( hits, _ = graph_service.client.scroll(
collection_name=f"{COLLECTION_PREFIX}_notes", collection_name=f"{COLLECTION_PREFIX}_notes",
@ -42,19 +43,18 @@ def render_graph_explorer(graph_service):
st.divider() st.divider()
# View Settings # Layout & Physik Einstellungen
with st.expander("👁️ Ansicht & Layout", expanded=True): with st.expander("👁️ Ansicht & Layout", expanded=True):
st.session_state.graph_depth = st.slider("Tiefe (Tier)", 1, 3, st.session_state.graph_depth) st.session_state.graph_depth = st.slider("Tiefe (Tier)", 1, 3, st.session_state.graph_depth)
st.session_state.graph_show_labels = st.checkbox("Kanten-Beschriftung", st.session_state.graph_show_labels) st.session_state.graph_show_labels = st.checkbox("Kanten-Beschriftung", st.session_state.graph_show_labels)
st.markdown("**Physik (BarnesHut)**") st.markdown("**Physik (BarnesHut)**")
# ACHTUNG: BarnesHut reagiert anders. Spring Length ist wichtig. st.session_state.graph_spacing = st.slider("Federlänge (Abstand)", 50, 800, st.session_state.graph_spacing)
st.session_state.graph_spacing = st.slider("Federlänge (Abstand)", 50, 500, st.session_state.graph_spacing, help="Wie lang sollen die Verbindungen sein?") st.session_state.graph_gravity = st.slider("Abstoßung (Gravity)", -20000, -500, st.session_state.graph_gravity)
st.session_state.graph_gravity = st.slider("Abstoßung (Gravity)", -20000, -1000, st.session_state.graph_gravity, help="Wie stark sollen sich Knoten abstoßen?")
if st.button("Reset Layout"): if st.button("Reset Layout"):
st.session_state.graph_spacing = 150 st.session_state.graph_spacing = 250
st.session_state.graph_gravity = -3000 st.session_state.graph_gravity = -4000
st.rerun() st.rerun()
st.divider() st.divider()
@ -62,57 +62,55 @@ def render_graph_explorer(graph_service):
for k, v in list(GRAPH_COLORS.items())[:8]: for k, v in list(GRAPH_COLORS.items())[:8]:
st.markdown(f"<span style='color:{v}'>●</span> {k}", unsafe_allow_html=True) st.markdown(f"<span style='color:{v}'>●</span> {k}", unsafe_allow_html=True)
# --- RECHTE SPALTE: GRAPH & ACTION BAR ---
with col_graph: with col_graph:
center_id = st.session_state.graph_center_id center_id = st.session_state.graph_center_id
if center_id: if center_id:
# Container für Action Bar OBERHALB des Graphen (Layout Fix) # Action Container oben fixieren (Layout Fix)
action_container = st.container() action_container = st.container()
# Graph Laden # Graph und Daten laden
with st.spinner(f"Lade Graph..."): with st.spinner(f"Lade Graph..."):
# Daten laden (Cache wird genutzt)
nodes, edges = graph_service.get_ego_graph( nodes, edges = graph_service.get_ego_graph(
center_id, center_id,
depth=st.session_state.graph_depth, depth=st.session_state.graph_depth,
show_labels=st.session_state.graph_show_labels show_labels=st.session_state.graph_show_labels
) )
# Fetch Note Data für Button & Debug # WICHTIG: Volle Daten inkl. Stitching für Editor holen
# Wir holen die Metadaten (inkl. path), was für den Editor-Callback reicht. note_data = graph_service.get_note_with_full_content(center_id)
note_data = graph_service._fetch_note_cached(center_id)
# --- ACTION BAR RENDEREN --- # Action Bar rendern
with action_container: with action_container:
c_act1, c_act2 = st.columns([3, 1]) c1, c2 = st.columns([3, 1])
with c_act1: with c1:
st.caption(f"Aktives Zentrum: **{center_id}**") st.caption(f"Aktives Zentrum: **{center_id}**")
with c_act2: with c2:
if note_data: if note_data:
st.button("📝 Bearbeiten", st.button("📝 Bearbeiten",
use_container_width=True, use_container_width=True,
on_click=switch_to_editor_callback, on_click=switch_to_editor_callback,
args=(note_data,)) args=(note_data,))
else: else:
st.error("Daten nicht verfügbar") st.error("Datenfehler: Note nicht gefunden")
# DATA INSPECTOR (Payload Debug) # Debug Inspector
with st.expander("🕵️ Data Inspector (Payload Debug)", expanded=False): with st.expander("🕵️ Data Inspector", expanded=False):
if note_data: if note_data:
st.json(note_data) st.json(note_data)
if 'path' not in note_data: if 'path' in note_data:
st.error("ACHTUNG: Feld 'path' fehlt im Qdrant-Payload! Update-Modus wird nicht funktionieren.") st.success(f"Pfad OK: {note_data['path']}")
else: else:
st.success(f"Pfad gefunden: {note_data['path']}") st.error("Pfad fehlt!")
else: else:
st.info("Keine Daten geladen.") st.info("Leer.")
if not nodes: if not nodes:
st.warning("Keine Daten gefunden.") st.warning("Keine Daten gefunden.")
else: else:
# --- CONFIGURATION (BarnesHut) --- # --- CONFIGURATION (BarnesHut) ---
# Height-Trick für Re-Render (da key-Parameter nicht funktioniert) # Height-Trick für Re-Render (da key-Parameter manchmal crasht)
# Ändere Height minimal basierend auf Gravity
dyn_height = 800 + (abs(st.session_state.graph_gravity) % 5) dyn_height = 800 + (abs(st.session_state.graph_gravity) % 5)
config = Config( config = Config(
@ -121,11 +119,10 @@ def render_graph_explorer(graph_service):
directed=True, directed=True,
physics={ physics={
"enabled": True, "enabled": True,
# BarnesHut ist der Standard und stabilste Solver für Agraph
"solver": "barnesHut", "solver": "barnesHut",
"barnesHut": { "barnesHut": {
"gravitationalConstant": st.session_state.graph_gravity, "gravitationalConstant": st.session_state.graph_gravity,
"centralGravity": 0.3, "centralGravity": 0.005,
"springLength": st.session_state.graph_spacing, "springLength": st.session_state.graph_spacing,
"springConstant": 0.04, "springConstant": 0.04,
"damping": 0.09, "damping": 0.09,
@ -141,7 +138,7 @@ def render_graph_explorer(graph_service):
return_value = agraph(nodes=nodes, edges=edges, config=config) return_value = agraph(nodes=nodes, edges=edges, config=config)
# Interaktions-Logik # Interaktions-Logik (Klick auf Node)
if return_value: if return_value:
if return_value != center_id: if return_value != center_id:
# Navigation: Neues Zentrum setzen # Navigation: Neues Zentrum setzen
@ -152,4 +149,4 @@ def render_graph_explorer(graph_service):
st.toast(f"Zentrum: {return_value}") st.toast(f"Zentrum: {return_value}")
else: else:
st.info("👈 Bitte wähle links eine Notiz aus.") st.info("👈 Bitte wähle links eine Notiz aus, um den Graphen zu starten.")

View File

@ -12,6 +12,26 @@ class GraphExplorerService:
self.edges_col = f"{prefix}_edges" self.edges_col = f"{prefix}_edges"
self._note_cache = {} self._note_cache = {}
def get_note_with_full_content(self, note_id):
"""
Lädt die Metadaten der Note und rekonstruiert den gesamten Text
aus den Chunks (Stitching). Wichtig für den Editor-Fallback.
"""
# 1. Metadaten holen
meta = self._fetch_note_cached(note_id)
if not meta: return None
# 2. Volltext aus Chunks bauen
full_text = self._fetch_full_text_stitched(note_id)
# 3. Ergebnis kombinieren (Wir überschreiben das 'fulltext' Feld mit dem frischen Stitching)
# Wir geben eine Kopie zurück, um den Cache nicht zu verfälschen
complete_note = meta.copy()
if full_text:
complete_note['fulltext'] = full_text
return complete_note
def get_ego_graph(self, center_note_id: str, depth=2, show_labels=True): def get_ego_graph(self, center_note_id: str, depth=2, show_labels=True):
""" """
Erstellt den Ego-Graphen um eine zentrale Notiz. Erstellt den Ego-Graphen um eine zentrale Notiz.
@ -25,6 +45,7 @@ class GraphExplorerService:
if not center_note: return [], [] if not center_note: return [], []
self._add_node_to_dict(nodes_dict, center_note, level=0) self._add_node_to_dict(nodes_dict, center_note, level=0)
# Initialset für Suche
level_1_ids = {center_note_id} level_1_ids = {center_note_id}
# Suche Kanten für Center (L1) # Suche Kanten für Center (L1)
@ -36,7 +57,7 @@ class GraphExplorerService:
if tgt_id: level_1_ids.add(tgt_id) if tgt_id: level_1_ids.add(tgt_id)
# Level 2 Suche (begrenzt für Performance) # Level 2 Suche (begrenzt für Performance)
if depth > 1 and len(level_1_ids) > 1 and len(level_1_ids) < 60: if depth > 1 and len(level_1_ids) > 1 and len(level_1_ids) < 80:
l1_subset = list(level_1_ids - {center_note_id}) l1_subset = list(level_1_ids - {center_note_id})
if l1_subset: if l1_subset:
l2_edges = self._find_connected_edges_batch(l1_subset) l2_edges = self._find_connected_edges_batch(l1_subset)
@ -107,6 +128,7 @@ class GraphExplorerService:
full_text = [] full_text = []
for c in chunks: for c in chunks:
# 'text' ist der reine Inhalt ohne Overlap
txt = c.payload.get('text', '') txt = c.payload.get('text', '')
if txt: full_text.append(txt) if txt: full_text.append(txt)
@ -133,13 +155,16 @@ class GraphExplorerService:
def _find_connected_edges(self, note_ids, note_title=None): def _find_connected_edges(self, note_ids, note_title=None):
"""Findet eingehende und ausgehende Kanten für Nodes.""" """Findet eingehende und ausgehende Kanten für Nodes."""
# 1. Chunk IDs ermitteln (da Edges oft an Chunks hängen)
scroll_filter = models.Filter(must=[models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids))])
chunks, _ = self.client.scroll(self.chunks_col, scroll_filter=scroll_filter, limit=200)
chunk_ids = [c.id for c in chunks]
results = [] results = []
# 1. Chunk IDs ermitteln (da Edges oft an Chunks hängen)
chunk_ids = []
if note_ids:
c_filter = models.Filter(must=[models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids))])
chunks, _ = self.client.scroll(self.chunks_col, scroll_filter=c_filter, limit=300)
chunk_ids = [c.id for c in chunks]
# 2. Outgoing Edges suchen # 2. Outgoing Edges suchen
# Source kann sein: Eine Chunk ID ODER die Note ID selbst (bei direkten Links) # Source kann sein: Eine Chunk ID ODER die Note ID selbst (bei direkten Links)
source_candidates = chunk_ids + note_ids source_candidates = chunk_ids + note_ids
@ -150,22 +175,27 @@ class GraphExplorerService:
# FIX: MatchExcept Workaround für Pydantic # FIX: MatchExcept Workaround für Pydantic
models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES})) models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))
]) ])
res_out, _ = self.client.scroll(self.edges_col, scroll_filter=out_f, limit=100, with_payload=True) res_out, _ = self.client.scroll(self.edges_col, scroll_filter=out_f, limit=500, with_payload=True)
results.extend(res_out) results.extend(res_out)
# 3. Incoming Edges suchen # 3. Incoming Edges suchen
# Target kann sein: Chunk ID, Note ID, oder Note Titel (Wikilinks) # Target kann sein: Chunk ID, Note ID, oder Note Titel (Wikilinks)
shoulds = [] shoulds = []
if chunk_ids: shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=chunk_ids))) if chunk_ids:
if note_title: shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=note_title))) shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=chunk_ids)))
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=note_ids)))
if note_ids:
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=note_ids)))
if note_title:
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=note_title)))
if shoulds: if shoulds:
in_f = models.Filter( in_f = models.Filter(
must=[models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))], must=[models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))],
should=shoulds should=shoulds
) )
res_in, _ = self.client.scroll(self.edges_col, scroll_filter=in_f, limit=100, with_payload=True) res_in, _ = self.client.scroll(self.edges_col, scroll_filter=in_f, limit=500, with_payload=True)
results.extend(res_in) results.extend(res_in)
return results return results