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
@@ -27,6 +27,7 @@ except ImportError:
27
27
  NUMPY_AVAILABLE = False
28
28
 
29
29
  from alma.storage.base import StorageBackend
30
+ from alma.storage.constants import POSTGRESQL_TABLE_NAMES, MemoryType
30
31
  from alma.types import (
31
32
  AntiPattern,
32
33
  DomainKnowledge,
@@ -57,7 +58,7 @@ class PostgreSQLStorage(StorageBackend):
57
58
  Uses native PostgreSQL vector operations for efficient similarity search.
58
59
  Falls back to application-level cosine similarity if pgvector is not installed.
59
60
 
60
- Database schema:
61
+ Database schema (uses canonical memory type names with alma_ prefix):
61
62
  - alma_heuristics: id, agent, project_id, condition, strategy, ...
62
63
  - alma_outcomes: id, agent, project_id, task_type, ...
63
64
  - alma_preferences: id, user_id, category, preference, ...
@@ -67,8 +68,14 @@ class PostgreSQLStorage(StorageBackend):
67
68
  Vector search:
68
69
  - Uses pgvector extension if available
69
70
  - Embeddings stored as VECTOR type with cosine distance operator (<=>)
71
+
72
+ Table names are derived from alma.storage.constants.POSTGRESQL_TABLE_NAMES
73
+ for consistency across all storage backends.
70
74
  """
71
75
 
76
+ # Table names from constants for consistent naming
77
+ TABLE_NAMES = POSTGRESQL_TABLE_NAMES
78
+
72
79
  def __init__(
73
80
  self,
74
81
  host: str,
@@ -80,6 +87,7 @@ class PostgreSQLStorage(StorageBackend):
80
87
  pool_size: int = 10,
81
88
  schema: str = "public",
82
89
  ssl_mode: str = "prefer",
90
+ auto_migrate: bool = True,
83
91
  ):
84
92
  """
85
93
  Initialize PostgreSQL storage.
@@ -94,6 +102,7 @@ class PostgreSQLStorage(StorageBackend):
94
102
  pool_size: Connection pool size
95
103
  schema: Database schema (default: public)
96
104
  ssl_mode: SSL mode (disable, allow, prefer, require, verify-ca, verify-full)
105
+ auto_migrate: If True, automatically apply pending migrations on startup
97
106
  """
98
107
  if not PSYCOPG_AVAILABLE:
99
108
  raise ImportError(
@@ -104,6 +113,10 @@ class PostgreSQLStorage(StorageBackend):
104
113
  self.schema = schema
105
114
  self._pgvector_available = False
106
115
 
116
+ # Migration support (lazy-loaded)
117
+ self._migration_runner = None
118
+ self._version_store = None
119
+
107
120
  # Build connection string
108
121
  conninfo = (
109
122
  f"host={host} port={port} dbname={database} "
@@ -121,6 +134,10 @@ class PostgreSQLStorage(StorageBackend):
121
134
  # Initialize database
122
135
  self._init_database()
123
136
 
137
+ # Auto-migrate if enabled
138
+ if auto_migrate:
139
+ self._ensure_migrated()
140
+
124
141
  @classmethod
125
142
  def from_config(cls, config: Dict[str, Any]) -> "PostgreSQLStorage":
126
143
  """Create instance from configuration."""
@@ -176,8 +193,9 @@ class PostgreSQLStorage(StorageBackend):
176
193
  )
177
194
 
178
195
  # Heuristics table
196
+ heuristics_table = self.TABLE_NAMES[MemoryType.HEURISTICS]
179
197
  conn.execute(f"""
180
- CREATE TABLE IF NOT EXISTS {self.schema}.alma_heuristics (
198
+ CREATE TABLE IF NOT EXISTS {self.schema}.{heuristics_table} (
181
199
  id TEXT PRIMARY KEY,
182
200
  agent TEXT NOT NULL,
183
201
  project_id TEXT NOT NULL,
@@ -194,12 +212,18 @@ class PostgreSQLStorage(StorageBackend):
194
212
  """)
195
213
  conn.execute(f"""
196
214
  CREATE INDEX IF NOT EXISTS idx_heuristics_project_agent
197
- ON {self.schema}.alma_heuristics(project_id, agent)
215
+ ON {self.schema}.{heuristics_table}(project_id, agent)
216
+ """)
217
+ # Confidence index for efficient filtering by confidence score
218
+ conn.execute(f"""
219
+ CREATE INDEX IF NOT EXISTS idx_heuristics_confidence
220
+ ON {self.schema}.{heuristics_table}(project_id, confidence DESC)
198
221
  """)
199
222
 
200
223
  # Outcomes table
224
+ outcomes_table = self.TABLE_NAMES[MemoryType.OUTCOMES]
201
225
  conn.execute(f"""
202
- CREATE TABLE IF NOT EXISTS {self.schema}.alma_outcomes (
226
+ CREATE TABLE IF NOT EXISTS {self.schema}.{outcomes_table} (
203
227
  id TEXT PRIMARY KEY,
204
228
  agent TEXT NOT NULL,
205
229
  project_id TEXT NOT NULL,
@@ -217,20 +241,21 @@ class PostgreSQLStorage(StorageBackend):
217
241
  """)
218
242
  conn.execute(f"""
219
243
  CREATE INDEX IF NOT EXISTS idx_outcomes_project_agent
220
- ON {self.schema}.alma_outcomes(project_id, agent)
244
+ ON {self.schema}.{outcomes_table}(project_id, agent)
221
245
  """)
222
246
  conn.execute(f"""
223
247
  CREATE INDEX IF NOT EXISTS idx_outcomes_task_type
224
- ON {self.schema}.alma_outcomes(project_id, agent, task_type)
248
+ ON {self.schema}.{outcomes_table}(project_id, agent, task_type)
225
249
  """)
226
250
  conn.execute(f"""
227
251
  CREATE INDEX IF NOT EXISTS idx_outcomes_timestamp
228
- ON {self.schema}.alma_outcomes(project_id, timestamp DESC)
252
+ ON {self.schema}.{outcomes_table}(project_id, timestamp DESC)
229
253
  """)
230
254
 
231
255
  # User preferences table
256
+ preferences_table = self.TABLE_NAMES[MemoryType.PREFERENCES]
232
257
  conn.execute(f"""
233
- CREATE TABLE IF NOT EXISTS {self.schema}.alma_preferences (
258
+ CREATE TABLE IF NOT EXISTS {self.schema}.{preferences_table} (
234
259
  id TEXT PRIMARY KEY,
235
260
  user_id TEXT NOT NULL,
236
261
  category TEXT,
@@ -243,12 +268,13 @@ class PostgreSQLStorage(StorageBackend):
243
268
  """)
244
269
  conn.execute(f"""
245
270
  CREATE INDEX IF NOT EXISTS idx_preferences_user
246
- ON {self.schema}.alma_preferences(user_id)
271
+ ON {self.schema}.{preferences_table}(user_id)
247
272
  """)
248
273
 
249
274
  # Domain knowledge table
275
+ domain_knowledge_table = self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]
250
276
  conn.execute(f"""
251
- CREATE TABLE IF NOT EXISTS {self.schema}.alma_domain_knowledge (
277
+ CREATE TABLE IF NOT EXISTS {self.schema}.{domain_knowledge_table} (
252
278
  id TEXT PRIMARY KEY,
253
279
  agent TEXT NOT NULL,
254
280
  project_id TEXT NOT NULL,
@@ -263,12 +289,18 @@ class PostgreSQLStorage(StorageBackend):
263
289
  """)
264
290
  conn.execute(f"""
265
291
  CREATE INDEX IF NOT EXISTS idx_domain_knowledge_project_agent
266
- ON {self.schema}.alma_domain_knowledge(project_id, agent)
292
+ ON {self.schema}.{domain_knowledge_table}(project_id, agent)
293
+ """)
294
+ # Confidence index for efficient filtering by confidence score
295
+ conn.execute(f"""
296
+ CREATE INDEX IF NOT EXISTS idx_domain_knowledge_confidence
297
+ ON {self.schema}.{domain_knowledge_table}(project_id, confidence DESC)
267
298
  """)
268
299
 
269
300
  # Anti-patterns table
301
+ anti_patterns_table = self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]
270
302
  conn.execute(f"""
271
- CREATE TABLE IF NOT EXISTS {self.schema}.alma_anti_patterns (
303
+ CREATE TABLE IF NOT EXISTS {self.schema}.{anti_patterns_table} (
272
304
  id TEXT PRIMARY KEY,
273
305
  agent TEXT NOT NULL,
274
306
  project_id TEXT NOT NULL,
@@ -284,19 +316,18 @@ class PostgreSQLStorage(StorageBackend):
284
316
  """)
285
317
  conn.execute(f"""
286
318
  CREATE INDEX IF NOT EXISTS idx_anti_patterns_project_agent
287
- ON {self.schema}.alma_anti_patterns(project_id, agent)
319
+ ON {self.schema}.{anti_patterns_table}(project_id, agent)
288
320
  """)
289
321
 
290
322
  # Create vector indexes if pgvector available
291
323
  # Using HNSW instead of IVFFlat because HNSW can be built on empty tables
292
324
  # IVFFlat requires existing data to build, which causes silent failures on fresh databases
293
325
  if self._pgvector_available:
294
- for table in [
295
- "alma_heuristics",
296
- "alma_outcomes",
297
- "alma_domain_knowledge",
298
- "alma_anti_patterns",
299
- ]:
326
+ # Vector-enabled tables use canonical memory type names
327
+ vector_tables = [
328
+ self.TABLE_NAMES[mt] for mt in MemoryType.VECTOR_ENABLED
329
+ ]
330
+ for table in vector_tables:
300
331
  try:
301
332
  conn.execute(f"""
302
333
  CREATE INDEX IF NOT EXISTS idx_{table}_embedding
@@ -359,7 +390,7 @@ class PostgreSQLStorage(StorageBackend):
359
390
  with self._get_connection() as conn:
360
391
  conn.execute(
361
392
  f"""
362
- INSERT INTO {self.schema}.alma_heuristics
393
+ INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
363
394
  (id, agent, project_id, condition, strategy, confidence,
364
395
  occurrence_count, success_count, last_validated, created_at, metadata, embedding)
365
396
  VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
@@ -398,7 +429,7 @@ class PostgreSQLStorage(StorageBackend):
398
429
  with self._get_connection() as conn:
399
430
  conn.execute(
400
431
  f"""
401
- INSERT INTO {self.schema}.alma_outcomes
432
+ INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]}
402
433
  (id, agent, project_id, task_type, task_description, success,
403
434
  strategy_used, duration_ms, error_message, user_feedback, timestamp, metadata, embedding)
404
435
  VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
@@ -438,7 +469,7 @@ class PostgreSQLStorage(StorageBackend):
438
469
  with self._get_connection() as conn:
439
470
  conn.execute(
440
471
  f"""
441
- INSERT INTO {self.schema}.alma_preferences
472
+ INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.PREFERENCES]}
442
473
  (id, user_id, category, preference, source, confidence, timestamp, metadata)
443
474
  VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
444
475
  ON CONFLICT (id) DO UPDATE SET
@@ -468,7 +499,7 @@ class PostgreSQLStorage(StorageBackend):
468
499
  with self._get_connection() as conn:
469
500
  conn.execute(
470
501
  f"""
471
- INSERT INTO {self.schema}.alma_domain_knowledge
502
+ INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]}
472
503
  (id, agent, project_id, domain, fact, source, confidence, last_verified, metadata, embedding)
473
504
  VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
474
505
  ON CONFLICT (id) DO UPDATE SET
@@ -502,7 +533,7 @@ class PostgreSQLStorage(StorageBackend):
502
533
  with self._get_connection() as conn:
503
534
  conn.execute(
504
535
  f"""
505
- INSERT INTO {self.schema}.alma_anti_patterns
536
+ INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]}
506
537
  (id, agent, project_id, pattern, why_bad, better_alternative,
507
538
  occurrence_count, last_seen, created_at, metadata, embedding)
508
539
  VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
@@ -548,7 +579,7 @@ class PostgreSQLStorage(StorageBackend):
548
579
  with self._get_connection() as conn:
549
580
  conn.executemany(
550
581
  f"""
551
- INSERT INTO {self.schema}.alma_heuristics
582
+ INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
552
583
  (id, agent, project_id, condition, strategy, confidence,
553
584
  occurrence_count, success_count, last_validated, created_at, metadata, embedding)
554
585
  VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
@@ -593,7 +624,7 @@ class PostgreSQLStorage(StorageBackend):
593
624
  with self._get_connection() as conn:
594
625
  conn.executemany(
595
626
  f"""
596
- INSERT INTO {self.schema}.alma_outcomes
627
+ INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]}
597
628
  (id, agent, project_id, task_type, task_description, success,
598
629
  strategy_used, duration_ms, error_message, user_feedback, timestamp, metadata, embedding)
599
630
  VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
@@ -641,7 +672,7 @@ class PostgreSQLStorage(StorageBackend):
641
672
  with self._get_connection() as conn:
642
673
  conn.executemany(
643
674
  f"""
644
- INSERT INTO {self.schema}.alma_domain_knowledge
675
+ INSERT INTO {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]}
645
676
  (id, agent, project_id, domain, fact, source, confidence, last_verified, metadata, embedding)
646
677
  VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
647
678
  ON CONFLICT (id) DO UPDATE SET
@@ -689,7 +720,7 @@ class PostgreSQLStorage(StorageBackend):
689
720
  # Use pgvector similarity search
690
721
  query = f"""
691
722
  SELECT *, 1 - (embedding <=> %s::vector) as similarity
692
- FROM {self.schema}.alma_heuristics
723
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
693
724
  WHERE project_id = %s AND confidence >= %s
694
725
  """
695
726
  params: List[Any] = [
@@ -708,7 +739,7 @@ class PostgreSQLStorage(StorageBackend):
708
739
  # Standard query
709
740
  query = f"""
710
741
  SELECT *
711
- FROM {self.schema}.alma_heuristics
742
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
712
743
  WHERE project_id = %s AND confidence >= %s
713
744
  """
714
745
  params = [project_id, min_confidence]
@@ -745,14 +776,14 @@ class PostgreSQLStorage(StorageBackend):
745
776
  if embedding and self._pgvector_available:
746
777
  query = f"""
747
778
  SELECT *, 1 - (embedding <=> %s::vector) as similarity
748
- FROM {self.schema}.alma_outcomes
779
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]}
749
780
  WHERE project_id = %s
750
781
  """
751
782
  params: List[Any] = [self._embedding_to_db(embedding), project_id]
752
783
  else:
753
784
  query = f"""
754
785
  SELECT *
755
- FROM {self.schema}.alma_outcomes
786
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]}
756
787
  WHERE project_id = %s
757
788
  """
758
789
  params = [project_id]
@@ -791,7 +822,7 @@ class PostgreSQLStorage(StorageBackend):
791
822
  ) -> List[UserPreference]:
792
823
  """Get user preferences."""
793
824
  with self._get_connection() as conn:
794
- query = f"SELECT * FROM {self.schema}.alma_preferences WHERE user_id = %s"
825
+ query = f"SELECT * FROM {self.schema}.{self.TABLE_NAMES[MemoryType.PREFERENCES]} WHERE user_id = %s"
795
826
  params: List[Any] = [user_id]
796
827
 
797
828
  if category:
@@ -816,14 +847,14 @@ class PostgreSQLStorage(StorageBackend):
816
847
  if embedding and self._pgvector_available:
817
848
  query = f"""
818
849
  SELECT *, 1 - (embedding <=> %s::vector) as similarity
819
- FROM {self.schema}.alma_domain_knowledge
850
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]}
820
851
  WHERE project_id = %s
821
852
  """
822
853
  params: List[Any] = [self._embedding_to_db(embedding), project_id]
823
854
  else:
824
855
  query = f"""
825
856
  SELECT *
826
- FROM {self.schema}.alma_domain_knowledge
857
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]}
827
858
  WHERE project_id = %s
828
859
  """
829
860
  params = [project_id]
@@ -864,14 +895,14 @@ class PostgreSQLStorage(StorageBackend):
864
895
  if embedding and self._pgvector_available:
865
896
  query = f"""
866
897
  SELECT *, 1 - (embedding <=> %s::vector) as similarity
867
- FROM {self.schema}.alma_anti_patterns
898
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]}
868
899
  WHERE project_id = %s
869
900
  """
870
901
  params: List[Any] = [self._embedding_to_db(embedding), project_id]
871
902
  else:
872
903
  query = f"""
873
904
  SELECT *
874
- FROM {self.schema}.alma_anti_patterns
905
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]}
875
906
  WHERE project_id = %s
876
907
  """
877
908
  params = [project_id]
@@ -934,7 +965,7 @@ class PostgreSQLStorage(StorageBackend):
934
965
  if embedding and self._pgvector_available:
935
966
  query = f"""
936
967
  SELECT *, 1 - (embedding <=> %s::vector) as similarity
937
- FROM {self.schema}.alma_heuristics
968
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
938
969
  WHERE project_id = %s AND confidence >= %s AND agent = ANY(%s)
939
970
  ORDER BY similarity DESC LIMIT %s
940
971
  """
@@ -948,7 +979,7 @@ class PostgreSQLStorage(StorageBackend):
948
979
  else:
949
980
  query = f"""
950
981
  SELECT *
951
- FROM {self.schema}.alma_heuristics
982
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
952
983
  WHERE project_id = %s AND confidence >= %s AND agent = ANY(%s)
953
984
  ORDER BY confidence DESC LIMIT %s
954
985
  """
@@ -983,7 +1014,7 @@ class PostgreSQLStorage(StorageBackend):
983
1014
  if embedding and self._pgvector_available:
984
1015
  query = f"""
985
1016
  SELECT *, 1 - (embedding <=> %s::vector) as similarity
986
- FROM {self.schema}.alma_outcomes
1017
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]}
987
1018
  WHERE project_id = %s AND agent = ANY(%s)
988
1019
  """
989
1020
  params: List[Any] = [
@@ -994,7 +1025,7 @@ class PostgreSQLStorage(StorageBackend):
994
1025
  else:
995
1026
  query = f"""
996
1027
  SELECT *
997
- FROM {self.schema}.alma_outcomes
1028
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]}
998
1029
  WHERE project_id = %s AND agent = ANY(%s)
999
1030
  """
1000
1031
  params = [project_id, agents]
@@ -1040,7 +1071,7 @@ class PostgreSQLStorage(StorageBackend):
1040
1071
  if embedding and self._pgvector_available:
1041
1072
  query = f"""
1042
1073
  SELECT *, 1 - (embedding <=> %s::vector) as similarity
1043
- FROM {self.schema}.alma_domain_knowledge
1074
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]}
1044
1075
  WHERE project_id = %s AND agent = ANY(%s)
1045
1076
  """
1046
1077
  params: List[Any] = [
@@ -1051,7 +1082,7 @@ class PostgreSQLStorage(StorageBackend):
1051
1082
  else:
1052
1083
  query = f"""
1053
1084
  SELECT *
1054
- FROM {self.schema}.alma_domain_knowledge
1085
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]}
1055
1086
  WHERE project_id = %s AND agent = ANY(%s)
1056
1087
  """
1057
1088
  params = [project_id, agents]
@@ -1093,7 +1124,7 @@ class PostgreSQLStorage(StorageBackend):
1093
1124
  if embedding and self._pgvector_available:
1094
1125
  query = f"""
1095
1126
  SELECT *, 1 - (embedding <=> %s::vector) as similarity
1096
- FROM {self.schema}.alma_anti_patterns
1127
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]}
1097
1128
  WHERE project_id = %s AND agent = ANY(%s)
1098
1129
  """
1099
1130
  params: List[Any] = [
@@ -1104,7 +1135,7 @@ class PostgreSQLStorage(StorageBackend):
1104
1135
  else:
1105
1136
  query = f"""
1106
1137
  SELECT *
1107
- FROM {self.schema}.alma_anti_patterns
1138
+ FROM {self.schema}.{self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]}
1108
1139
  WHERE project_id = %s AND agent = ANY(%s)
1109
1140
  """
1110
1141
  params = [project_id, agents]
@@ -1150,7 +1181,7 @@ class PostgreSQLStorage(StorageBackend):
1150
1181
 
1151
1182
  with self._get_connection() as conn:
1152
1183
  cursor = conn.execute(
1153
- f"UPDATE {self.schema}.alma_heuristics SET {', '.join(set_clauses)} WHERE id = %s",
1184
+ f"UPDATE {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]} SET {', '.join(set_clauses)} WHERE id = %s",
1154
1185
  params,
1155
1186
  )
1156
1187
  conn.commit()
@@ -1166,7 +1197,7 @@ class PostgreSQLStorage(StorageBackend):
1166
1197
  if success:
1167
1198
  cursor = conn.execute(
1168
1199
  f"""
1169
- UPDATE {self.schema}.alma_heuristics
1200
+ UPDATE {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
1170
1201
  SET occurrence_count = occurrence_count + 1,
1171
1202
  success_count = success_count + 1,
1172
1203
  last_validated = %s
@@ -1177,7 +1208,7 @@ class PostgreSQLStorage(StorageBackend):
1177
1208
  else:
1178
1209
  cursor = conn.execute(
1179
1210
  f"""
1180
- UPDATE {self.schema}.alma_heuristics
1211
+ UPDATE {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]}
1181
1212
  SET occurrence_count = occurrence_count + 1,
1182
1213
  last_validated = %s
1183
1214
  WHERE id = %s
@@ -1195,7 +1226,7 @@ class PostgreSQLStorage(StorageBackend):
1195
1226
  """Update confidence score for a heuristic."""
1196
1227
  with self._get_connection() as conn:
1197
1228
  cursor = conn.execute(
1198
- f"UPDATE {self.schema}.alma_heuristics SET confidence = %s WHERE id = %s",
1229
+ f"UPDATE {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]} SET confidence = %s WHERE id = %s",
1199
1230
  (new_confidence, heuristic_id),
1200
1231
  )
1201
1232
  conn.commit()
@@ -1209,7 +1240,7 @@ class PostgreSQLStorage(StorageBackend):
1209
1240
  """Update confidence score for domain knowledge."""
1210
1241
  with self._get_connection() as conn:
1211
1242
  cursor = conn.execute(
1212
- f"UPDATE {self.schema}.alma_domain_knowledge SET confidence = %s WHERE id = %s",
1243
+ f"UPDATE {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]} SET confidence = %s WHERE id = %s",
1213
1244
  (new_confidence, knowledge_id),
1214
1245
  )
1215
1246
  conn.commit()
@@ -1221,7 +1252,7 @@ class PostgreSQLStorage(StorageBackend):
1221
1252
  """Delete a heuristic by ID."""
1222
1253
  with self._get_connection() as conn:
1223
1254
  cursor = conn.execute(
1224
- f"DELETE FROM {self.schema}.alma_heuristics WHERE id = %s",
1255
+ f"DELETE FROM {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]} WHERE id = %s",
1225
1256
  (heuristic_id,),
1226
1257
  )
1227
1258
  conn.commit()
@@ -1231,7 +1262,7 @@ class PostgreSQLStorage(StorageBackend):
1231
1262
  """Delete an outcome by ID."""
1232
1263
  with self._get_connection() as conn:
1233
1264
  cursor = conn.execute(
1234
- f"DELETE FROM {self.schema}.alma_outcomes WHERE id = %s",
1265
+ f"DELETE FROM {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]} WHERE id = %s",
1235
1266
  (outcome_id,),
1236
1267
  )
1237
1268
  conn.commit()
@@ -1241,7 +1272,7 @@ class PostgreSQLStorage(StorageBackend):
1241
1272
  """Delete domain knowledge by ID."""
1242
1273
  with self._get_connection() as conn:
1243
1274
  cursor = conn.execute(
1244
- f"DELETE FROM {self.schema}.alma_domain_knowledge WHERE id = %s",
1275
+ f"DELETE FROM {self.schema}.{self.TABLE_NAMES[MemoryType.DOMAIN_KNOWLEDGE]} WHERE id = %s",
1245
1276
  (knowledge_id,),
1246
1277
  )
1247
1278
  conn.commit()
@@ -1251,7 +1282,7 @@ class PostgreSQLStorage(StorageBackend):
1251
1282
  """Delete an anti-pattern by ID."""
1252
1283
  with self._get_connection() as conn:
1253
1284
  cursor = conn.execute(
1254
- f"DELETE FROM {self.schema}.alma_anti_patterns WHERE id = %s",
1285
+ f"DELETE FROM {self.schema}.{self.TABLE_NAMES[MemoryType.ANTI_PATTERNS]} WHERE id = %s",
1255
1286
  (anti_pattern_id,),
1256
1287
  )
1257
1288
  conn.commit()
@@ -1265,7 +1296,7 @@ class PostgreSQLStorage(StorageBackend):
1265
1296
  ) -> int:
1266
1297
  """Delete old outcomes."""
1267
1298
  with self._get_connection() as conn:
1268
- query = f"DELETE FROM {self.schema}.alma_outcomes WHERE project_id = %s AND timestamp < %s"
1299
+ query = f"DELETE FROM {self.schema}.{self.TABLE_NAMES[MemoryType.OUTCOMES]} WHERE project_id = %s AND timestamp < %s"
1269
1300
  params: List[Any] = [project_id, older_than]
1270
1301
 
1271
1302
  if agent:
@@ -1287,7 +1318,7 @@ class PostgreSQLStorage(StorageBackend):
1287
1318
  ) -> int:
1288
1319
  """Delete low-confidence heuristics."""
1289
1320
  with self._get_connection() as conn:
1290
- query = f"DELETE FROM {self.schema}.alma_heuristics WHERE project_id = %s AND confidence < %s"
1321
+ query = f"DELETE FROM {self.schema}.{self.TABLE_NAMES[MemoryType.HEURISTICS]} WHERE project_id = %s AND confidence < %s"
1291
1322
  params: List[Any] = [project_id, below_confidence]
1292
1323
 
1293
1324
  if agent:
@@ -1317,29 +1348,25 @@ class PostgreSQLStorage(StorageBackend):
1317
1348
  }
1318
1349
 
1319
1350
  with self._get_connection() as conn:
1320
- tables = [
1321
- ("heuristics", "alma_heuristics"),
1322
- ("outcomes", "alma_outcomes"),
1323
- ("domain_knowledge", "alma_domain_knowledge"),
1324
- ("anti_patterns", "alma_anti_patterns"),
1325
- ]
1326
-
1327
- for stat_name, table in tables:
1328
- query = f"SELECT COUNT(*) as count FROM {self.schema}.{table} WHERE project_id = %s"
1329
- params: List[Any] = [project_id]
1330
- if agent:
1331
- query += " AND agent = %s"
1332
- params.append(agent)
1333
- cursor = conn.execute(query, params)
1334
- row = cursor.fetchone()
1335
- stats[f"{stat_name}_count"] = row["count"] if row else 0
1336
-
1337
- # Preferences don't have project_id
1338
- cursor = conn.execute(
1339
- f"SELECT COUNT(*) as count FROM {self.schema}.alma_preferences"
1340
- )
1341
- row = cursor.fetchone()
1342
- stats["preferences_count"] = row["count"] if row else 0
1351
+ # Use canonical memory types for stats
1352
+ for memory_type in MemoryType.ALL:
1353
+ table = self.TABLE_NAMES[memory_type]
1354
+ if memory_type == MemoryType.PREFERENCES:
1355
+ # Preferences don't have project_id
1356
+ cursor = conn.execute(
1357
+ f"SELECT COUNT(*) as count FROM {self.schema}.{table}"
1358
+ )
1359
+ row = cursor.fetchone()
1360
+ stats[f"{memory_type}_count"] = row["count"] if row else 0
1361
+ else:
1362
+ query = f"SELECT COUNT(*) as count FROM {self.schema}.{table} WHERE project_id = %s"
1363
+ params: List[Any] = [project_id]
1364
+ if agent:
1365
+ query += " AND agent = %s"
1366
+ params.append(agent)
1367
+ cursor = conn.execute(query, params)
1368
+ row = cursor.fetchone()
1369
+ stats[f"{memory_type}_count"] = row["count"] if row else 0
1343
1370
 
1344
1371
  stats["total_count"] = sum(
1345
1372
  stats.get(k, 0) for k in stats if k.endswith("_count")
@@ -1450,3 +1477,83 @@ class PostgreSQLStorage(StorageBackend):
1450
1477
  """Close connection pool."""
1451
1478
  if self._pool:
1452
1479
  self._pool.close()
1480
+
1481
+ # ==================== MIGRATION SUPPORT ====================
1482
+
1483
+ def _get_version_store(self):
1484
+ """Get or create the version store."""
1485
+ if self._version_store is None:
1486
+ from alma.storage.migrations.version_stores import PostgreSQLVersionStore
1487
+
1488
+ self._version_store = PostgreSQLVersionStore(self._pool, self.schema)
1489
+ return self._version_store
1490
+
1491
+ def _get_migration_runner(self):
1492
+ """Get or create the migration runner."""
1493
+ if self._migration_runner is None:
1494
+ from alma.storage.migrations.runner import MigrationRunner
1495
+ from alma.storage.migrations.versions import v1_0_0 # noqa: F401
1496
+
1497
+ self._migration_runner = MigrationRunner(
1498
+ version_store=self._get_version_store(),
1499
+ backend="postgresql",
1500
+ )
1501
+ return self._migration_runner
1502
+
1503
+ def _ensure_migrated(self) -> None:
1504
+ """Ensure database is migrated to latest version."""
1505
+ runner = self._get_migration_runner()
1506
+ if runner.needs_migration():
1507
+ with self._get_connection() as conn:
1508
+ applied = runner.migrate(conn)
1509
+ if applied:
1510
+ logger.info(f"Applied {len(applied)} migrations: {applied}")
1511
+
1512
+ def get_schema_version(self) -> Optional[str]:
1513
+ """Get the current schema version."""
1514
+ return self._get_version_store().get_current_version()
1515
+
1516
+ def get_migration_status(self) -> Dict[str, Any]:
1517
+ """Get migration status information."""
1518
+ runner = self._get_migration_runner()
1519
+ status = runner.get_status()
1520
+ status["migration_supported"] = True
1521
+ return status
1522
+
1523
+ def migrate(
1524
+ self,
1525
+ target_version: Optional[str] = None,
1526
+ dry_run: bool = False,
1527
+ ) -> List[str]:
1528
+ """
1529
+ Apply pending schema migrations.
1530
+
1531
+ Args:
1532
+ target_version: Optional target version (applies all if not specified)
1533
+ dry_run: If True, show what would be done without making changes
1534
+
1535
+ Returns:
1536
+ List of applied migration versions
1537
+ """
1538
+ runner = self._get_migration_runner()
1539
+ with self._get_connection() as conn:
1540
+ return runner.migrate(conn, target_version=target_version, dry_run=dry_run)
1541
+
1542
+ def rollback(
1543
+ self,
1544
+ target_version: str,
1545
+ dry_run: bool = False,
1546
+ ) -> List[str]:
1547
+ """
1548
+ Roll back schema to a previous version.
1549
+
1550
+ Args:
1551
+ target_version: Version to roll back to
1552
+ dry_run: If True, show what would be done without making changes
1553
+
1554
+ Returns:
1555
+ List of rolled back migration versions
1556
+ """
1557
+ runner = self._get_migration_runner()
1558
+ with self._get_connection() as conn:
1559
+ return runner.rollback(conn, target_version=target_version, dry_run=dry_run)