""" 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 = """

Legend

Link Types:

Temporal - Time-based (cyan, dashed)
Semantic - Meaning-based (pink, solid)
Entity - Same entity (gold)

Node Colors:

Gray - No entities
Light Blue - 1 entity
Dark Blue - 2+ entities
Tip: Hover over nodes/edges for details
Controls: Drag to move, scroll to zoom
""" # Generate the HTML output_file = "memory_graph_interactive.html" net.save_graph(output_file) # Read the generated HTML and inject our legend with open(output_file, 'r') as f: html_content = f.read() # Inject legend after the opening body tag html_content = html_content.replace('', '' + legend_html) # Add script to disable physics after stabilization for better performance physics_script = """ """ html_content = html_content.replace('', physics_script + '') # Write back with open(output_file, 'w') as f: f.write(html_content) print(f"\n{'='*80}") print("INTERACTIVE GRAPH GENERATED") print(f"{'='*80}") print(f"\nFile: {output_file}") print(f"Units: {len(units)}") print(f"Links: {len(links)}") print("\nFeatures:") print(" • Smooth, physics-based layout") print(" • Interactive - drag nodes, zoom, pan") print(" • Hover for details on nodes and edges") print(" • Color-coded by link type and entity count") print(" • Built-in navigation controls") print(f"\n{'='*80}") print(f"✓ Open {output_file} in your browser to explore!") print(f"{'='*80}\n") if __name__ == "__main__": create_interactive_graph()