"""
Interactive HTML graph visualization of memory system.
Uses pyvis to create a smooth, interactive network graph that can be
explored in the browser. Shows all memory units and their links with weights.
"""
import psycopg2
from dotenv import load_dotenv
import os
from pyvis.network import Network
import networkx as nx
load_dotenv()
def create_interactive_graph():
"""Create an interactive HTML graph visualization."""
# Connect to database
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cursor = conn.cursor()
# Get all memory units (no agent_id filter)
cursor.execute("""
SELECT id, text, event_date, context
FROM memory_units
ORDER BY event_date
""")
units = cursor.fetchall()
# Get all links with weights (no agent_id filter)
cursor.execute("""
SELECT
ml.from_unit_id,
ml.to_unit_id,
ml.link_type,
ml.weight,
e.canonical_name as entity_name
FROM memory_links ml
LEFT JOIN entities e ON ml.entity_id = e.id
ORDER BY ml.link_type, ml.weight DESC
""")
links = cursor.fetchall()
# Get entity information (no agent_id filter)
cursor.execute("""
SELECT ue.unit_id, e.canonical_name, e.entity_type
FROM unit_entities ue
JOIN entities e ON ue.entity_id = e.id
ORDER BY ue.unit_id
""")
unit_entities = cursor.fetchall()
cursor.close()
conn.close()
# Build entity mapping
entity_map = {}
for unit_id, entity_name, entity_type in unit_entities:
if unit_id not in entity_map:
entity_map[unit_id] = []
entity_map[unit_id].append(f"{entity_name} ({entity_type})")
# Create pyvis network
net = Network(
height="900px",
width="100%",
bgcolor="#ffffff",
font_color="#000000",
heading="Entity-Aware Memory Graph - Interactive Visualization"
)
# Configure physics for smooth layout with performance optimizations
net.set_options("""
{
"nodes": {
"font": {
"size": 14,
"face": "Tahoma"
},
"borderWidth": 2,
"borderWidthSelected": 3
},
"edges": {
"smooth": {
"enabled": false
},
"font": {
"size": 10,
"align": "middle"
}
},
"physics": {
"enabled": true,
"stabilization": {
"enabled": true,
"iterations": 100,
"updateInterval": 10
},
"barnesHut": {
"gravitationalConstant": -12000,
"centralGravity": 0.2,
"springLength": 350,
"springConstant": 0.02,
"damping": 0.09,
"avoidOverlap": 0.8
},
"solver": "barnesHut",
"timestep": 0.5,
"adaptiveTimestep": true
},
"interaction": {
"hover": true,
"tooltipDelay": 100,
"navigationButtons": true,
"keyboard": true
}
}
""")
# Add nodes
for unit_id, text, event_date, context in units:
# Truncate text for display
display_text = text[:50] + "..." if len(text) > 50 else text
# Get entities
entities = entity_map.get(unit_id, [])
entity_str = "\\n".join(entities) if entities else "No entities"
# Build node label and title (hover)
label = display_text
title = f"""
Text: {text}
Date: {event_date.date()}
Context: {context}
Entities: {entity_str}
"""
# Color by entity count
if len(entities) == 0:
color = "#e0e0e0" # Gray
size = 20
elif len(entities) == 1:
color = "#90caf9" # Light blue
size = 25
else:
color = "#42a5f5" # Dark blue
size = 30
net.add_node(
str(unit_id),
label=label,
title=title,
color=color,
size=size,
shape="box",
font={"color": "#000000"}
)
# Add edges with colors and weights
for from_id, to_id, link_type, weight, entity_name in links:
# Set color and style based on link type
if link_type == 'temporal':
color = "#00bcd4" # Cyan
dashes = [5, 5]
width = 0.5
label = f"T: {weight:.2f}"
elif link_type == 'semantic':
color = "#ff69b4" # Pink
dashes = False
width = 0.5
label = f"S: {weight:.2f}"
elif link_type == 'entity':
color = "#ffd700" # Gold
dashes = False
width = 0.8
label = f"{entity_name}: {weight:.2f}"
else:
color = "#999999"
dashes = False
width = 0.5
label = f"{weight:.2f}"
net.add_edge(
str(from_id),
str(to_id),
value=weight * 1, # Scale for visual thickness
color=color,
dashes=dashes,
width=width,
label=label,
title=f"{link_type.upper()}: {weight:.3f}" + (f" (Entity: {entity_name})" if entity_name else "")
)
# Add legend as HTML
legend_html = """