problem fix
ausgehende Kanten
This commit is contained in:
parent
3861246ac6
commit
f7a4dab707
|
|
@ -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.")
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user