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.
- alma/__init__.py +33 -1
- alma/core.py +124 -16
- alma/extraction/auto_learner.py +4 -3
- alma/graph/__init__.py +26 -1
- alma/graph/backends/__init__.py +14 -0
- alma/graph/backends/kuzu.py +624 -0
- alma/graph/backends/memgraph.py +432 -0
- alma/integration/claude_agents.py +22 -10
- alma/learning/protocols.py +3 -3
- alma/mcp/tools.py +9 -11
- alma/observability/__init__.py +84 -0
- alma/observability/config.py +302 -0
- alma/observability/logging.py +424 -0
- alma/observability/metrics.py +583 -0
- alma/observability/tracing.py +440 -0
- alma/retrieval/engine.py +65 -4
- alma/storage/__init__.py +29 -0
- alma/storage/azure_cosmos.py +343 -132
- alma/storage/base.py +58 -0
- alma/storage/constants.py +103 -0
- alma/storage/file_based.py +3 -8
- alma/storage/migrations/__init__.py +21 -0
- alma/storage/migrations/base.py +321 -0
- alma/storage/migrations/runner.py +323 -0
- alma/storage/migrations/version_stores.py +337 -0
- alma/storage/migrations/versions/__init__.py +11 -0
- alma/storage/migrations/versions/v1_0_0.py +373 -0
- alma/storage/postgresql.py +185 -78
- alma/storage/sqlite_local.py +149 -50
- alma/testing/__init__.py +46 -0
- alma/testing/factories.py +301 -0
- alma/testing/mocks.py +389 -0
- {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/METADATA +42 -8
- {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/RECORD +36 -19
- {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/WHEEL +0 -0
- {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/top_level.txt +0 -0
alma/storage/sqlite_local.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1142
|
-
for
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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 =
|
|
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
|
|
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[
|
|
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 =
|
|
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
|
|
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[
|
|
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 =
|
|
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
|
|
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[
|
|
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 =
|
|
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
|
|
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[
|
|
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)
|
alma/testing/__init__.py
ADDED
|
@@ -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
|
+
]
|