alma-memory 0.5.0__py3-none-any.whl → 0.5.1__py3-none-any.whl

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.
Files changed (36) hide show
  1. alma/__init__.py +33 -1
  2. alma/core.py +124 -16
  3. alma/extraction/auto_learner.py +4 -3
  4. alma/graph/__init__.py +26 -1
  5. alma/graph/backends/__init__.py +14 -0
  6. alma/graph/backends/kuzu.py +624 -0
  7. alma/graph/backends/memgraph.py +432 -0
  8. alma/integration/claude_agents.py +22 -10
  9. alma/learning/protocols.py +3 -3
  10. alma/mcp/tools.py +9 -11
  11. alma/observability/__init__.py +84 -0
  12. alma/observability/config.py +302 -0
  13. alma/observability/logging.py +424 -0
  14. alma/observability/metrics.py +583 -0
  15. alma/observability/tracing.py +440 -0
  16. alma/retrieval/engine.py +65 -4
  17. alma/storage/__init__.py +29 -0
  18. alma/storage/azure_cosmos.py +343 -132
  19. alma/storage/base.py +58 -0
  20. alma/storage/constants.py +103 -0
  21. alma/storage/file_based.py +3 -8
  22. alma/storage/migrations/__init__.py +21 -0
  23. alma/storage/migrations/base.py +321 -0
  24. alma/storage/migrations/runner.py +323 -0
  25. alma/storage/migrations/version_stores.py +337 -0
  26. alma/storage/migrations/versions/__init__.py +11 -0
  27. alma/storage/migrations/versions/v1_0_0.py +373 -0
  28. alma/storage/postgresql.py +185 -78
  29. alma/storage/sqlite_local.py +149 -50
  30. alma/testing/__init__.py +46 -0
  31. alma/testing/factories.py +301 -0
  32. alma/testing/mocks.py +389 -0
  33. {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/METADATA +42 -8
  34. {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/RECORD +36 -19
  35. {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/WHEEL +0 -0
  36. {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,7 @@ from typing import Any, Dict, List, Optional, Tuple
16
16
  import numpy as np
17
17
 
18
18
  from alma.storage.base import StorageBackend
19
+ from alma.storage.constants import SQLITE_TABLE_NAMES, MemoryType
19
20
  from alma.types import (
20
21
  AntiPattern,
21
22
  DomainKnowledge,
@@ -56,6 +57,7 @@ class SQLiteStorage(StorageBackend):
56
57
  self,
57
58
  db_path: Path,
58
59
  embedding_dim: int = 384, # Default for all-MiniLM-L6-v2
60
+ auto_migrate: bool = True,
59
61
  ):
60
62
  """
61
63
  Initialize SQLite storage.
@@ -63,11 +65,16 @@ class SQLiteStorage(StorageBackend):
63
65
  Args:
64
66
  db_path: Path to SQLite database file
65
67
  embedding_dim: Dimension of embedding vectors
68
+ auto_migrate: If True, automatically apply pending migrations on startup
66
69
  """
67
70
  self.db_path = Path(db_path)
68
71
  self.db_path.parent.mkdir(parents=True, exist_ok=True)
69
72
  self.embedding_dim = embedding_dim
70
73
 
74
+ # Migration support (lazy-loaded)
75
+ self._migration_runner = None
76
+ self._version_store = None
77
+
71
78
  # Initialize database
72
79
  self._init_database()
73
80
 
@@ -77,6 +84,10 @@ class SQLiteStorage(StorageBackend):
77
84
  self._index_dirty: Dict[str, bool] = {} # Track which indexes need rebuilding
78
85
  self._load_faiss_indices()
79
86
 
87
+ # Auto-migrate if enabled
88
+ if auto_migrate:
89
+ self._ensure_migrated()
90
+
80
91
  @classmethod
81
92
  def from_config(cls, config: Dict[str, Any]) -> "SQLiteStorage":
82
93
  """Create instance from configuration."""
@@ -236,12 +247,7 @@ class SQLiteStorage(StorageBackend):
236
247
  memory_types: List of memory types to load. If None, loads all types.
237
248
  """
238
249
  if memory_types is None:
239
- memory_types = [
240
- "heuristics",
241
- "outcomes",
242
- "domain_knowledge",
243
- "anti_patterns",
244
- ]
250
+ memory_types = list(MemoryType.VECTOR_ENABLED)
245
251
 
246
252
  for memory_type in memory_types:
247
253
  if FAISS_AVAILABLE:
@@ -401,7 +407,7 @@ class SQLiteStorage(StorageBackend):
401
407
  )
402
408
 
403
409
  # Add embedding to index
404
- self._add_to_index("heuristics", heuristic.id, heuristic.embedding)
410
+ self._add_to_index(MemoryType.HEURISTICS, heuristic.id, heuristic.embedding)
405
411
  logger.debug(f"Saved heuristic: {heuristic.id}")
406
412
  return heuristic.id
407
413
 
@@ -433,7 +439,7 @@ class SQLiteStorage(StorageBackend):
433
439
  )
434
440
 
435
441
  # Add embedding to index
436
- self._add_to_index("outcomes", outcome.id, outcome.embedding)
442
+ self._add_to_index(MemoryType.OUTCOMES, outcome.id, outcome.embedding)
437
443
  logger.debug(f"Saved outcome: {outcome.id}")
438
444
  return outcome.id
439
445
 
@@ -489,7 +495,9 @@ class SQLiteStorage(StorageBackend):
489
495
  )
490
496
 
491
497
  # Add embedding to index
492
- self._add_to_index("domain_knowledge", knowledge.id, knowledge.embedding)
498
+ self._add_to_index(
499
+ MemoryType.DOMAIN_KNOWLEDGE, knowledge.id, knowledge.embedding
500
+ )
493
501
  logger.debug(f"Saved domain knowledge: {knowledge.id}")
494
502
  return knowledge.id
495
503
 
@@ -531,7 +539,9 @@ class SQLiteStorage(StorageBackend):
531
539
  )
532
540
 
533
541
  # Add embedding to index
534
- self._add_to_index("anti_patterns", anti_pattern.id, anti_pattern.embedding)
542
+ self._add_to_index(
543
+ MemoryType.ANTI_PATTERNS, anti_pattern.id, anti_pattern.embedding
544
+ )
535
545
  logger.debug(f"Saved anti-pattern: {anti_pattern.id}")
536
546
  return anti_pattern.id
537
547
 
@@ -571,7 +581,7 @@ class SQLiteStorage(StorageBackend):
571
581
 
572
582
  # Add embeddings to index
573
583
  for h in heuristics:
574
- self._add_to_index("heuristics", h.id, h.embedding)
584
+ self._add_to_index(MemoryType.HEURISTICS, h.id, h.embedding)
575
585
 
576
586
  logger.debug(f"Batch saved {len(heuristics)} heuristics")
577
587
  return [h.id for h in heuristics]
@@ -611,7 +621,7 @@ class SQLiteStorage(StorageBackend):
611
621
 
612
622
  # Add embeddings to index
613
623
  for o in outcomes:
614
- self._add_to_index("outcomes", o.id, o.embedding)
624
+ self._add_to_index(MemoryType.OUTCOMES, o.id, o.embedding)
615
625
 
616
626
  logger.debug(f"Batch saved {len(outcomes)} outcomes")
617
627
  return [o.id for o in outcomes]
@@ -649,7 +659,7 @@ class SQLiteStorage(StorageBackend):
649
659
 
650
660
  # Add embeddings to index
651
661
  for k in knowledge_items:
652
- self._add_to_index("domain_knowledge", k.id, k.embedding)
662
+ self._add_to_index(MemoryType.DOMAIN_KNOWLEDGE, k.id, k.embedding)
653
663
 
654
664
  logger.debug(f"Batch saved {len(knowledge_items)} domain knowledge items")
655
665
  return [k.id for k in knowledge_items]
@@ -668,7 +678,9 @@ class SQLiteStorage(StorageBackend):
668
678
  # If embedding provided, use vector search to get candidate IDs
669
679
  candidate_ids = None
670
680
  if embedding:
671
- search_results = self._search_index("heuristics", embedding, top_k * 2)
681
+ search_results = self._search_index(
682
+ MemoryType.HEURISTICS, embedding, top_k * 2
683
+ )
672
684
  candidate_ids = [id for id, _ in search_results]
673
685
 
674
686
  with self._get_connection() as conn:
@@ -706,7 +718,9 @@ class SQLiteStorage(StorageBackend):
706
718
  """Get outcomes with optional vector search."""
707
719
  candidate_ids = None
708
720
  if embedding:
709
- search_results = self._search_index("outcomes", embedding, top_k * 2)
721
+ search_results = self._search_index(
722
+ MemoryType.OUTCOMES, embedding, top_k * 2
723
+ )
710
724
  candidate_ids = [id for id, _ in search_results]
711
725
 
712
726
  with self._get_connection() as conn:
@@ -772,7 +786,7 @@ class SQLiteStorage(StorageBackend):
772
786
  candidate_ids = None
773
787
  if embedding:
774
788
  search_results = self._search_index(
775
- "domain_knowledge", embedding, top_k * 2
789
+ MemoryType.DOMAIN_KNOWLEDGE, embedding, top_k * 2
776
790
  )
777
791
  candidate_ids = [id for id, _ in search_results]
778
792
 
@@ -813,7 +827,9 @@ class SQLiteStorage(StorageBackend):
813
827
  """Get anti-patterns with optional vector search."""
814
828
  candidate_ids = None
815
829
  if embedding:
816
- search_results = self._search_index("anti_patterns", embedding, top_k * 2)
830
+ search_results = self._search_index(
831
+ MemoryType.ANTI_PATTERNS, embedding, top_k * 2
832
+ )
817
833
  candidate_ids = [id for id, _ in search_results]
818
834
 
819
835
  with self._get_connection() as conn:
@@ -856,7 +872,7 @@ class SQLiteStorage(StorageBackend):
856
872
  candidate_ids = None
857
873
  if embedding:
858
874
  search_results = self._search_index(
859
- "heuristics", embedding, top_k * 2 * len(agents)
875
+ MemoryType.HEURISTICS, embedding, top_k * 2 * len(agents)
860
876
  )
861
877
  candidate_ids = [id for id, _ in search_results]
862
878
 
@@ -896,7 +912,7 @@ class SQLiteStorage(StorageBackend):
896
912
  candidate_ids = None
897
913
  if embedding:
898
914
  search_results = self._search_index(
899
- "outcomes", embedding, top_k * 2 * len(agents)
915
+ MemoryType.OUTCOMES, embedding, top_k * 2 * len(agents)
900
916
  )
901
917
  candidate_ids = [id for id, _ in search_results]
902
918
 
@@ -942,7 +958,7 @@ class SQLiteStorage(StorageBackend):
942
958
  candidate_ids = None
943
959
  if embedding:
944
960
  search_results = self._search_index(
945
- "domain_knowledge", embedding, top_k * 2 * len(agents)
961
+ MemoryType.DOMAIN_KNOWLEDGE, embedding, top_k * 2 * len(agents)
946
962
  )
947
963
  candidate_ids = [id for id, _ in search_results]
948
964
 
@@ -984,7 +1000,7 @@ class SQLiteStorage(StorageBackend):
984
1000
  candidate_ids = None
985
1001
  if embedding:
986
1002
  search_results = self._search_index(
987
- "anti_patterns", embedding, top_k * 2 * len(agents)
1003
+ MemoryType.ANTI_PATTERNS, embedding, top_k * 2 * len(agents)
988
1004
  )
989
1005
  candidate_ids = [id for id, _ in search_results]
990
1006
 
@@ -1138,19 +1154,22 @@ class SQLiteStorage(StorageBackend):
1138
1154
  with self._get_connection() as conn:
1139
1155
  cursor = conn.cursor()
1140
1156
 
1141
- tables = ["heuristics", "outcomes", "domain_knowledge", "anti_patterns"]
1142
- for table in tables:
1143
- query = f"SELECT COUNT(*) FROM {table} WHERE project_id = ?"
1144
- params: List[Any] = [project_id]
1145
- if agent:
1146
- query += " AND agent = ?"
1147
- params.append(agent)
1148
- cursor.execute(query, params)
1149
- stats[f"{table}_count"] = cursor.fetchone()[0]
1150
-
1151
- # Preferences don't have project_id
1152
- cursor.execute("SELECT COUNT(*) FROM preferences")
1153
- stats["preferences_count"] = cursor.fetchone()[0]
1157
+ # Use canonical memory types for stats
1158
+ for memory_type in MemoryType.ALL:
1159
+ if memory_type == MemoryType.PREFERENCES:
1160
+ # Preferences don't have project_id
1161
+ cursor.execute(
1162
+ f"SELECT COUNT(*) FROM {SQLITE_TABLE_NAMES[memory_type]}"
1163
+ )
1164
+ stats[f"{memory_type}_count"] = cursor.fetchone()[0]
1165
+ else:
1166
+ query = f"SELECT COUNT(*) FROM {SQLITE_TABLE_NAMES[memory_type]} WHERE project_id = ?"
1167
+ params: List[Any] = [project_id]
1168
+ if agent:
1169
+ query += " AND agent = ?"
1170
+ params.append(agent)
1171
+ cursor.execute(query, params)
1172
+ stats[f"{memory_type}_count"] = cursor.fetchone()[0]
1154
1173
 
1155
1174
  # Embedding counts
1156
1175
  cursor.execute("SELECT COUNT(*) FROM embeddings")
@@ -1290,16 +1309,16 @@ class SQLiteStorage(StorageBackend):
1290
1309
  with self._get_connection() as conn:
1291
1310
  # Also remove from embedding index
1292
1311
  conn.execute(
1293
- "DELETE FROM embeddings WHERE memory_type = 'heuristics' AND memory_id = ?",
1294
- (heuristic_id,),
1312
+ "DELETE FROM embeddings WHERE memory_type = ? AND memory_id = ?",
1313
+ (MemoryType.HEURISTICS, heuristic_id),
1295
1314
  )
1296
1315
  cursor = conn.execute(
1297
- "DELETE FROM heuristics WHERE id = ?",
1316
+ f"DELETE FROM {SQLITE_TABLE_NAMES[MemoryType.HEURISTICS]} WHERE id = ?",
1298
1317
  (heuristic_id,),
1299
1318
  )
1300
1319
  if cursor.rowcount > 0:
1301
1320
  # Mark index as dirty for lazy rebuild on next search
1302
- self._index_dirty["heuristics"] = True
1321
+ self._index_dirty[MemoryType.HEURISTICS] = True
1303
1322
  return True
1304
1323
  return False
1305
1324
 
@@ -1308,16 +1327,16 @@ class SQLiteStorage(StorageBackend):
1308
1327
  with self._get_connection() as conn:
1309
1328
  # Also remove from embedding index
1310
1329
  conn.execute(
1311
- "DELETE FROM embeddings WHERE memory_type = 'outcomes' AND memory_id = ?",
1312
- (outcome_id,),
1330
+ "DELETE FROM embeddings WHERE memory_type = ? AND memory_id = ?",
1331
+ (MemoryType.OUTCOMES, outcome_id),
1313
1332
  )
1314
1333
  cursor = conn.execute(
1315
- "DELETE FROM outcomes WHERE id = ?",
1334
+ f"DELETE FROM {SQLITE_TABLE_NAMES[MemoryType.OUTCOMES]} WHERE id = ?",
1316
1335
  (outcome_id,),
1317
1336
  )
1318
1337
  if cursor.rowcount > 0:
1319
1338
  # Mark index as dirty for lazy rebuild on next search
1320
- self._index_dirty["outcomes"] = True
1339
+ self._index_dirty[MemoryType.OUTCOMES] = True
1321
1340
  return True
1322
1341
  return False
1323
1342
 
@@ -1326,16 +1345,16 @@ class SQLiteStorage(StorageBackend):
1326
1345
  with self._get_connection() as conn:
1327
1346
  # Also remove from embedding index
1328
1347
  conn.execute(
1329
- "DELETE FROM embeddings WHERE memory_type = 'domain_knowledge' AND memory_id = ?",
1330
- (knowledge_id,),
1348
+ "DELETE FROM embeddings WHERE memory_type = ? AND memory_id = ?",
1349
+ (MemoryType.DOMAIN_KNOWLEDGE, knowledge_id),
1331
1350
  )
1332
1351
  cursor = conn.execute(
1333
- "DELETE FROM domain_knowledge WHERE id = ?",
1352
+ f"DELETE FROM {SQLITE_TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]} WHERE id = ?",
1334
1353
  (knowledge_id,),
1335
1354
  )
1336
1355
  if cursor.rowcount > 0:
1337
1356
  # Mark index as dirty for lazy rebuild on next search
1338
- self._index_dirty["domain_knowledge"] = True
1357
+ self._index_dirty[MemoryType.DOMAIN_KNOWLEDGE] = True
1339
1358
  return True
1340
1359
  return False
1341
1360
 
@@ -1344,15 +1363,95 @@ class SQLiteStorage(StorageBackend):
1344
1363
  with self._get_connection() as conn:
1345
1364
  # Also remove from embedding index
1346
1365
  conn.execute(
1347
- "DELETE FROM embeddings WHERE memory_type = 'anti_patterns' AND memory_id = ?",
1348
- (anti_pattern_id,),
1366
+ "DELETE FROM embeddings WHERE memory_type = ? AND memory_id = ?",
1367
+ (MemoryType.ANTI_PATTERNS, anti_pattern_id),
1349
1368
  )
1350
1369
  cursor = conn.execute(
1351
- "DELETE FROM anti_patterns WHERE id = ?",
1370
+ f"DELETE FROM {SQLITE_TABLE_NAMES[MemoryType.ANTI_PATTERNS]} WHERE id = ?",
1352
1371
  (anti_pattern_id,),
1353
1372
  )
1354
1373
  if cursor.rowcount > 0:
1355
1374
  # Mark index as dirty for lazy rebuild on next search
1356
- self._index_dirty["anti_patterns"] = True
1375
+ self._index_dirty[MemoryType.ANTI_PATTERNS] = True
1357
1376
  return True
1358
1377
  return False
1378
+
1379
+ # ==================== MIGRATION SUPPORT ====================
1380
+
1381
+ def _get_version_store(self):
1382
+ """Get or create the version store."""
1383
+ if self._version_store is None:
1384
+ from alma.storage.migrations.version_stores import SQLiteVersionStore
1385
+
1386
+ self._version_store = SQLiteVersionStore(self.db_path)
1387
+ return self._version_store
1388
+
1389
+ def _get_migration_runner(self):
1390
+ """Get or create the migration runner."""
1391
+ if self._migration_runner is None:
1392
+ from alma.storage.migrations.runner import MigrationRunner
1393
+ from alma.storage.migrations.versions import v1_0_0 # noqa: F401
1394
+
1395
+ self._migration_runner = MigrationRunner(
1396
+ version_store=self._get_version_store(),
1397
+ backend="sqlite",
1398
+ )
1399
+ return self._migration_runner
1400
+
1401
+ def _ensure_migrated(self) -> None:
1402
+ """Ensure database is migrated to latest version."""
1403
+ runner = self._get_migration_runner()
1404
+ if runner.needs_migration():
1405
+ with self._get_connection() as conn:
1406
+ applied = runner.migrate(conn)
1407
+ if applied:
1408
+ logger.info(f"Applied {len(applied)} migrations: {applied}")
1409
+
1410
+ def get_schema_version(self) -> Optional[str]:
1411
+ """Get the current schema version."""
1412
+ return self._get_version_store().get_current_version()
1413
+
1414
+ def get_migration_status(self) -> Dict[str, Any]:
1415
+ """Get migration status information."""
1416
+ runner = self._get_migration_runner()
1417
+ status = runner.get_status()
1418
+ status["migration_supported"] = True
1419
+ return status
1420
+
1421
+ def migrate(
1422
+ self,
1423
+ target_version: Optional[str] = None,
1424
+ dry_run: bool = False,
1425
+ ) -> List[str]:
1426
+ """
1427
+ Apply pending schema migrations.
1428
+
1429
+ Args:
1430
+ target_version: Optional target version (applies all if not specified)
1431
+ dry_run: If True, show what would be done without making changes
1432
+
1433
+ Returns:
1434
+ List of applied migration versions
1435
+ """
1436
+ runner = self._get_migration_runner()
1437
+ with self._get_connection() as conn:
1438
+ return runner.migrate(conn, target_version=target_version, dry_run=dry_run)
1439
+
1440
+ def rollback(
1441
+ self,
1442
+ target_version: str,
1443
+ dry_run: bool = False,
1444
+ ) -> List[str]:
1445
+ """
1446
+ Roll back schema to a previous version.
1447
+
1448
+ Args:
1449
+ target_version: Version to roll back to
1450
+ dry_run: If True, show what would be done without making changes
1451
+
1452
+ Returns:
1453
+ List of rolled back migration versions
1454
+ """
1455
+ runner = self._get_migration_runner()
1456
+ with self._get_connection() as conn:
1457
+ return runner.rollback(conn, target_version=target_version, dry_run=dry_run)
@@ -0,0 +1,46 @@
1
+ """
2
+ ALMA Testing Module.
3
+
4
+ Provides reusable test utilities for ALMA integrations:
5
+
6
+ - MockStorage: In-memory storage backend for isolated testing
7
+ - MockEmbedder: Deterministic fake embedding provider
8
+ - Factory functions: Create test data with sensible defaults
9
+
10
+ Example usage:
11
+ >>> from alma.testing import MockStorage, create_test_heuristic
12
+ >>>
13
+ >>> def test_my_integration():
14
+ ... storage = MockStorage()
15
+ ... heuristic = create_test_heuristic(agent="test-agent")
16
+ ... storage.save_heuristic(heuristic)
17
+ ... found = storage.get_heuristics("test-project", agent="test-agent")
18
+ ... assert len(found) == 1
19
+
20
+ The module is designed for:
21
+ - Unit testing ALMA integrations
22
+ - Testing agent hooks without real storage
23
+ - Creating test fixtures with minimal boilerplate
24
+ - Isolated testing without external dependencies
25
+ """
26
+
27
+ from alma.testing.factories import (
28
+ create_test_anti_pattern,
29
+ create_test_heuristic,
30
+ create_test_knowledge,
31
+ create_test_outcome,
32
+ create_test_preference,
33
+ )
34
+ from alma.testing.mocks import MockEmbedder, MockStorage
35
+
36
+ __all__ = [
37
+ # Mocks
38
+ "MockStorage",
39
+ "MockEmbedder",
40
+ # Factories
41
+ "create_test_heuristic",
42
+ "create_test_outcome",
43
+ "create_test_preference",
44
+ "create_test_knowledge",
45
+ "create_test_anti_pattern",
46
+ ]