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")
# 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_show_labels", True)
# Defaults angepasst für BarnesHut (andere Skala!)
st.session_state.setdefault("graph_spacing", 150)
st.session_state.setdefault("graph_gravity", -3000)
st.session_state.setdefault("graph_spacing", 250)
st.session_state.setdefault("graph_gravity", -4000)
col_ctrl, col_graph = st.columns([1, 4])
# --- LINKE SPALTE: CONTROLS ---
with col_ctrl:
st.subheader("Fokus")
# Suche
# Sucheingabe
search_term = st.text_input("Suche Notiz", placeholder="Titel eingeben...")
options = {}
# Suchlogik Qdrant
if search_term:
hits, _ = graph_service.client.scroll(
collection_name=f"{COLLECTION_PREFIX}_notes",
@ -42,19 +43,18 @@ def render_graph_explorer(graph_service):
st.divider()
# View Settings
# Layout & Physik Einstellungen
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_show_labels = st.checkbox("Kanten-Beschriftung", st.session_state.graph_show_labels)
st.markdown("**Physik (BarnesHut)**")
# ACHTUNG: BarnesHut reagiert anders. Spring Length ist wichtig.
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, -1000, st.session_state.graph_gravity, help="Wie stark sollen sich Knoten abstoßen?")
st.session_state.graph_spacing = st.slider("Federlänge (Abstand)", 50, 800, st.session_state.graph_spacing)
st.session_state.graph_gravity = st.slider("Abstoßung (Gravity)", -20000, -500, st.session_state.graph_gravity)
if st.button("Reset Layout"):
st.session_state.graph_spacing = 150
st.session_state.graph_gravity = -3000
st.session_state.graph_spacing = 250
st.session_state.graph_gravity = -4000
st.rerun()
st.divider()
@ -62,57 +62,55 @@ def render_graph_explorer(graph_service):
for k, v in list(GRAPH_COLORS.items())[:8]:
st.markdown(f"<span style='color:{v}'>●</span> {k}", unsafe_allow_html=True)
# --- RECHTE SPALTE: GRAPH & ACTION BAR ---
with col_graph:
center_id = st.session_state.graph_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()
# Graph Laden
# Graph und Daten laden
with st.spinner(f"Lade Graph..."):
# Daten laden (Cache wird genutzt)
nodes, edges = graph_service.get_ego_graph(
center_id,
depth=st.session_state.graph_depth,
show_labels=st.session_state.graph_show_labels
)
# Fetch Note Data für Button & Debug
# Wir holen die Metadaten (inkl. path), was für den Editor-Callback reicht.
note_data = graph_service._fetch_note_cached(center_id)
# WICHTIG: Volle Daten inkl. Stitching für Editor holen
note_data = graph_service.get_note_with_full_content(center_id)
# --- ACTION BAR RENDEREN ---
# Action Bar rendern
with action_container:
c_act1, c_act2 = st.columns([3, 1])
with c_act1:
c1, c2 = st.columns([3, 1])
with c1:
st.caption(f"Aktives Zentrum: **{center_id}**")
with c_act2:
with c2:
if note_data:
st.button("📝 Bearbeiten",
use_container_width=True,
on_click=switch_to_editor_callback,
args=(note_data,))
else:
st.error("Daten nicht verfügbar")
st.error("Datenfehler: Note nicht gefunden")
# DATA INSPECTOR (Payload Debug)
with st.expander("🕵️ Data Inspector (Payload Debug)", expanded=False):
# Debug Inspector
with st.expander("🕵️ Data Inspector", expanded=False):
if note_data:
st.json(note_data)
if 'path' not in note_data:
st.error("ACHTUNG: Feld 'path' fehlt im Qdrant-Payload! Update-Modus wird nicht funktionieren.")
else:
st.success(f"Pfad gefunden: {note_data['path']}")
else:
st.info("Keine Daten geladen.")
if 'path' in note_data:
st.success(f"Pfad OK: {note_data['path']}")
else:
st.error("Pfad fehlt!")
else:
st.info("Leer.")
if not nodes:
st.warning("Keine Daten gefunden.")
else:
# --- CONFIGURATION (BarnesHut) ---
# Height-Trick für Re-Render (da key-Parameter nicht funktioniert)
# Ändere Height minimal basierend auf Gravity
# Height-Trick für Re-Render (da key-Parameter manchmal crasht)
dyn_height = 800 + (abs(st.session_state.graph_gravity) % 5)
config = Config(
@ -121,11 +119,10 @@ def render_graph_explorer(graph_service):
directed=True,
physics={
"enabled": True,
# BarnesHut ist der Standard und stabilste Solver für Agraph
"solver": "barnesHut",
"barnesHut": {
"gravitationalConstant": st.session_state.graph_gravity,
"centralGravity": 0.3,
"centralGravity": 0.005,
"springLength": st.session_state.graph_spacing,
"springConstant": 0.04,
"damping": 0.09,
@ -141,7 +138,7 @@ def render_graph_explorer(graph_service):
return_value = agraph(nodes=nodes, edges=edges, config=config)
# Interaktions-Logik
# Interaktions-Logik (Klick auf Node)
if return_value:
if return_value != center_id:
# Navigation: Neues Zentrum setzen
@ -152,4 +149,4 @@ def render_graph_explorer(graph_service):
st.toast(f"Zentrum: {return_value}")
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._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):
"""
Erstellt den Ego-Graphen um eine zentrale Notiz.
@ -25,6 +45,7 @@ class GraphExplorerService:
if not center_note: return [], []
self._add_node_to_dict(nodes_dict, center_note, level=0)
# Initialset für Suche
level_1_ids = {center_note_id}
# Suche Kanten für Center (L1)
@ -36,7 +57,7 @@ class GraphExplorerService:
if tgt_id: level_1_ids.add(tgt_id)
# 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})
if l1_subset:
l2_edges = self._find_connected_edges_batch(l1_subset)
@ -107,6 +128,7 @@ class GraphExplorerService:
full_text = []
for c in chunks:
# 'text' ist der reine Inhalt ohne Overlap
txt = c.payload.get('text', '')
if txt: full_text.append(txt)
@ -133,13 +155,16 @@ class GraphExplorerService:
def _find_connected_edges(self, note_ids, note_title=None):
"""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 = []
# 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
# Source kann sein: Eine Chunk ID ODER die Note ID selbst (bei direkten Links)
source_candidates = chunk_ids + note_ids
@ -150,22 +175,27 @@ class GraphExplorerService:
# FIX: MatchExcept Workaround für Pydantic
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)
# 3. Incoming Edges suchen
# Target kann sein: Chunk ID, Note ID, oder Note Titel (Wikilinks)
shoulds = []
if chunk_ids: shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=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=note_ids)))
if chunk_ids:
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=chunk_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:
in_f = 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_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)
return results