superlocalmemory 2.3.7 → 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.
- package/CHANGELOG.md +41 -0
- package/README.md +25 -0
- package/hooks/memory-profile-skill.js +7 -18
- package/mcp_server.py +74 -12
- package/package.json +1 -1
- package/src/__pycache__/auto_backup.cpython-312.pyc +0 -0
- package/src/__pycache__/memory_store_v2.cpython-312.pyc +0 -0
- package/src/__pycache__/pattern_learner.cpython-312.pyc +0 -0
- package/src/auto_backup.py +424 -0
- package/src/graph_engine.py +126 -39
- package/src/memory-profiles.py +321 -243
- package/src/memory_store_v2.py +82 -31
- package/src/pattern_learner.py +126 -44
- package/ui/app.js +526 -17
- package/ui/index.html +182 -1
- package/ui_server.py +340 -43
package/src/graph_engine.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
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
|
-
|
|
671
|
-
|
|
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
|
-
|
|
822
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
-
|
|
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,
|