superlocalmemory 2.3.6 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -239,9 +239,21 @@ class ClusterBuilder:
239
239
  """Initialize cluster builder."""
240
240
  self.db_path = db_path
241
241
 
242
+ def _get_active_profile(self) -> str:
243
+ """Get the currently active profile name from config."""
244
+ config_file = MEMORY_DIR / "profiles.json"
245
+ if config_file.exists():
246
+ try:
247
+ with open(config_file, 'r') as f:
248
+ config = json.load(f)
249
+ return config.get('active_profile', 'default')
250
+ except (json.JSONDecodeError, IOError):
251
+ pass
252
+ return 'default'
253
+
242
254
  def detect_communities(self) -> int:
243
255
  """
244
- Run Leiden algorithm to find memory clusters.
256
+ Run Leiden algorithm to find memory clusters (active profile only).
245
257
 
246
258
  Returns:
247
259
  Number of clusters created
@@ -255,13 +267,16 @@ class ClusterBuilder:
255
267
 
256
268
  conn = sqlite3.connect(self.db_path)
257
269
  cursor = conn.cursor()
270
+ active_profile = self._get_active_profile()
258
271
 
259
272
  try:
260
- # Load all edges
273
+ # Load edges for active profile's memories only
261
274
  edges = cursor.execute('''
262
- SELECT source_memory_id, target_memory_id, weight
263
- FROM graph_edges
264
- ''').fetchall()
275
+ SELECT ge.source_memory_id, ge.target_memory_id, ge.weight
276
+ FROM graph_edges ge
277
+ WHERE ge.source_memory_id IN (SELECT id FROM memories WHERE profile = ?)
278
+ AND ge.target_memory_id IN (SELECT id FROM memories WHERE profile = ?)
279
+ ''', (active_profile, active_profile)).fetchall()
265
280
 
266
281
  if not edges:
267
282
  logger.warning("No edges found - cannot build clusters")
@@ -418,11 +433,36 @@ class GraphEngine:
418
433
  self.cluster_builder = ClusterBuilder(db_path)
419
434
  self._ensure_graph_tables()
420
435
 
436
+ def _get_active_profile(self) -> str:
437
+ """Get the currently active profile name from config."""
438
+ config_file = MEMORY_DIR / "profiles.json"
439
+ if config_file.exists():
440
+ try:
441
+ with open(config_file, 'r') as f:
442
+ config = json.load(f)
443
+ return config.get('active_profile', 'default')
444
+ except (json.JSONDecodeError, IOError):
445
+ pass
446
+ return 'default'
447
+
421
448
  def _ensure_graph_tables(self):
422
- """Create graph tables if they don't exist."""
449
+ """Create graph tables if they don't exist, or recreate if schema is incomplete."""
423
450
  conn = sqlite3.connect(self.db_path)
424
451
  cursor = conn.cursor()
425
452
 
453
+ # Check if existing tables have correct schema (not just id column)
454
+ for table_name, required_cols in [
455
+ ('graph_nodes', {'memory_id', 'entities'}),
456
+ ('graph_edges', {'source_memory_id', 'target_memory_id', 'weight'}),
457
+ ('graph_clusters', {'name', 'member_count'}),
458
+ ]:
459
+ cursor.execute(f"PRAGMA table_info({table_name})")
460
+ existing_cols = {row[1] for row in cursor.fetchall()}
461
+ if existing_cols and not required_cols.issubset(existing_cols):
462
+ # Table exists but has incomplete schema — drop and recreate
463
+ logger.warning(f"Dropping incomplete {table_name} table (missing: {required_cols - existing_cols})")
464
+ cursor.execute(f'DROP TABLE IF EXISTS {table_name}')
465
+
426
466
  # Graph nodes table
427
467
  cursor.execute('''
428
468
  CREATE TABLE IF NOT EXISTS graph_nodes (
@@ -516,11 +556,14 @@ class GraphEngine:
516
556
  'fix': "Run 'superlocalmemoryv2:status' first to initialize the database, or add some memories."
517
557
  }
518
558
 
519
- # Load all memories
559
+ # Load memories for active profile only
560
+ active_profile = self._get_active_profile()
561
+ logger.info(f"Building graph for profile: {active_profile}")
520
562
  memories = cursor.execute('''
521
563
  SELECT id, content, summary FROM memories
564
+ WHERE profile = ?
522
565
  ORDER BY id
523
- ''').fetchall()
566
+ ''', (active_profile,)).fetchall()
524
567
 
525
568
  if len(memories) == 0:
526
569
  logger.warning("No memories found")
@@ -553,11 +596,29 @@ class GraphEngine:
553
596
  'fix': "Use incremental updates or reduce memory count with compression."
554
597
  }
555
598
 
556
- # Clear existing graph data
557
- cursor.execute('DELETE FROM graph_edges')
558
- cursor.execute('DELETE FROM graph_nodes')
559
- cursor.execute('DELETE FROM graph_clusters')
560
- cursor.execute('UPDATE memories SET cluster_id = NULL')
599
+ # Clear existing graph data for this profile's memories
600
+ profile_memory_ids = [m[0] for m in memories]
601
+ if profile_memory_ids:
602
+ placeholders = ','.join('?' * len(profile_memory_ids))
603
+ cursor.execute(f'''
604
+ DELETE FROM graph_edges
605
+ WHERE source_memory_id IN ({placeholders})
606
+ OR target_memory_id IN ({placeholders})
607
+ ''', profile_memory_ids + profile_memory_ids)
608
+ cursor.execute(f'''
609
+ DELETE FROM graph_nodes
610
+ WHERE memory_id IN ({placeholders})
611
+ ''', profile_memory_ids)
612
+ # Remove orphaned clusters (no remaining members)
613
+ cursor.execute('''
614
+ DELETE FROM graph_clusters
615
+ WHERE id NOT IN (
616
+ SELECT DISTINCT cluster_id FROM memories
617
+ WHERE cluster_id IS NOT NULL
618
+ )
619
+ ''')
620
+ cursor.execute('UPDATE memories SET cluster_id = NULL WHERE profile = ?',
621
+ (active_profile,))
561
622
  conn.commit()
562
623
 
563
624
  logger.info(f"Processing {len(memories)} memories")
@@ -646,7 +707,7 @@ class GraphEngine:
646
707
 
647
708
  def get_related(self, memory_id: int, max_hops: int = 2) -> List[Dict]:
648
709
  """
649
- Get memories connected to this memory via graph edges.
710
+ Get memories connected to this memory via graph edges (active profile only).
650
711
 
651
712
  Args:
652
713
  memory_id: Source memory ID
@@ -657,18 +718,21 @@ class GraphEngine:
657
718
  """
658
719
  conn = sqlite3.connect(self.db_path)
659
720
  cursor = conn.cursor()
721
+ active_profile = self._get_active_profile()
660
722
 
661
723
  try:
662
- # Get 1-hop neighbors
724
+ # Get 1-hop neighbors (filtered to active profile)
663
725
  edges = cursor.execute('''
664
- SELECT target_memory_id, relationship_type, weight, shared_entities
665
- FROM graph_edges
666
- WHERE source_memory_id = ?
726
+ SELECT ge.target_memory_id, ge.relationship_type, ge.weight, ge.shared_entities
727
+ FROM graph_edges ge
728
+ JOIN memories m ON ge.target_memory_id = m.id
729
+ WHERE ge.source_memory_id = ? AND m.profile = ?
667
730
  UNION
668
- SELECT source_memory_id, relationship_type, weight, shared_entities
669
- FROM graph_edges
670
- WHERE target_memory_id = ?
671
- ''', (memory_id, memory_id)).fetchall()
731
+ SELECT ge.source_memory_id, ge.relationship_type, ge.weight, ge.shared_entities
732
+ FROM graph_edges ge
733
+ JOIN memories m ON ge.source_memory_id = m.id
734
+ WHERE ge.target_memory_id = ? AND m.profile = ?
735
+ ''', (memory_id, active_profile, memory_id, active_profile)).fetchall()
672
736
 
673
737
  results = []
674
738
  seen_ids = {memory_id}
@@ -743,7 +807,7 @@ class GraphEngine:
743
807
 
744
808
  def get_cluster_members(self, cluster_id: int) -> List[Dict]:
745
809
  """
746
- Get all memories in a cluster.
810
+ Get all memories in a cluster (filtered by active profile).
747
811
 
748
812
  Args:
749
813
  cluster_id: Cluster ID
@@ -753,14 +817,15 @@ class GraphEngine:
753
817
  """
754
818
  conn = sqlite3.connect(self.db_path)
755
819
  cursor = conn.cursor()
820
+ active_profile = self._get_active_profile()
756
821
 
757
822
  try:
758
823
  memories = cursor.execute('''
759
824
  SELECT id, summary, importance, tags, created_at
760
825
  FROM memories
761
- WHERE cluster_id = ?
826
+ WHERE cluster_id = ? AND profile = ?
762
827
  ORDER BY importance DESC
763
- ''', (cluster_id,)).fetchall()
828
+ ''', (cluster_id, active_profile)).fetchall()
764
829
 
765
830
  return [
766
831
  {
@@ -814,12 +879,14 @@ class GraphEngine:
814
879
  VALUES (?, ?, ?)
815
880
  ''', (memory_id, json.dumps(new_entities), json.dumps(new_vector.tolist())))
816
881
 
817
- # Compare to existing memories
882
+ # Compare to existing memories in the same profile
883
+ active_profile = self._get_active_profile()
818
884
  existing = cursor.execute('''
819
- SELECT memory_id, embedding_vector, entities
820
- FROM graph_nodes
821
- WHERE memory_id != ?
822
- ''', (memory_id,)).fetchall()
885
+ SELECT gn.memory_id, gn.embedding_vector, gn.entities
886
+ FROM graph_nodes gn
887
+ JOIN memories m ON gn.memory_id = m.id
888
+ WHERE gn.memory_id != ? AND m.profile = ?
889
+ ''', (memory_id, active_profile)).fetchall()
823
890
 
824
891
  edges_added = 0
825
892
 
@@ -871,24 +938,44 @@ class GraphEngine:
871
938
  conn.close()
872
939
 
873
940
  def get_stats(self) -> Dict[str, any]:
874
- """Get graph statistics."""
941
+ """Get graph statistics for the active profile."""
875
942
  conn = sqlite3.connect(self.db_path)
876
943
  cursor = conn.cursor()
944
+ active_profile = self._get_active_profile()
877
945
 
878
946
  try:
879
- nodes = cursor.execute('SELECT COUNT(*) FROM graph_nodes').fetchone()[0]
880
- edges = cursor.execute('SELECT COUNT(*) FROM graph_edges').fetchone()[0]
881
- clusters = cursor.execute('SELECT COUNT(*) FROM graph_clusters').fetchone()[0]
947
+ # Count nodes for active profile's memories
948
+ nodes = cursor.execute('''
949
+ SELECT COUNT(*) FROM graph_nodes
950
+ WHERE memory_id IN (SELECT id FROM memories WHERE profile = ?)
951
+ ''', (active_profile,)).fetchone()[0]
952
+
953
+ # Count edges where at least one end is in active profile
954
+ edges = cursor.execute('''
955
+ SELECT COUNT(*) FROM graph_edges
956
+ WHERE source_memory_id IN (SELECT id FROM memories WHERE profile = ?)
957
+ ''', (active_profile,)).fetchone()[0]
958
+
959
+ # Clusters that have members in active profile
960
+ clusters = cursor.execute('''
961
+ SELECT COUNT(DISTINCT cluster_id) FROM memories
962
+ WHERE cluster_id IS NOT NULL AND profile = ?
963
+ ''', (active_profile,)).fetchone()[0]
882
964
 
883
- # Cluster breakdown
965
+ # Cluster breakdown for active profile
884
966
  cluster_info = cursor.execute('''
885
- SELECT name, member_count, avg_importance
886
- FROM graph_clusters
887
- ORDER BY member_count DESC
967
+ SELECT gc.name, gc.member_count, gc.avg_importance
968
+ FROM graph_clusters gc
969
+ WHERE gc.id IN (
970
+ SELECT DISTINCT cluster_id FROM memories
971
+ WHERE cluster_id IS NOT NULL AND profile = ?
972
+ )
973
+ ORDER BY gc.member_count DESC
888
974
  LIMIT 10
889
- ''').fetchall()
975
+ ''', (active_profile,)).fetchall()
890
976
 
891
977
  return {
978
+ 'profile': active_profile,
892
979
  'nodes': nodes,
893
980
  'edges': edges,
894
981
  'clusters': clusters,