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")
|
||||
|
||||
# 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.")
|
||||
if 'path' in note_data:
|
||||
st.success(f"Pfad OK: {note_data['path']}")
|
||||
else:
|
||||
st.success(f"Pfad gefunden: {note_data['path']}")
|
||||
st.error("Pfad fehlt!")
|
||||
else:
|
||||
st.info("Keine Daten geladen.")
|
||||
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.")
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user