Ui_Update_cytos
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 3s

This commit is contained in:
Lars 2025-12-28 11:15:11 +01:00
parent e93bab6ea7
commit 876ee898d8
2 changed files with 185 additions and 101 deletions

View File

@ -69,20 +69,26 @@ def render_graph_explorer_cytoscape(graph_service):
search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...", key="cy_search") search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...", key="cy_search")
if search_term: if search_term:
hits, _ = graph_service.client.scroll( try:
collection_name=f"{COLLECTION_PREFIX}_notes", hits, _ = graph_service.client.scroll(
limit=10, collection_name=f"{COLLECTION_PREFIX}_notes",
scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchText(text=search_term))]) limit=10,
) scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchText(text=search_term))])
options = {h.payload['title']: h.payload['note_id'] for h in hits} )
options = {}
if options: for h in hits:
selected_title = st.selectbox("Ergebnisse:", list(options.keys()), key="cy_select") if h.payload and 'title' in h.payload and 'note_id' in h.payload:
if st.button("Laden", use_container_width=True, key="cy_load"): options[h.payload['title']] = h.payload['note_id']
new_id = options[selected_title]
st.session_state.graph_center_id = new_id if options:
st.session_state.graph_inspected_id = new_id selected_title = st.selectbox("Ergebnisse:", list(options.keys()), key="cy_select")
st.rerun() if st.button("Laden", use_container_width=True, key="cy_load"):
new_id = options[selected_title]
st.session_state.graph_center_id = new_id
st.session_state.graph_inspected_id = new_id
st.rerun()
except Exception as e:
st.error(f"Fehler bei der Suche: {e}")
st.divider() st.divider()
@ -174,7 +180,12 @@ def render_graph_explorer_cytoscape(graph_service):
st.markdown(f"**ID:** `{inspected_data.get('note_id')}`") st.markdown(f"**ID:** `{inspected_data.get('note_id')}`")
st.markdown(f"**Typ:** `{inspected_data.get('type')}`") st.markdown(f"**Typ:** `{inspected_data.get('type')}`")
with col_i2: with col_i2:
st.markdown(f"**Tags:** {', '.join(inspected_data.get('tags', []))}") tags = inspected_data.get('tags', [])
if isinstance(tags, list):
tags_str = ', '.join(tags) if tags else "Keine"
else:
tags_str = str(tags) if tags else "Keine"
st.markdown(f"**Tags:** {tags_str}")
path_check = "" if inspected_data.get('path') else "" path_check = "" if inspected_data.get('path') else ""
st.markdown(f"**Pfad:** {path_check}") st.markdown(f"**Pfad:** {path_check}")
@ -189,12 +200,27 @@ def render_graph_explorer_cytoscape(graph_service):
# --- GRAPH ELEMENTS --- # --- GRAPH ELEMENTS ---
cy_elements = [] cy_elements = []
# Validierung: Prüfe ob nodes_data vorhanden ist
if not nodes_data:
st.warning("⚠️ Keine Knoten gefunden. Bitte wähle eine andere Notiz.")
# Zeige trotzdem den Inspector, falls Daten vorhanden
if inspected_data:
st.info(f"**Hinweis:** Die Notiz '{inspected_data.get('title', inspected_id)}' wurde gefunden, hat aber keine Verbindungen im Graphen.")
return
# Erstelle Set aller Node-IDs für schnelle Validierung
node_ids = {n.id for n in nodes_data if hasattr(n, 'id') and n.id}
# Nodes hinzufügen
for n in nodes_data: for n in nodes_data:
if not hasattr(n, 'id') or not n.id:
continue
is_center = (n.id == center_id) is_center = (n.id == center_id)
is_inspected = (n.id == inspected_id) is_inspected = (n.id == inspected_id)
tooltip_text = n.title if n.title else n.label tooltip_text = getattr(n, 'title', None) or getattr(n, 'label', '')
display_label = n.label display_label = getattr(n, 'label', str(n.id))
if len(display_label) > 15 and " " in display_label: if len(display_label) > 15 and " " in display_label:
display_label = display_label.replace(" ", "\n", 1) display_label = display_label.replace(" ", "\n", 1)
@ -202,7 +228,7 @@ def render_graph_explorer_cytoscape(graph_service):
"data": { "data": {
"id": n.id, "id": n.id,
"label": display_label, "label": display_label,
"bg_color": n.color, "bg_color": getattr(n, 'color', '#8395a7'),
"tooltip": tooltip_text "tooltip": tooltip_text
}, },
# Wir steuern das Aussehen rein über Klassen (.inspected / .center) # Wir steuern das Aussehen rein über Klassen (.inspected / .center)
@ -211,18 +237,22 @@ def render_graph_explorer_cytoscape(graph_service):
} }
cy_elements.append(cy_node) cy_elements.append(cy_node)
for e in edges_data: # Edges hinzufügen - nur wenn beide Nodes im Graph vorhanden sind
target_id = getattr(e, "to", getattr(e, "target", None)) if edges_data:
if target_id: for e in edges_data:
cy_edge = { source_id = getattr(e, "source", None)
"data": { target_id = getattr(e, "to", getattr(e, "target", None))
"source": e.source, # Nur hinzufügen, wenn beide IDs vorhanden UND beide Nodes im Graph sind
"target": target_id, if source_id and target_id and source_id in node_ids and target_id in node_ids:
"label": e.label, cy_edge = {
"line_color": e.color "data": {
"source": source_id,
"target": target_id,
"label": getattr(e, "label", ""),
"line_color": getattr(e, "color", "#bdc3c7")
}
} }
} cy_elements.append(cy_edge)
cy_elements.append(cy_edge)
# --- STYLESHEET --- # --- STYLESHEET ---
stylesheet = [ stylesheet = [
@ -292,43 +322,47 @@ def render_graph_explorer_cytoscape(graph_service):
] ]
# --- RENDER --- # --- RENDER ---
graph_key = f"cy_{center_id}_{st.session_state.cy_depth}_{st.session_state.cy_ideal_edge_len}" # Nur rendern, wenn Elemente vorhanden sind
if not cy_elements:
st.warning("⚠️ Keine Graph-Elemente zum Anzeigen gefunden.")
else:
graph_key = f"cy_{center_id}_{st.session_state.cy_depth}_{st.session_state.cy_ideal_edge_len}"
clicked_elements = cytoscape( clicked_elements = cytoscape(
elements=cy_elements, elements=cy_elements,
stylesheet=stylesheet, stylesheet=stylesheet,
layout={ layout={
"name": "cose", "name": "cose",
"idealEdgeLength": st.session_state.cy_ideal_edge_len, "idealEdgeLength": st.session_state.cy_ideal_edge_len,
"nodeOverlap": 20, "nodeOverlap": 20,
"refresh": 20, "refresh": 20,
"fit": True, "fit": True,
"padding": 50, "padding": 50,
"randomize": False, "randomize": False,
"componentSpacing": 100, "componentSpacing": 100,
"nodeRepulsion": st.session_state.cy_node_repulsion, "nodeRepulsion": st.session_state.cy_node_repulsion,
"edgeElasticity": 100, "edgeElasticity": 100,
"nestingFactor": 5, "nestingFactor": 5,
"gravity": 80, "gravity": 80,
"numIter": 1000, "numIter": 1000,
"initialTemp": 200, "initialTemp": 200,
"coolingFactor": 0.95, "coolingFactor": 0.95,
"minTemp": 1.0, "minTemp": 1.0,
"animate": False "animate": False
}, },
key=graph_key, key=graph_key,
height="700px" height="700px"
) )
# --- EVENT HANDLING --- # --- EVENT HANDLING ---
if clicked_elements: if clicked_elements:
clicked_nodes = clicked_elements.get("nodes", []) clicked_nodes = clicked_elements.get("nodes", [])
if clicked_nodes: if clicked_nodes:
clicked_id = clicked_nodes[0] clicked_id = clicked_nodes[0]
if clicked_id != st.session_state.graph_inspected_id: if clicked_id != st.session_state.graph_inspected_id:
st.session_state.graph_inspected_id = clicked_id st.session_state.graph_inspected_id = clicked_id
st.rerun() st.rerun()
else: else:
st.info("👈 Bitte wähle links eine Notiz aus.") st.info("👈 Bitte wähle links eine Notiz aus.")

View File

@ -78,7 +78,7 @@ class GraphExplorerService:
# A. Fulltext für Center Node holen (Chunks zusammenfügen) # A. Fulltext für Center Node holen (Chunks zusammenfügen)
center_text = self._fetch_full_text_stitched(center_note_id) center_text = self._fetch_full_text_stitched(center_note_id)
if center_note_id in nodes_dict: if center_note_id in nodes_dict:
orig_title = nodes_dict[center_note_id].title orig_title = getattr(nodes_dict[center_note_id], 'title', None) or getattr(nodes_dict[center_note_id], 'label', '')
clean_full = self._clean_markdown(center_text[:2000]) clean_full = self._clean_markdown(center_text[:2000])
# Wir packen den Text in den Tooltip (title attribute) # Wir packen den Text in den Tooltip (title attribute)
nodes_dict[center_note_id].title = f"{orig_title}\n\n📄 INHALT:\n{clean_full}..." nodes_dict[center_note_id].title = f"{orig_title}\n\n📄 INHALT:\n{clean_full}..."
@ -91,7 +91,8 @@ class GraphExplorerService:
if nid != center_note_id: if nid != center_note_id:
prev_raw = previews.get(nid, "Kein Vorschau-Text.") prev_raw = previews.get(nid, "Kein Vorschau-Text.")
clean_prev = self._clean_markdown(prev_raw[:600]) clean_prev = self._clean_markdown(prev_raw[:600])
node_obj.title = f"{node_obj.title}\n\n🔍 VORSCHAU:\n{clean_prev}..." current_title = getattr(node_obj, 'title', None) or getattr(node_obj, 'label', '')
node_obj.title = f"{current_title}\n\n🔍 VORSCHAU:\n{clean_prev}..."
# Graphen bauen (Nodes & Edges finalisieren) # Graphen bauen (Nodes & Edges finalisieren)
final_edges = [] final_edges = []
@ -167,27 +168,28 @@ class GraphExplorerService:
results = [] results = []
if not note_ids:
return results
# 1. OUTGOING EDGES (Der "Owner"-Fix) # 1. OUTGOING EDGES (Der "Owner"-Fix)
# Wir suchen Kanten, die im Feld 'note_id' (Owner) eine unserer Notizen haben. # Wir suchen Kanten, die im Feld 'note_id' (Owner) eine unserer Notizen haben.
# Das findet ALLE ausgehenden Kanten, egal ob sie an einem Chunk oder der Note hängen. # Das findet ALLE ausgehenden Kanten, egal ob sie an einem Chunk oder der Note hängen.
if note_ids: out_filter = models.Filter(must=[
out_filter = models.Filter(must=[ models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids)),
models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids)), models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))
models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES})) ])
]) # Limit erhöht, um alle Kanten zu finden
# Limit hoch, um alles zu finden res_out, _ = self.client.scroll(self.edges_col, scroll_filter=out_filter, limit=2000, with_payload=True)
res_out, _ = self.client.scroll(self.edges_col, scroll_filter=out_filter, limit=500, with_payload=True) results.extend(res_out)
results.extend(res_out)
# 2. INCOMING EDGES (Ziel = Chunk ID oder Titel oder Note ID) # 2. INCOMING EDGES (Ziel = Chunk ID oder Titel oder Note ID)
# Hier müssen wir Chunks auflösen, um Treffer auf Chunks zu finden. # Hier müssen wir Chunks auflösen, um Treffer auf Chunks zu finden.
# Chunk IDs der aktuellen Notes holen # Chunk IDs der aktuellen Notes holen (Limit erhöht)
chunk_ids = [] chunk_ids = []
if note_ids: c_filter = models.Filter(must=[models.FieldCondition(key="note_id", match=models.MatchAny(any=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=1000, with_payload=False)
chunks, _ = self.client.scroll(self.chunks_col, scroll_filter=c_filter, limit=300) chunk_ids = [c.id for c in chunks]
chunk_ids = [c.id for c in chunks]
shoulds = [] shoulds = []
# Case A: Edge zeigt auf einen unserer Chunks # Case A: Edge zeigt auf einen unserer Chunks
@ -195,42 +197,66 @@ class GraphExplorerService:
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=chunk_ids))) shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=chunk_ids)))
# Case B: Edge zeigt direkt auf unsere Note ID # Case B: Edge zeigt direkt auf unsere Note ID
if note_ids: shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=note_ids)))
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=note_ids)))
# Case C: Edge zeigt auf unseren Titel (Wikilinks) # Case C: Edge zeigt auf unseren Titel (Wikilinks) - auch wenn note_title None ist, versuchen wir es mit den Titeln der Notes
if note_title: 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.MatchValue(value=note_title)))
else:
# Fallback: Lade Titel der Notes, wenn note_title nicht übergeben wurde
for nid in note_ids:
note = self._fetch_note_cached(nid)
if note and note.get("title"):
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=note.get("title"))))
if shoulds: if shoulds:
in_filter = models.Filter( in_filter = 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_filter, limit=500, with_payload=True) # Limit erhöht, um alle eingehenden Kanten zu finden
res_in, _ = self.client.scroll(self.edges_col, scroll_filter=in_filter, limit=2000, with_payload=True)
results.extend(res_in) results.extend(res_in)
return results return results
def _find_connected_edges_batch(self, note_ids): def _find_connected_edges_batch(self, note_ids):
# Wrapper für Level 2 Suche # Wrapper für Level 2 Suche - lade Titel für alle Notes
return self._find_connected_edges(note_ids) note_titles = []
for nid in note_ids:
note = self._fetch_note_cached(nid)
if note and note.get("title"):
note_titles.append(note.get("title"))
# Verwende den ersten Titel als Fallback (oder None, wenn keine gefunden)
title = note_titles[0] if note_titles else None
return self._find_connected_edges(note_ids, note_title=title)
def _process_edge(self, record, nodes_dict, unique_edges, current_depth): def _process_edge(self, record, nodes_dict, unique_edges, current_depth):
"""Verarbeitet eine rohe Edge, löst IDs auf und fügt sie den Dictionaries hinzu.""" """Verarbeitet eine rohe Edge, löst IDs auf und fügt sie den Dictionaries hinzu."""
if not record or not record.payload:
return None, None
payload = record.payload payload = record.payload
src_ref = payload.get("source_id") src_ref = payload.get("source_id")
tgt_ref = payload.get("target_id") tgt_ref = payload.get("target_id")
kind = payload.get("kind") kind = payload.get("kind")
provenance = payload.get("provenance", "explicit") provenance = payload.get("provenance", "explicit")
# Prüfe, ob beide Referenzen vorhanden sind
if not src_ref or not tgt_ref:
return None, None
# IDs zu Notes auflösen # IDs zu Notes auflösen
src_note = self._resolve_note_from_ref(src_ref) src_note = self._resolve_note_from_ref(src_ref)
tgt_note = self._resolve_note_from_ref(tgt_ref) tgt_note = self._resolve_note_from_ref(tgt_ref)
if src_note and tgt_note: if src_note and tgt_note:
src_id = src_note['note_id'] src_id = src_note.get('note_id')
tgt_id = tgt_note['note_id'] tgt_id = tgt_note.get('note_id')
# Prüfe, ob beide IDs vorhanden sind
if not src_id or not tgt_id:
return None, None
if src_id != tgt_id: if src_id != tgt_id:
# Nodes hinzufügen # Nodes hinzufügen
@ -245,7 +271,7 @@ class GraphExplorerService:
# Bevorzuge explizite Kanten vor Smart Kanten # Bevorzuge explizite Kanten vor Smart Kanten
is_current_explicit = (provenance in ["explicit", "rule"]) is_current_explicit = (provenance in ["explicit", "rule"])
if existing: if existing:
is_existing_explicit = (existing['provenance'] in ["explicit", "rule"]) is_existing_explicit = (existing.get('provenance', '') in ["explicit", "rule"])
if is_existing_explicit and not is_current_explicit: if is_existing_explicit and not is_current_explicit:
should_update = False should_update = False
@ -275,25 +301,49 @@ class GraphExplorerService:
try: try:
# Versuch 1: Chunk ID direkt # Versuch 1: Chunk ID direkt
res = self.client.retrieve(self.chunks_col, ids=[ref_str], with_payload=True) res = self.client.retrieve(self.chunks_col, ids=[ref_str], with_payload=True)
if res: return self._fetch_note_cached(res[0].payload.get("note_id")) if res and res[0].payload:
except: pass note_id = res[0].payload.get("note_id")
if note_id:
note = self._fetch_note_cached(note_id)
if note: return note
except Exception:
pass
# Versuch 2: NoteID#Section (Hash abtrennen) # Versuch 2: NoteID#Section (Hash abtrennen)
possible_note_id = ref_str.split("#")[0] possible_note_id = ref_str.split("#")[0]
if self._fetch_note_cached(possible_note_id): return self._fetch_note_cached(possible_note_id) note = self._fetch_note_cached(possible_note_id)
if note: return note
# Fall B: Note ID direkt # Fall B: Note ID direkt
if self._fetch_note_cached(ref_str): return self._fetch_note_cached(ref_str) note = self._fetch_note_cached(ref_str)
if note: return note
# Fall C: Titel (exakte Übereinstimmung)
try:
res, _ = self.client.scroll(
collection_name=self.notes_col,
scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchValue(value=str(ref_str)))]),
limit=1, with_payload=True
)
if res and res[0].payload:
self._note_cache[res[0].payload['note_id']] = res[0].payload
return res[0].payload
except Exception:
pass
# Fall D: Titel (Text-Suche für Fuzzy-Matching, falls exakte Suche fehlschlägt)
try:
res, _ = self.client.scroll(
collection_name=self.notes_col,
scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchText(text=str(ref_str)))]),
limit=1, with_payload=True
)
if res and res[0].payload:
self._note_cache[res[0].payload['note_id']] = res[0].payload
return res[0].payload
except Exception:
pass
# Fall C: Titel
res, _ = self.client.scroll(
collection_name=self.notes_col,
scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchValue(value=ref_str))]),
limit=1, with_payload=True
)
if res:
self._note_cache[res[0].payload['note_id']] = res[0].payload
return res[0].payload
return None return None
def _add_node_to_dict(self, node_dict, note_payload, level=1): def _add_node_to_dict(self, node_dict, note_payload, level=1):