Spaces:
Sleeping
Sleeping
| """ | |
| Brain Dump Sanctuary - Core Pipeline (Day 1 MVP + LangChain Integration) | |
| Now with Neo4j Graph Database Backend | |
| Runs entirely on CPU - no GPU needed | |
| """ | |
| import numpy as np | |
| from sentence_transformers import SentenceTransformer | |
| from sklearn.cluster import HDBSCAN | |
| import umap | |
| import plotly.graph_objects as go | |
| from datetime import datetime | |
| import json | |
| import os | |
| from dotenv import load_dotenv | |
| from neo4j import GraphDatabase | |
| from neo4j.exceptions import ServiceUnavailable, AuthError | |
| # Load environment variables from .env file | |
| load_dotenv() | |
| # LangChain imports for cluster labeling with Gemini | |
| from langchain_google_genai import ChatGoogleGenerativeAI | |
| from langchain_core.prompts import ChatPromptTemplate | |
| from langchain_core.output_parsers import StrOutputParser | |
| from langchain_core.runnables import RunnableWithFallbacks | |
| # ============== 1. DATABASE LAYER - Neo4j ============== | |
| class BrainDumpDB: | |
| """ | |
| Neo4j-based graph database for Brain Dump Sanctuary. | |
| Graph Schema: | |
| - Node: Dump {id, text, embedding[], cluster_id, created_at} | |
| - Node: Cluster {id, label, description, created_at} | |
| - Relationship: Dump -[:IN_CLUSTER]-> Cluster | |
| - Relationship: Dump -[:SIMILAR_TO {weight}]-> Dump | |
| """ | |
| def __init__(self): | |
| """Initialize Neo4j connection from environment variables.""" | |
| self.uri = os.getenv("NEO4J_URI", "neo4j://localhost:7687") | |
| self.user = os.getenv("NEO4J_USER", "neo4j") | |
| self.password = os.getenv("NEO4J_PASSWORD", "password") | |
| try: | |
| self.driver = GraphDatabase.driver(self.uri, auth=(self.user, self.password)) | |
| self.driver.verify_connectivity() | |
| print(f"✓ Connected to Neo4j at {self.uri}") | |
| except (ServiceUnavailable, AuthError) as e: | |
| print(f"✗ Failed to connect to Neo4j: {e}") | |
| print(" Make sure NEO4J_URI, NEO4J_USER, and NEO4J_PASSWORD are set in .env") | |
| raise | |
| self._init_schema() | |
| def _init_schema(self): | |
| """Initialize graph schema with nodes and indexes.""" | |
| with self.driver.session() as session: | |
| # Create Dump nodes with indexes | |
| session.run(""" | |
| CREATE INDEX dump_id_index IF NOT EXISTS | |
| FOR (d:Dump) ON (d.id) | |
| """) | |
| session.run(""" | |
| CREATE INDEX dump_created_index IF NOT EXISTS | |
| FOR (d:Dump) ON (d.created_at) | |
| """) | |
| # Create Cluster nodes with indexes | |
| session.run(""" | |
| CREATE INDEX cluster_id_index IF NOT EXISTS | |
| FOR (c:Cluster) ON (c.id) | |
| """) | |
| print("✓ Graph schema initialized") | |
| def check_duplicate(self, text): | |
| """ | |
| Check if a dump with the same text already exists in the database. | |
| Returns the dump_id if duplicate found, None otherwise. | |
| """ | |
| with self.driver.session() as session: | |
| result = session.run(""" | |
| MATCH (d:Dump {text: $text}) | |
| RETURN d.id as dump_id | |
| LIMIT 1 | |
| """, text=text) | |
| row = result.single() | |
| if row: | |
| return row["dump_id"] | |
| return None | |
| def add_dump(self, text): | |
| """ | |
| Add a new brain dump to the database. | |
| Returns (dump_id, is_duplicate) tuple. | |
| If duplicate exists, returns (existing_dump_id, True). | |
| If new dump created, returns (new_dump_id, False). | |
| """ | |
| # Check for duplicate first | |
| existing_id = self.check_duplicate(text) | |
| if existing_id: | |
| return (existing_id, True) | |
| # No duplicate, create new dump | |
| with self.driver.session() as session: | |
| result = session.run(""" | |
| CREATE (d:Dump { | |
| id: randomUuid(), | |
| text: $text, | |
| created_at: datetime() | |
| }) | |
| RETURN d.id as dump_id | |
| """, text=text) | |
| dump_id = result.single()["dump_id"] | |
| return (dump_id, False) | |
| def get_all_dumps(self): | |
| """Get all dumps with their cluster assignments and timestamps.""" | |
| with self.driver.session() as session: | |
| result = session.run(""" | |
| MATCH (d:Dump) | |
| OPTIONAL MATCH (d)-[:IN_CLUSTER]->(c:Cluster) | |
| RETURN d.id as id, d.text as text, c.id as cluster_id, d.created_at as created_at | |
| ORDER BY d.created_at DESC | |
| """) | |
| return [(row["id"], row["text"], row["cluster_id"], row["created_at"]) for row in result] | |
| def update_embedding(self, dump_id, embedding): | |
| """Store embedding vector for a dump.""" | |
| # Convert numpy array to list for Neo4j storage | |
| embedding_list = embedding.astype(np.float32).tolist() | |
| with self.driver.session() as session: | |
| session.run(""" | |
| MATCH (d:Dump {id: $dump_id}) | |
| SET d.embedding = $embedding | |
| """, dump_id=dump_id, embedding=embedding_list) | |
| def update_cluster(self, dump_id, cluster_id): | |
| """Assign dump to a cluster.""" | |
| with self.driver.session() as session: | |
| # Remove existing cluster relationship | |
| session.run(""" | |
| MATCH (d:Dump {id: $dump_id})-[r:IN_CLUSTER]->() | |
| DELETE r | |
| """, dump_id=dump_id) | |
| # Create or match cluster node and add relationship | |
| session.run(""" | |
| MATCH (d:Dump {id: $dump_id}) | |
| MERGE (c:Cluster {id: $cluster_id}) | |
| ON CREATE SET c.created_at = datetime() | |
| CREATE (d)-[:IN_CLUSTER]->(c) | |
| """, dump_id=dump_id, cluster_id=int(cluster_id)) | |
| def get_embeddings(self): | |
| """Retrieve all embeddings from database.""" | |
| with self.driver.session() as session: | |
| result = session.run(""" | |
| MATCH (d:Dump) | |
| WHERE d.embedding IS NOT NULL | |
| RETURN d.id as dump_id, d.embedding as embedding | |
| """) | |
| embeddings = [] | |
| for row in result: | |
| dump_id = row["dump_id"] | |
| embedding_list = row["embedding"] | |
| # Convert list back to numpy array | |
| embedding = np.array(embedding_list, dtype=np.float32) | |
| embeddings.append((dump_id, embedding)) | |
| return embeddings | |
| def save_cluster_label(self, cluster_id, label, description=None): | |
| """Save or update a cluster label.""" | |
| with self.driver.session() as session: | |
| session.run(""" | |
| MERGE (c:Cluster {id: $cluster_id}) | |
| ON CREATE SET c.created_at = datetime() | |
| SET c.label = $label, c.description = $description | |
| """, cluster_id=int(cluster_id), label=label, description=description) | |
| def get_cluster_label(self, cluster_id): | |
| """Get label for a specific cluster.""" | |
| with self.driver.session() as session: | |
| result = session.run(""" | |
| MATCH (c:Cluster {id: $cluster_id}) | |
| RETURN c.label as label, c.description as description | |
| """, cluster_id=int(cluster_id)) | |
| row = result.single() | |
| if row: | |
| return (row["label"], row["description"]) | |
| return (None, None) | |
| def get_all_cluster_labels(self): | |
| """Get all cluster labels.""" | |
| with self.driver.session() as session: | |
| result = session.run(""" | |
| MATCH (c:Cluster) | |
| RETURN c.id as cluster_id, c.label as label, c.description as description | |
| """) | |
| return { | |
| row["cluster_id"]: { | |
| "label": row["label"], | |
| "description": row["description"] | |
| } | |
| for row in result | |
| } | |
| def get_cluster_dumps(self, cluster_id): | |
| """Get all dumps in a specific cluster.""" | |
| with self.driver.session() as session: | |
| result = session.run(""" | |
| MATCH (d:Dump)-[:IN_CLUSTER]->(c:Cluster {id: $cluster_id}) | |
| RETURN d.id as dump_id, d.text as text | |
| """, cluster_id=int(cluster_id)) | |
| return [(row["dump_id"], row["text"]) for row in result] | |
| def add_generated_dump(self, text, cluster_id): | |
| """ | |
| Add a generated braindump directly to a specific cluster. | |
| Returns (dump_id, is_duplicate) tuple. | |
| If duplicate exists, returns (existing_dump_id, True) without adding to cluster. | |
| If new dump created, returns (new_dump_id, False). | |
| """ | |
| # Check for duplicate first | |
| existing_id = self.check_duplicate(text) | |
| if existing_id: | |
| return (existing_id, True) | |
| # No duplicate, create new dump and assign to cluster | |
| with self.driver.session() as session: | |
| result = session.run(""" | |
| CREATE (d:Dump { | |
| id: randomUuid(), | |
| text: $text, | |
| created_at: datetime() | |
| }) | |
| WITH d | |
| MATCH (c:Cluster {id: $cluster_id}) | |
| CREATE (d)-[:IN_CLUSTER]->(c) | |
| RETURN d.id as dump_id | |
| """, text=text, cluster_id=int(cluster_id)) | |
| dump_id = result.single()["dump_id"] | |
| return (dump_id, False) | |
| def save_feed_cache(self, dump_id, summary, questions, image_urls=None): | |
| """ | |
| Store cached feed data (summary, questions, and image URLs) for a dump. | |
| Args: | |
| dump_id: ID of the dump | |
| summary: Summary text from FeedAgent | |
| questions: List of reflection questions | |
| image_urls: List of image URLs related to the brain dump | |
| """ | |
| with self.driver.session() as session: | |
| # Store as JSON strings for Neo4j compatibility | |
| questions_json = json.dumps(questions) if isinstance(questions, list) else questions | |
| image_urls_json = json.dumps(image_urls) if image_urls else json.dumps([]) | |
| session.run(""" | |
| MATCH (d:Dump {id: $dump_id}) | |
| SET d.summary = $summary, | |
| d.questions = $questions, | |
| d.image_urls = $image_urls, | |
| d.feed_cache_generated_at = datetime() | |
| """, dump_id=dump_id, summary=summary, questions=questions_json, image_urls=image_urls_json) | |
| def get_feed_cache(self, dump_id): | |
| """ | |
| Retrieve cached feed data for a dump. | |
| Returns: {"summary": str, "questions": list, "image_urls": list} or None if not cached | |
| """ | |
| with self.driver.session() as session: | |
| result = session.run(""" | |
| MATCH (d:Dump {id: $dump_id}) | |
| RETURN d.summary as summary, d.questions as questions, d.image_urls as image_urls | |
| """, dump_id=dump_id) | |
| row = result.single() | |
| if row and row["summary"]: | |
| questions = json.loads(row["questions"]) if row["questions"] else [] | |
| image_urls = json.loads(row["image_urls"]) if row["image_urls"] else [] | |
| return { | |
| "summary": row["summary"], | |
| "questions": questions, | |
| "image_urls": image_urls | |
| } | |
| return None | |
| def close(self): | |
| """Close database connection.""" | |
| self.driver.close() | |
| # ============== 2. EMBEDDING ENGINE ============== | |
| class EmbeddingEngine: | |
| def __init__(self, model_name='all-MiniLM-L6-v2'): | |
| print(f"Loading embedding model: {model_name}") | |
| self.model = SentenceTransformer(model_name) | |
| print("✓ Model loaded (CPU mode)") | |
| def embed(self, texts): | |
| """Fast CPU inference - 5-10ms per text""" | |
| if isinstance(texts, str): | |
| texts = [texts] | |
| embeddings = self.model.encode(texts, show_progress_bar=True) | |
| return embeddings | |
| # ============== 3. CLUSTERING ENGINE ============== | |
| class ClusterEngine: | |
| def __init__(self, min_cluster_size=3): | |
| self.min_cluster_size = min_cluster_size | |
| self.clusterer = None | |
| self.reducer = None | |
| self.llm = None | |
| self.labeling_chain = None | |
| # Initialize LangChain for cluster labeling | |
| self._init_labeling_chain() | |
| def _init_labeling_chain(self): | |
| """Initialize LangChain chain for automatic cluster labeling with Google Gemini""" | |
| try: | |
| api_key = os.environ.get("GOOGLE_API_KEY") | |
| if not api_key: | |
| print("⚠️ GOOGLE_API_KEY not found. Cluster labeling will be disabled.") | |
| print(" Get your API key from: https://aistudio.google.com/app/apikey") | |
| return | |
| # Create primary Gemini LLM | |
| primary_llm = ChatGoogleGenerativeAI( | |
| model="gemini-2.5-flash", | |
| temperature=0.3, | |
| google_api_key=api_key | |
| ) | |
| # Create fallback Gemini LLM (using same model but could use gemini-pro) | |
| fallback_llm = ChatGoogleGenerativeAI( | |
| model="gemini-2.5-flash", | |
| temperature=0.5, | |
| google_api_key=api_key | |
| ) | |
| # LLM with fallback support | |
| self.llm = primary_llm.with_fallbacks([fallback_llm]) | |
| # Create labeling prompt | |
| prompt = ChatPromptTemplate.from_template( | |
| """You are an expert at identifying themes in clusters of related thoughts. | |
| Given these brain dump entries from a cluster: | |
| {cluster_texts} | |
| Task: Generate a short, descriptive label (2-4 words) that captures the unifying theme. | |
| Rules: | |
| - Be specific and insightful | |
| - Use natural language, not generic terms | |
| - Focus on the underlying curiosity or topic | |
| - Be specific if entries are less. | |
| - Be broad if entries are more. | |
| Label:""" | |
| ) | |
| # Create the chain | |
| self.labeling_chain = ( | |
| prompt | |
| | self.llm | |
| | StrOutputParser() | |
| ) | |
| print("✓ LangChain cluster labeling initialized with Google Gemini") | |
| except Exception as e: | |
| print(f"⚠️ Could not initialize cluster labeling: {e}") | |
| self.llm = None | |
| self.labeling_chain = None | |
| def generate_cluster_label(self, cluster_texts): | |
| """Generate a label for a cluster using LangChain""" | |
| if not self.labeling_chain: | |
| return "Cluster" | |
| try: | |
| # Take up to 5 representative texts | |
| sample_texts = cluster_texts[:5] | |
| texts_str = "\n".join([f"- {text}" for text in sample_texts]) | |
| label = self.labeling_chain.invoke({"cluster_texts": texts_str}) | |
| return label.strip() | |
| except Exception as e: | |
| print(f"⚠️ Cluster labeling failed: {e}") | |
| return "Cluster" | |
| def fit_predict(self, embeddings): | |
| """HDBSCAN clustering - works great on CPU""" | |
| print(f"Clustering {len(embeddings)} brain dumps...") | |
| n_samples = len(embeddings) | |
| # Adjust min_cluster_size for small datasets | |
| effective_min_cluster_size = min(self.min_cluster_size, max(2, n_samples // 2)) | |
| # HDBSCAN for semantic clustering | |
| self.clusterer = HDBSCAN( | |
| min_cluster_size=effective_min_cluster_size, | |
| metric='euclidean', | |
| cluster_selection_method='eom', | |
| min_samples=1 # More lenient clustering | |
| ) | |
| clusters = self.clusterer.fit_predict(embeddings) | |
| # UMAP for 2D visualization with better separation parameters | |
| # n_neighbors controls local vs global structure (lower = tighter clusters) | |
| n_neighbors = max(2, min(10, n_samples - 1)) # Reduced from 15 for tighter clusters | |
| # For very small datasets, use simpler initialization | |
| init = 'spectral' if n_samples > 10 else 'random' | |
| self.reducer = umap.UMAP( | |
| n_components=2, | |
| random_state=42, | |
| n_neighbors=n_neighbors, | |
| min_dist=0.3, # Increased from 0.1 for better separation between clusters | |
| spread=1.5, # Controls how clumped embeddings are (higher = more spread) | |
| metric='euclidean', | |
| init=init, | |
| negative_sample_rate=10, # Helps with separation | |
| repulsion_strength=1.2 # Pushes dissimilar points apart | |
| ) | |
| coords_2d = self.reducer.fit_transform(embeddings) | |
| print(f"✓ Found {len(set(clusters)) - (1 if -1 in clusters else 0)} clusters") | |
| return clusters, coords_2d | |
| # ============== 4. VISUALIZATION ============== | |
| def build_graph_edges(embeddings, clusters, max_edges_per_node=5, similarity_threshold=0.7): | |
| """Build edges between semantically similar items (Obsidian knowledge graph style)""" | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| edges = [] | |
| similarities = cosine_similarity(embeddings) | |
| # Add edges between items in same cluster | |
| unique_clusters = set(clusters) | |
| for cluster_id in unique_clusters: | |
| if cluster_id == -1: # Skip noise | |
| continue | |
| cluster_indices = np.where(clusters == cluster_id)[0] | |
| # Connect items within cluster (up to max_edges_per_node each) | |
| for i in cluster_indices: | |
| # Find most similar items in same cluster | |
| cluster_similarities = [ | |
| (j, similarities[i][j]) for j in cluster_indices if i != j | |
| ] | |
| cluster_similarities.sort(key=lambda x: x[1], reverse=True) | |
| for j, sim in cluster_similarities[:max_edges_per_node]: | |
| if i < j: # Avoid duplicates | |
| edges.append((i, j, sim)) | |
| return edges | |
| def create_knowledge_graph(dump_data, coords_2d, clusters, cluster_labels=None, embeddings=None): | |
| """ | |
| Obsidian-style knowledge graph visualization with force-directed layout. | |
| Shows connections between related brain dumps in a network style. | |
| """ | |
| fig = go.Figure() | |
| # Color palette for clusters | |
| cluster_colors = [ | |
| '#FF6B6B', # Red | |
| '#4ECDC4', # Teal | |
| '#45B7D1', # Sky Blue | |
| '#FFA07A', # Light Salmon | |
| '#98D8C8', # Mint | |
| '#F7DC6F', # Yellow | |
| '#BB8FCE', # Purple | |
| '#85C1E2', # Light Blue | |
| '#F8B739', # Orange | |
| '#52BE80', # Green | |
| '#EC7063', # Coral | |
| '#AF7AC5', # Lavender | |
| '#5DADE2', # Ocean Blue | |
| '#48C9B0', # Turquoise | |
| '#F1948A', # Pink | |
| '#85929E', # Gray Blue | |
| '#F39C12', # Dark Orange | |
| '#3498DB', # Bright Blue | |
| '#E74C3C', # Bright Red | |
| '#9B59B6' # Violet | |
| ] | |
| # Color map for clusters | |
| unique_clusters = set(clusters) | |
| colors = {-1: '#95A5A6'} # Gray for noise points | |
| for i, c in enumerate([c for c in unique_clusters if c != -1]): | |
| colors[c] = cluster_colors[i % len(cluster_colors)] | |
| # Build edges between related items | |
| edges = [] | |
| if embeddings is not None: | |
| edges = build_graph_edges(embeddings, clusters) | |
| # Draw edges first (so they appear behind nodes) | |
| for i, j, similarity in edges: | |
| x0, y0 = coords_2d[i] | |
| x1, y1 = coords_2d[j] | |
| # Edge opacity based on similarity | |
| edge_opacity = 0.2 + (similarity - 0.7) * 0.4 # Range: 0.2-0.6 | |
| edge_opacity = max(0.1, min(0.6, edge_opacity)) | |
| fig.add_trace(go.Scatter( | |
| x=[x0, x1, None], | |
| y=[y0, y1, None], | |
| mode='lines', | |
| line=dict( | |
| width=1.5, | |
| color=f'rgba(200, 200, 200, {edge_opacity})', | |
| ), | |
| hoverinfo='none', | |
| showlegend=False, | |
| name='', | |
| )) | |
| # Draw nodes (clusters) | |
| for cluster_id in unique_clusters: | |
| mask = clusters == cluster_id | |
| cluster_coords = coords_2d[mask] | |
| cluster_texts = [dump_data[i][1] for i in range(len(dump_data)) if clusters[i] == cluster_id] | |
| # Get cluster label if available | |
| if cluster_labels and cluster_id in cluster_labels: | |
| label = cluster_labels[cluster_id] | |
| else: | |
| label = f"Cluster {cluster_id}" if cluster_id != -1 else "Unclustered" | |
| # Determine node size (larger for bigger clusters) | |
| node_size = min(24, 14 + len(cluster_texts) // 2) | |
| fig.add_trace(go.Scatter( | |
| x=cluster_coords[:, 0], | |
| y=cluster_coords[:, 1], | |
| mode='markers', | |
| name=label, | |
| marker=dict( | |
| size=node_size, | |
| color=colors[cluster_id], | |
| line=dict(width=2, color='rgba(255, 255, 255, 0.8)'), | |
| opacity=0.95, | |
| symbol='circle', | |
| ), | |
| text=cluster_texts, | |
| hovertext=[f"<b>{text}</b>" for text in cluster_texts], | |
| hoverinfo='text', | |
| showlegend=True, | |
| )) | |
| fig.update_layout( | |
| title={ | |
| 'text': "🧠 Brain Dump Sanctuary - Knowledge Graph", | |
| 'x': 0.5, | |
| 'xanchor': 'center', | |
| 'font': {'size': 20, 'color': 'white'} | |
| }, | |
| showlegend=True, | |
| hovermode='closest', | |
| width=1400, | |
| height=800, | |
| plot_bgcolor='#0d1117', | |
| paper_bgcolor='#0d1117', | |
| font=dict(color='white', family='monospace'), | |
| xaxis=dict( | |
| showgrid=False, | |
| zeroline=False, | |
| showticklabels=False, | |
| showline=False, | |
| ), | |
| yaxis=dict( | |
| showgrid=False, | |
| zeroline=False, | |
| showticklabels=False, | |
| showline=False, | |
| ), | |
| margin=dict(l=0, r=200, t=50, b=0), | |
| legend=dict( | |
| orientation="v", | |
| yanchor="top", | |
| y=0.99, | |
| xanchor="left", | |
| x=1.02, | |
| bgcolor='rgba(13, 17, 23, 0.9)', | |
| bordercolor='#30363d', | |
| borderwidth=1, | |
| font=dict(size=11), | |
| ), | |
| ) | |
| return fig | |
| def create_cluster_graph(dump_data, coords_2d, clusters, cluster_labels=None): | |
| """Interactive Plotly visualization with cluster labels (Legacy - use create_knowledge_graph instead)""" | |
| fig = go.Figure() | |
| # Expanded color palette with 20 distinct colors | |
| cluster_colors = [ | |
| '#FF6B6B', # Red | |
| '#4ECDC4', # Teal | |
| '#45B7D1', # Sky Blue | |
| '#FFA07A', # Light Salmon | |
| '#98D8C8', # Mint | |
| '#F7DC6F', # Yellow | |
| '#BB8FCE', # Purple | |
| '#85C1E2', # Light Blue | |
| '#F8B739', # Orange | |
| '#52BE80', # Green | |
| '#EC7063', # Coral | |
| '#AF7AC5', # Lavender | |
| '#5DADE2', # Ocean Blue | |
| '#48C9B0', # Turquoise | |
| '#F1948A', # Pink | |
| '#85929E', # Gray Blue | |
| '#F39C12', # Dark Orange | |
| '#3498DB', # Bright Blue | |
| '#E74C3C', # Bright Red | |
| '#9B59B6' # Violet | |
| ] | |
| # Color map for clusters | |
| unique_clusters = set(clusters) | |
| colors = {-1: '#95A5A6'} # Gray for noise points | |
| for i, c in enumerate([c for c in unique_clusters if c != -1]): | |
| colors[c] = cluster_colors[i % len(cluster_colors)] | |
| # Plot each cluster | |
| for cluster_id in unique_clusters: | |
| mask = clusters == cluster_id | |
| cluster_coords = coords_2d[mask] | |
| cluster_texts = [dump_data[i][1] for i in range(len(dump_data)) if clusters[i] == cluster_id] | |
| # Get cluster label if available | |
| if cluster_labels and cluster_id in cluster_labels: | |
| label = cluster_labels[cluster_id] | |
| else: | |
| label = f"Cluster {cluster_id}" if cluster_id != -1 else "Unclustered" | |
| fig.add_trace(go.Scatter( | |
| x=cluster_coords[:, 0], | |
| y=cluster_coords[:, 1], | |
| mode='markers+text', | |
| name=label, | |
| marker=dict( | |
| size=14, # Slightly larger for visibility | |
| color=colors[cluster_id], | |
| line=dict(width=2, color='white'), # Thicker white border | |
| opacity=0.9 # Slight transparency to see overlaps | |
| ), | |
| text=[f"{i+1}" for i in range(len(cluster_texts))], | |
| textposition="top center", | |
| textfont=dict(size=10, color='white'), | |
| hovertext=cluster_texts, | |
| hoverinfo='text' | |
| )) | |
| fig.update_layout( | |
| title="Brain Dump Sanctuary - Semantic Clusters", | |
| showlegend=True, | |
| hovermode='closest', | |
| width=1200, # Wider for better separation | |
| height=700, # Taller for better separation | |
| plot_bgcolor='#1a1a1a', # Dark background | |
| paper_bgcolor='#0d1117', | |
| font=dict(color='white'), | |
| xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), | |
| yaxis=dict(showgrid=False, zeroline=False, showticklabels=False), | |
| legend=dict( | |
| orientation="v", | |
| yanchor="top", | |
| y=1, | |
| xanchor="left", | |
| x=1.02, | |
| bgcolor='rgba(13, 17, 23, 0.8)', | |
| bordercolor='#30363d', | |
| borderwidth=1 | |
| ) | |
| ) | |
| return fig | |
| # ============== 5. DEMO PIPELINE ============== | |
| def run_demo(): | |
| """Complete pipeline demo""" | |
| # Sample brain dumps | |
| sample_dumps = [ | |
| # Consciousness & Mind (8 items) | |
| "Why do dreams feel so real but fade so quickly?", | |
| "Is consciousness an emergent property?", | |
| "Why do we forget things we just read?", | |
| "What causes earworms (songs stuck in head)?", | |
| "Why does time feel faster as we age?", | |
| "How does anesthesia actually work?", | |
| "What makes us self-aware?", | |
| "Can machines ever truly be conscious?", | |
| # Physics & Quantum (8 items) | |
| "How does quantum entanglement actually work?", | |
| "Is math discovered or invented?", | |
| "What happens at the event horizon of a black hole?", | |
| "Why is the speed of light constant?", | |
| "What is dark matter made of?", | |
| "How can particles be in two places at once?", | |
| "What came before the Big Bang?", | |
| "Why does time only move forward?", | |
| # AI & Technology (8 items) | |
| "Are LLMs actually understanding or just pattern matching?", | |
| "How do neural networks learn representations?", | |
| "What makes a question 'good' vs 'bad'?", | |
| "Can AI ever be truly creative?", | |
| "How do transformers attend to context?", | |
| "What is the halting problem and why does it matter?", | |
| "Will we ever achieve AGI?", | |
| "How do computers generate random numbers?", | |
| # Biology & Nature (8 items) | |
| "How do birds navigate during migration?", | |
| "What causes the smell of rain on dry ground?", | |
| "What's the connection between gut bacteria and mood?", | |
| "How do whales communicate across oceans?", | |
| "Why do cats purr?", | |
| "How do fireflies synchronize their flashing?", | |
| "Why do we yawn when others yawn?", | |
| "How do octopuses change color instantly?", | |
| # Food & Chemistry (8 items) | |
| "What makes sourdough bread different from regular bread?", | |
| "Why does coffee smell better than it tastes?", | |
| "What makes food spicy and why do we like it?", | |
| "How does fermentation preserve food?", | |
| "Why does chocolate melt at body temperature?", | |
| "What causes that metallic taste when you bite foil?", | |
| "Why do onions make us cry?", | |
| "How do flavors combine to create umami?", | |
| # Music & Art (8 items) | |
| "Why do some songs give me chills?", | |
| "What makes a melody memorable?", | |
| "How does rhythm affect our emotions?", | |
| "Why do major keys sound happy and minor keys sad?", | |
| "What makes abstract art 'good'?", | |
| "How does color theory influence mood?", | |
| "Why do we find symmetry beautiful?", | |
| "What is the golden ratio in design?", | |
| # Psychology & Society (8 items) | |
| "Why do we procrastinate even when we know better?", | |
| "How does confirmation bias shape our beliefs?", | |
| "What causes impostor syndrome?", | |
| "Why are first impressions so lasting?", | |
| "How do echo chambers form online?", | |
| "What makes some ideas go viral?", | |
| "Why is it hard to change someone's mind?", | |
| "How does groupthink override individual judgment?", | |
| # Language & Communication (4 items) | |
| "Why do different languages have different sounds?", | |
| "How did writing systems evolve independently?", | |
| "What makes a joke funny across cultures?", | |
| "Why do babies learn language so easily?" | |
| ] | |
| print("=== BRAIN DUMP SANCTUARY - DEMO ===\n") | |
| # 1. Initialize | |
| db = BrainDumpDB() | |
| embedder = EmbeddingEngine() | |
| clusterer = ClusterEngine(min_cluster_size=2) | |
| # 2. Add dumps to database | |
| print("\n[1/4] Adding brain dumps to database...") | |
| for dump in sample_dumps: | |
| dump_id, is_duplicate = db.add_dump(dump) | |
| if is_duplicate: | |
| print(f" Skipped duplicate: {dump[:50]}...") | |
| # 3. Generate embeddings | |
| print("\n[2/4] Generating embeddings (CPU)...") | |
| dumps = db.get_all_dumps() | |
| texts = [d[1] for d in dumps] | |
| embeddings = embedder.embed(texts) | |
| for i, (dump_id, text, _, created_at) in enumerate(dumps): | |
| db.update_embedding(dump_id, embeddings[i]) | |
| # 4. Cluster | |
| print("\n[3/5] Performing semantic clustering...") | |
| clusters, coords_2d = clusterer.fit_predict(embeddings) | |
| for i, (dump_id, _, _, _) in enumerate(dumps): | |
| db.update_cluster(dump_id, clusters[i]) | |
| # 5. Auto-generate cluster labels using LangChain | |
| print("\n[4/5] Generating cluster labels with LLM...") | |
| cluster_labels = {} | |
| unique_clusters = set(clusters) - {-1} | |
| for cluster_id in unique_clusters: | |
| cluster_dumps = [dumps[i][1] for i in range(len(dumps)) if clusters[i] == cluster_id] | |
| label = clusterer.generate_cluster_label(cluster_dumps) | |
| cluster_labels[cluster_id] = label | |
| db.save_cluster_label(cluster_id, label) | |
| print(f" Cluster {cluster_id}: '{label}'") | |
| # 6. Visualize | |
| print("\n[5/5] Creating knowledge graph visualization...") | |
| fig = create_knowledge_graph(dumps, coords_2d, clusters, cluster_labels, embeddings) | |
| fig.write_html("brain_dump_knowledge_graph.html") | |
| print("✓ Saved to: brain_dump_knowledge_graph.html") | |
| # 7. Show cluster insights | |
| print("\n=== CLUSTER INSIGHTS ===") | |
| for cluster_id in sorted(unique_clusters): | |
| cluster_dumps = [dumps[i][1] for i in range(len(dumps)) if clusters[i] == cluster_id] | |
| label = cluster_labels.get(cluster_id, f"Cluster {cluster_id}") | |
| print(f"\n{label} ({len(cluster_dumps)} items):") | |
| for dump in cluster_dumps[:3]: # Show first 3 | |
| print(f" - {dump}") | |
| if __name__ == "__main__": | |
| run_demo() |