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")
if search_term:
hits, _ = graph_service.client.scroll(
collection_name=f"{COLLECTION_PREFIX}_notes",
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}
if options:
selected_title = st.selectbox("Ergebnisse:", list(options.keys()), key="cy_select")
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()
try:
hits, _ = graph_service.client.scroll(
collection_name=f"{COLLECTION_PREFIX}_notes",
limit=10,
scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchText(text=search_term))])
)
options = {}
for h in hits:
if h.payload and 'title' in h.payload and 'note_id' in h.payload:
options[h.payload['title']] = h.payload['note_id']
if options:
selected_title = st.selectbox("Ergebnisse:", list(options.keys()), key="cy_select")
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()
@ -174,7 +180,12 @@ def render_graph_explorer_cytoscape(graph_service):
st.markdown(f"**ID:** `{inspected_data.get('note_id')}`")
st.markdown(f"**Typ:** `{inspected_data.get('type')}`")
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 ""
st.markdown(f"**Pfad:** {path_check}")
@ -189,12 +200,27 @@ def render_graph_explorer_cytoscape(graph_service):
# --- GRAPH 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:
if not hasattr(n, 'id') or not n.id:
continue
is_center = (n.id == center_id)
is_inspected = (n.id == inspected_id)
tooltip_text = n.title if n.title else n.label
display_label = n.label
tooltip_text = getattr(n, 'title', None) or getattr(n, 'label', '')
display_label = getattr(n, 'label', str(n.id))
if len(display_label) > 15 and " " in display_label:
display_label = display_label.replace(" ", "\n", 1)
@ -202,7 +228,7 @@ def render_graph_explorer_cytoscape(graph_service):
"data": {
"id": n.id,
"label": display_label,
"bg_color": n.color,
"bg_color": getattr(n, 'color', '#8395a7'),
"tooltip": tooltip_text
},
# 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)
for e in edges_data:
target_id = getattr(e, "to", getattr(e, "target", None))
if target_id:
cy_edge = {
"data": {
"source": e.source,
"target": target_id,
"label": e.label,
"line_color": e.color
# Edges hinzufügen - nur wenn beide Nodes im Graph vorhanden sind
if edges_data:
for e in edges_data:
source_id = getattr(e, "source", None)
target_id = getattr(e, "to", getattr(e, "target", None))
# Nur hinzufügen, wenn beide IDs vorhanden UND beide Nodes im Graph sind
if source_id and target_id and source_id in node_ids and target_id in node_ids:
cy_edge = {
"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 = [
@ -292,43 +322,47 @@ def render_graph_explorer_cytoscape(graph_service):
]
# --- 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(
elements=cy_elements,
stylesheet=stylesheet,
layout={
"name": "cose",
"idealEdgeLength": st.session_state.cy_ideal_edge_len,
"nodeOverlap": 20,
"refresh": 20,
"fit": True,
"padding": 50,
"randomize": False,
"componentSpacing": 100,
"nodeRepulsion": st.session_state.cy_node_repulsion,
"edgeElasticity": 100,
"nestingFactor": 5,
"gravity": 80,
"numIter": 1000,
"initialTemp": 200,
"coolingFactor": 0.95,
"minTemp": 1.0,
"animate": False
},
key=graph_key,
height="700px"
)
clicked_elements = cytoscape(
elements=cy_elements,
stylesheet=stylesheet,
layout={
"name": "cose",
"idealEdgeLength": st.session_state.cy_ideal_edge_len,
"nodeOverlap": 20,
"refresh": 20,
"fit": True,
"padding": 50,
"randomize": False,
"componentSpacing": 100,
"nodeRepulsion": st.session_state.cy_node_repulsion,
"edgeElasticity": 100,
"nestingFactor": 5,
"gravity": 80,
"numIter": 1000,
"initialTemp": 200,
"coolingFactor": 0.95,
"minTemp": 1.0,
"animate": False
},
key=graph_key,
height="700px"
)
# --- EVENT HANDLING ---
if clicked_elements:
clicked_nodes = clicked_elements.get("nodes", [])
if clicked_nodes:
clicked_id = clicked_nodes[0]
if clicked_id != st.session_state.graph_inspected_id:
st.session_state.graph_inspected_id = clicked_id
st.rerun()
# --- EVENT HANDLING ---
if clicked_elements:
clicked_nodes = clicked_elements.get("nodes", [])
if clicked_nodes:
clicked_id = clicked_nodes[0]
if clicked_id != st.session_state.graph_inspected_id:
st.session_state.graph_inspected_id = clicked_id
st.rerun()
else:
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)
center_text = self._fetch_full_text_stitched(center_note_id)
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])
# Wir packen den Text in den Tooltip (title attribute)
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:
prev_raw = previews.get(nid, "Kein Vorschau-Text.")
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)
final_edges = []
@ -167,27 +168,28 @@ class GraphExplorerService:
results = []
if not note_ids:
return results
# 1. OUTGOING EDGES (Der "Owner"-Fix)
# 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.
if note_ids:
out_filter = models.Filter(must=[
models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids)),
models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))
])
# Limit hoch, um alles zu finden
res_out, _ = self.client.scroll(self.edges_col, scroll_filter=out_filter, limit=500, with_payload=True)
results.extend(res_out)
out_filter = models.Filter(must=[
models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids)),
models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))
])
# Limit erhöht, um alle Kanten zu finden
res_out, _ = self.client.scroll(self.edges_col, scroll_filter=out_filter, limit=2000, with_payload=True)
results.extend(res_out)
# 2. INCOMING EDGES (Ziel = Chunk ID oder Titel oder Note ID)
# 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 = []
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]
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)
chunk_ids = [c.id for c in chunks]
shoulds = []
# 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)))
# 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:
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:
in_filter = models.Filter(
must=[models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))],
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)
return results
def _find_connected_edges_batch(self, note_ids):
# Wrapper für Level 2 Suche
return self._find_connected_edges(note_ids)
# Wrapper für Level 2 Suche - lade Titel für alle Notes
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):
"""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
src_ref = payload.get("source_id")
tgt_ref = payload.get("target_id")
kind = payload.get("kind")
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
src_note = self._resolve_note_from_ref(src_ref)
tgt_note = self._resolve_note_from_ref(tgt_ref)
if src_note and tgt_note:
src_id = src_note['note_id']
tgt_id = tgt_note['note_id']
src_id = src_note.get('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:
# Nodes hinzufügen
@ -245,7 +271,7 @@ class GraphExplorerService:
# Bevorzuge explizite Kanten vor Smart Kanten
is_current_explicit = (provenance in ["explicit", "rule"])
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:
should_update = False
@ -275,25 +301,49 @@ class GraphExplorerService:
try:
# Versuch 1: Chunk ID direkt
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"))
except: pass
if res and res[0].payload:
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)
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
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
def _add_node_to_dict(self, node_dict, note_payload, level=1):