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
alma/storage/base.py CHANGED
@@ -508,6 +508,64 @@ class StorageBackend(ABC):
508
508
  """
509
509
  pass
510
510
 
511
+ # ==================== MIGRATION SUPPORT ====================
512
+
513
+ def get_schema_version(self) -> Optional[str]:
514
+ """
515
+ Get the current schema version.
516
+
517
+ Returns:
518
+ Current schema version string, or None if not tracked
519
+ """
520
+ # Default implementation returns None (no version tracking)
521
+ return None
522
+
523
+ def get_migration_status(self) -> Dict[str, Any]:
524
+ """
525
+ Get migration status information.
526
+
527
+ Returns:
528
+ Dict with current version, pending migrations, etc.
529
+ """
530
+ return {
531
+ "current_version": self.get_schema_version(),
532
+ "target_version": None,
533
+ "pending_count": 0,
534
+ "pending_versions": [],
535
+ "needs_migration": False,
536
+ "migration_supported": False,
537
+ }
538
+
539
+ def migrate(
540
+ self, target_version: Optional[str] = None, dry_run: bool = False
541
+ ) -> List[str]:
542
+ """
543
+ Apply pending schema migrations.
544
+
545
+ Args:
546
+ target_version: Optional target version (applies all if not specified)
547
+ dry_run: If True, show what would be done without making changes
548
+
549
+ Returns:
550
+ List of applied migration versions
551
+ """
552
+ # Default implementation does nothing
553
+ return []
554
+
555
+ def rollback(self, target_version: str, dry_run: bool = False) -> List[str]:
556
+ """
557
+ Roll back schema to a previous version.
558
+
559
+ Args:
560
+ target_version: Version to roll back to
561
+ dry_run: If True, show what would be done without making changes
562
+
563
+ Returns:
564
+ List of rolled back migration versions
565
+ """
566
+ # Default implementation does nothing
567
+ return []
568
+
511
569
  # ==================== UTILITY ====================
512
570
 
513
571
  @classmethod
@@ -0,0 +1,103 @@
1
+ """
2
+ ALMA Storage Constants.
3
+
4
+ Canonical naming conventions for memory types across all storage backends.
5
+ This ensures consistency for:
6
+ - Data migration between backends
7
+ - Backend-agnostic code
8
+ - Documentation consistency
9
+ """
10
+
11
+ from typing import Dict
12
+
13
+
14
+ class MemoryType:
15
+ """
16
+ Canonical memory type identifiers.
17
+
18
+ These are the internal names used consistently across all backends.
19
+ Each backend may add a prefix or transform these for their specific
20
+ storage format, but the canonical names remain constant.
21
+ """
22
+
23
+ HEURISTICS = "heuristics"
24
+ OUTCOMES = "outcomes"
25
+ PREFERENCES = "preferences"
26
+ DOMAIN_KNOWLEDGE = "domain_knowledge"
27
+ ANTI_PATTERNS = "anti_patterns"
28
+
29
+ # All memory types as a tuple for iteration
30
+ ALL = (
31
+ HEURISTICS,
32
+ OUTCOMES,
33
+ PREFERENCES,
34
+ DOMAIN_KNOWLEDGE,
35
+ ANTI_PATTERNS,
36
+ )
37
+
38
+ # Memory types that support embeddings/vector search
39
+ VECTOR_ENABLED = (
40
+ HEURISTICS,
41
+ OUTCOMES,
42
+ DOMAIN_KNOWLEDGE,
43
+ ANTI_PATTERNS,
44
+ )
45
+
46
+
47
+ def get_table_name(memory_type: str, prefix: str = "") -> str:
48
+ """
49
+ Get the table/container name for a memory type.
50
+
51
+ Args:
52
+ memory_type: One of the MemoryType constants
53
+ prefix: Optional prefix to add (e.g., "alma_" for PostgreSQL)
54
+
55
+ Returns:
56
+ The formatted table/container name
57
+
58
+ Example:
59
+ >>> get_table_name(MemoryType.HEURISTICS, "alma_")
60
+ 'alma_heuristics'
61
+ >>> get_table_name(MemoryType.DOMAIN_KNOWLEDGE)
62
+ 'domain_knowledge'
63
+ """
64
+ if memory_type not in MemoryType.ALL:
65
+ raise ValueError(f"Unknown memory type: {memory_type}")
66
+ return f"{prefix}{memory_type}"
67
+
68
+
69
+ def get_table_names(prefix: str = "") -> Dict[str, str]:
70
+ """
71
+ Get all table/container names with an optional prefix.
72
+
73
+ Args:
74
+ prefix: Optional prefix to add (e.g., "alma_" for PostgreSQL)
75
+
76
+ Returns:
77
+ Dict mapping canonical memory type to table/container name
78
+
79
+ Example:
80
+ >>> get_table_names("alma_")
81
+ {
82
+ 'heuristics': 'alma_heuristics',
83
+ 'outcomes': 'alma_outcomes',
84
+ 'preferences': 'alma_preferences',
85
+ 'domain_knowledge': 'alma_domain_knowledge',
86
+ 'anti_patterns': 'alma_anti_patterns',
87
+ }
88
+ """
89
+ return {mt: get_table_name(mt, prefix) for mt in MemoryType.ALL}
90
+
91
+
92
+ # Pre-computed table name mappings for each backend
93
+ # These are the canonical mappings that should be used
94
+
95
+ # PostgreSQL uses alma_ prefix with underscores
96
+ POSTGRESQL_TABLE_NAMES = get_table_names("alma_")
97
+
98
+ # SQLite uses no prefix (local file-based, no collision risk)
99
+ SQLITE_TABLE_NAMES = get_table_names("")
100
+
101
+ # Azure Cosmos uses alma_ prefix with underscores (standardized)
102
+ # Note: Previously used hyphens, now standardized to underscores
103
+ AZURE_COSMOS_CONTAINER_NAMES = get_table_names("alma_")
@@ -12,6 +12,7 @@ from pathlib import Path
12
12
  from typing import Any, Dict, List, Optional
13
13
 
14
14
  from alma.storage.base import StorageBackend
15
+ from alma.storage.constants import MemoryType
15
16
  from alma.types import (
16
17
  AntiPattern,
17
18
  DomainKnowledge,
@@ -49,14 +50,8 @@ class FileBasedStorage(StorageBackend):
49
50
  self.storage_dir = Path(storage_dir)
50
51
  self.storage_dir.mkdir(parents=True, exist_ok=True)
51
52
 
52
- # File paths
53
- self._files = {
54
- "heuristics": self.storage_dir / "heuristics.json",
55
- "outcomes": self.storage_dir / "outcomes.json",
56
- "preferences": self.storage_dir / "preferences.json",
57
- "domain_knowledge": self.storage_dir / "domain_knowledge.json",
58
- "anti_patterns": self.storage_dir / "anti_patterns.json",
59
- }
53
+ # File paths (using canonical memory type names)
54
+ self._files = {mt: self.storage_dir / f"{mt}.json" for mt in MemoryType.ALL}
60
55
 
61
56
  # Initialize empty files if they don't exist
62
57
  for file_path in self._files.values():
@@ -0,0 +1,21 @@
1
+ """
2
+ ALMA Schema Migration Framework.
3
+
4
+ Provides version tracking and migration capabilities for storage backends.
5
+ """
6
+
7
+ from alma.storage.migrations.base import (
8
+ Migration,
9
+ MigrationError,
10
+ MigrationRegistry,
11
+ SchemaVersion,
12
+ )
13
+ from alma.storage.migrations.runner import MigrationRunner
14
+
15
+ __all__ = [
16
+ "Migration",
17
+ "MigrationError",
18
+ "MigrationRegistry",
19
+ "MigrationRunner",
20
+ "SchemaVersion",
21
+ ]
@@ -0,0 +1,321 @@
1
+ """
2
+ ALMA Migration Framework - Base Classes.
3
+
4
+ Provides abstract migration classes and version tracking utilities.
5
+ """
6
+
7
+ import logging
8
+ from abc import ABC, abstractmethod
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime, timezone
11
+ from typing import Any, Callable, Dict, List, Optional, Type
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class MigrationError(Exception):
17
+ """Exception raised when a migration fails."""
18
+
19
+ def __init__(
20
+ self,
21
+ message: str,
22
+ version: Optional[str] = None,
23
+ cause: Optional[Exception] = None,
24
+ ):
25
+ self.version = version
26
+ self.cause = cause
27
+ super().__init__(message)
28
+
29
+
30
+ @dataclass
31
+ class SchemaVersion:
32
+ """
33
+ Represents a schema version record.
34
+
35
+ Attributes:
36
+ version: Semantic version string (e.g., "1.0.0")
37
+ applied_at: When the migration was applied
38
+ description: Human-readable description of changes
39
+ checksum: Optional hash for integrity verification
40
+ """
41
+
42
+ version: str
43
+ applied_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
44
+ description: str = ""
45
+ checksum: Optional[str] = None
46
+
47
+ def __lt__(self, other: "SchemaVersion") -> bool:
48
+ """Compare versions for sorting."""
49
+ return self._parse_version(self.version) < self._parse_version(other.version)
50
+
51
+ @staticmethod
52
+ def _parse_version(version: str) -> tuple:
53
+ """Parse version string into comparable tuple."""
54
+ parts = version.split(".")
55
+ result = []
56
+ for part in parts:
57
+ try:
58
+ result.append(int(part))
59
+ except ValueError:
60
+ result.append(part)
61
+ return tuple(result)
62
+
63
+
64
+ class Migration(ABC):
65
+ """
66
+ Abstract base class for schema migrations.
67
+
68
+ Subclasses must implement upgrade() and optionally downgrade().
69
+
70
+ Example:
71
+ class AddTagsColumn(Migration):
72
+ version = "1.1.0"
73
+ description = "Add tags column to heuristics table"
74
+
75
+ def upgrade(self, connection):
76
+ connection.execute(
77
+ "ALTER TABLE heuristics ADD COLUMN tags TEXT"
78
+ )
79
+
80
+ def downgrade(self, connection):
81
+ connection.execute(
82
+ "ALTER TABLE heuristics DROP COLUMN tags"
83
+ )
84
+ """
85
+
86
+ # These must be set by subclasses
87
+ version: str = ""
88
+ description: str = ""
89
+ # Optional: previous version this migration depends on
90
+ depends_on: Optional[str] = None
91
+
92
+ @abstractmethod
93
+ def upgrade(self, connection: Any) -> None:
94
+ """
95
+ Apply the migration.
96
+
97
+ Args:
98
+ connection: Database connection or storage instance
99
+ """
100
+ pass
101
+
102
+ def downgrade(self, connection: Any) -> None:
103
+ """
104
+ Revert the migration (optional).
105
+
106
+ Args:
107
+ connection: Database connection or storage instance
108
+
109
+ Raises:
110
+ NotImplementedError: If downgrade is not supported
111
+ """
112
+ raise NotImplementedError(
113
+ f"Downgrade not implemented for migration {self.version}"
114
+ )
115
+
116
+ def pre_check(self, connection: Any) -> bool:
117
+ """
118
+ Optional pre-migration check.
119
+
120
+ Override to verify prerequisites before migration.
121
+
122
+ Args:
123
+ connection: Database connection
124
+
125
+ Returns:
126
+ True if migration can proceed, False otherwise
127
+ """
128
+ return True
129
+
130
+ def post_check(self, connection: Any) -> bool:
131
+ """
132
+ Optional post-migration verification.
133
+
134
+ Override to verify migration was successful.
135
+
136
+ Args:
137
+ connection: Database connection
138
+
139
+ Returns:
140
+ True if migration was successful
141
+ """
142
+ return True
143
+
144
+
145
+ class MigrationRegistry:
146
+ """
147
+ Registry for available migrations.
148
+
149
+ Manages migration discovery, ordering, and execution planning.
150
+ """
151
+
152
+ def __init__(self) -> None:
153
+ self._migrations: Dict[str, Type[Migration]] = {}
154
+ self._backend_migrations: Dict[str, Dict[str, Type[Migration]]] = {}
155
+
156
+ def register(
157
+ self, migration_class: Type[Migration], backend: Optional[str] = None
158
+ ) -> Type[Migration]:
159
+ """
160
+ Register a migration class.
161
+
162
+ Args:
163
+ migration_class: The migration class to register
164
+ backend: Optional backend name (e.g., "sqlite", "postgresql")
165
+
166
+ Returns:
167
+ The migration class (for use as decorator)
168
+ """
169
+ version = migration_class.version
170
+ if not version:
171
+ raise ValueError(
172
+ f"Migration {migration_class.__name__} must have a version"
173
+ )
174
+
175
+ if backend:
176
+ if backend not in self._backend_migrations:
177
+ self._backend_migrations[backend] = {}
178
+ self._backend_migrations[backend][version] = migration_class
179
+ logger.debug(f"Registered migration {version} for backend {backend}")
180
+ else:
181
+ self._migrations[version] = migration_class
182
+ logger.debug(f"Registered global migration {version}")
183
+
184
+ return migration_class
185
+
186
+ def get_migration(
187
+ self, version: str, backend: Optional[str] = None
188
+ ) -> Optional[Type[Migration]]:
189
+ """
190
+ Get a migration class by version.
191
+
192
+ Args:
193
+ version: Version string to look up
194
+ backend: Optional backend name
195
+
196
+ Returns:
197
+ Migration class or None if not found
198
+ """
199
+ if backend and backend in self._backend_migrations:
200
+ migration = self._backend_migrations[backend].get(version)
201
+ if migration:
202
+ return migration
203
+ return self._migrations.get(version)
204
+
205
+ def get_all_migrations(
206
+ self, backend: Optional[str] = None
207
+ ) -> List[Type[Migration]]:
208
+ """
209
+ Get all migrations in version order.
210
+
211
+ Args:
212
+ backend: Optional backend name to filter migrations
213
+
214
+ Returns:
215
+ List of migration classes sorted by version
216
+ """
217
+ migrations = dict(self._migrations)
218
+ if backend and backend in self._backend_migrations:
219
+ migrations.update(self._backend_migrations[backend])
220
+
221
+ return [
222
+ cls
223
+ for _, cls in sorted(
224
+ migrations.items(),
225
+ key=lambda x: SchemaVersion._parse_version(x[0]),
226
+ )
227
+ ]
228
+
229
+ def get_pending_migrations(
230
+ self,
231
+ current_version: Optional[str],
232
+ backend: Optional[str] = None,
233
+ ) -> List[Type[Migration]]:
234
+ """
235
+ Get migrations that need to be applied.
236
+
237
+ Args:
238
+ current_version: Current schema version (None if fresh install)
239
+ backend: Optional backend name
240
+
241
+ Returns:
242
+ List of migration classes that need to be applied
243
+ """
244
+ all_migrations = self.get_all_migrations(backend)
245
+
246
+ if current_version is None:
247
+ return all_migrations
248
+
249
+ current = SchemaVersion._parse_version(current_version)
250
+ return [
251
+ m
252
+ for m in all_migrations
253
+ if SchemaVersion._parse_version(m.version) > current
254
+ ]
255
+
256
+ def get_rollback_migrations(
257
+ self,
258
+ current_version: str,
259
+ target_version: str,
260
+ backend: Optional[str] = None,
261
+ ) -> List[Type[Migration]]:
262
+ """
263
+ Get migrations that need to be rolled back.
264
+
265
+ Args:
266
+ current_version: Current schema version
267
+ target_version: Target version to roll back to
268
+ backend: Optional backend name
269
+
270
+ Returns:
271
+ List of migration classes to roll back (in reverse order)
272
+ """
273
+ all_migrations = self.get_all_migrations(backend)
274
+
275
+ current = SchemaVersion._parse_version(current_version)
276
+ target = SchemaVersion._parse_version(target_version)
277
+
278
+ rollback = [
279
+ m
280
+ for m in all_migrations
281
+ if target < SchemaVersion._parse_version(m.version) <= current
282
+ ]
283
+
284
+ # Return in reverse order for rollback
285
+ return list(reversed(rollback))
286
+
287
+
288
+ # Global registry instance
289
+ _global_registry = MigrationRegistry()
290
+
291
+
292
+ def register_migration(
293
+ backend: Optional[str] = None,
294
+ ) -> Callable[[Type[Migration]], Type[Migration]]:
295
+ """
296
+ Decorator to register a migration class.
297
+
298
+ Args:
299
+ backend: Optional backend name
300
+
301
+ Example:
302
+ @register_migration()
303
+ class MyMigration(Migration):
304
+ version = "1.0.0"
305
+ ...
306
+
307
+ @register_migration(backend="postgresql")
308
+ class PostgresSpecificMigration(Migration):
309
+ version = "1.0.1"
310
+ ...
311
+ """
312
+
313
+ def decorator(cls: Type[Migration]) -> Type[Migration]:
314
+ return _global_registry.register(cls, backend)
315
+
316
+ return decorator
317
+
318
+
319
+ def get_registry() -> MigrationRegistry:
320
+ """Get the global migration registry."""
321
+ return _global_registry