alma-memory 0.4.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 (94) hide show
  1. alma/__init__.py +121 -45
  2. alma/confidence/__init__.py +1 -1
  3. alma/confidence/engine.py +92 -58
  4. alma/confidence/types.py +34 -14
  5. alma/config/loader.py +3 -2
  6. alma/consolidation/__init__.py +23 -0
  7. alma/consolidation/engine.py +678 -0
  8. alma/consolidation/prompts.py +84 -0
  9. alma/core.py +136 -28
  10. alma/domains/__init__.py +6 -6
  11. alma/domains/factory.py +12 -9
  12. alma/domains/schemas.py +17 -3
  13. alma/domains/types.py +8 -4
  14. alma/events/__init__.py +75 -0
  15. alma/events/emitter.py +284 -0
  16. alma/events/storage_mixin.py +246 -0
  17. alma/events/types.py +126 -0
  18. alma/events/webhook.py +425 -0
  19. alma/exceptions.py +49 -0
  20. alma/extraction/__init__.py +31 -0
  21. alma/extraction/auto_learner.py +265 -0
  22. alma/extraction/extractor.py +420 -0
  23. alma/graph/__init__.py +106 -0
  24. alma/graph/backends/__init__.py +32 -0
  25. alma/graph/backends/kuzu.py +624 -0
  26. alma/graph/backends/memgraph.py +432 -0
  27. alma/graph/backends/memory.py +236 -0
  28. alma/graph/backends/neo4j.py +417 -0
  29. alma/graph/base.py +159 -0
  30. alma/graph/extraction.py +198 -0
  31. alma/graph/store.py +860 -0
  32. alma/harness/__init__.py +4 -4
  33. alma/harness/base.py +18 -9
  34. alma/harness/domains.py +27 -11
  35. alma/initializer/__init__.py +1 -1
  36. alma/initializer/initializer.py +51 -43
  37. alma/initializer/types.py +25 -17
  38. alma/integration/__init__.py +9 -9
  39. alma/integration/claude_agents.py +32 -20
  40. alma/integration/helena.py +32 -22
  41. alma/integration/victor.py +57 -33
  42. alma/learning/__init__.py +27 -27
  43. alma/learning/forgetting.py +198 -148
  44. alma/learning/heuristic_extractor.py +40 -24
  45. alma/learning/protocols.py +65 -17
  46. alma/learning/validation.py +7 -2
  47. alma/mcp/__init__.py +4 -4
  48. alma/mcp/__main__.py +2 -1
  49. alma/mcp/resources.py +17 -16
  50. alma/mcp/server.py +102 -44
  51. alma/mcp/tools.py +180 -45
  52. alma/observability/__init__.py +84 -0
  53. alma/observability/config.py +302 -0
  54. alma/observability/logging.py +424 -0
  55. alma/observability/metrics.py +583 -0
  56. alma/observability/tracing.py +440 -0
  57. alma/progress/__init__.py +3 -3
  58. alma/progress/tracker.py +26 -20
  59. alma/progress/types.py +8 -12
  60. alma/py.typed +0 -0
  61. alma/retrieval/__init__.py +11 -11
  62. alma/retrieval/cache.py +20 -21
  63. alma/retrieval/embeddings.py +4 -4
  64. alma/retrieval/engine.py +179 -39
  65. alma/retrieval/scoring.py +73 -63
  66. alma/session/__init__.py +2 -2
  67. alma/session/manager.py +5 -5
  68. alma/session/types.py +5 -4
  69. alma/storage/__init__.py +70 -0
  70. alma/storage/azure_cosmos.py +414 -133
  71. alma/storage/base.py +215 -4
  72. alma/storage/chroma.py +1443 -0
  73. alma/storage/constants.py +103 -0
  74. alma/storage/file_based.py +59 -28
  75. alma/storage/migrations/__init__.py +21 -0
  76. alma/storage/migrations/base.py +321 -0
  77. alma/storage/migrations/runner.py +323 -0
  78. alma/storage/migrations/version_stores.py +337 -0
  79. alma/storage/migrations/versions/__init__.py +11 -0
  80. alma/storage/migrations/versions/v1_0_0.py +373 -0
  81. alma/storage/pinecone.py +1080 -0
  82. alma/storage/postgresql.py +1559 -0
  83. alma/storage/qdrant.py +1306 -0
  84. alma/storage/sqlite_local.py +504 -60
  85. alma/testing/__init__.py +46 -0
  86. alma/testing/factories.py +301 -0
  87. alma/testing/mocks.py +389 -0
  88. alma/types.py +62 -14
  89. alma_memory-0.5.1.dist-info/METADATA +939 -0
  90. alma_memory-0.5.1.dist-info/RECORD +93 -0
  91. {alma_memory-0.4.0.dist-info → alma_memory-0.5.1.dist-info}/WHEEL +1 -1
  92. alma_memory-0.4.0.dist-info/METADATA +0 -488
  93. alma_memory-0.4.0.dist-info/RECORD +0 -52
  94. {alma_memory-0.4.0.dist-info → alma_memory-0.5.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,84 @@
1
+ """
2
+ ALMA Consolidation Prompts.
3
+
4
+ LLM prompts for intelligently merging similar memories.
5
+ """
6
+
7
+ # Prompt for merging multiple similar heuristics into one
8
+ MERGE_HEURISTICS_PROMPT = """You are a memory consolidation agent. Given these similar heuristics that have been identified as near-duplicates based on semantic similarity, create a single consolidated heuristic that captures the essence of all.
9
+
10
+ Similar Heuristics:
11
+ {heuristics}
12
+
13
+ Create a consolidated heuristic that:
14
+ 1. Generalizes the condition to cover all cases
15
+ 2. Combines the strategies into a comprehensive approach
16
+ 3. Preserves any unique insights from individual heuristics
17
+
18
+ Output a JSON object with exactly these fields:
19
+ {{
20
+ "condition": "The generalized condition that triggers this heuristic",
21
+ "strategy": "The merged strategy combining all approaches",
22
+ "confidence": <average confidence as a float between 0 and 1>
23
+ }}
24
+
25
+ Only output the JSON object, no other text."""
26
+
27
+ # Prompt for merging similar domain knowledge
28
+ MERGE_DOMAIN_KNOWLEDGE_PROMPT = """You are a memory consolidation agent. Given these similar domain knowledge facts that have been identified as near-duplicates, create a single consolidated fact that captures all the information.
29
+
30
+ Similar Domain Knowledge:
31
+ {knowledge_items}
32
+
33
+ Create a consolidated fact that:
34
+ 1. Combines all unique information
35
+ 2. Removes redundancy
36
+ 3. Maintains accuracy
37
+
38
+ Output a JSON object with exactly these fields:
39
+ {{
40
+ "fact": "The consolidated fact combining all information",
41
+ "confidence": <average confidence as a float between 0 and 1>
42
+ }}
43
+
44
+ Only output the JSON object, no other text."""
45
+
46
+ # Prompt for merging anti-patterns
47
+ MERGE_ANTI_PATTERNS_PROMPT = """You are a memory consolidation agent. Given these similar anti-patterns that have been identified as near-duplicates, create a single consolidated anti-pattern.
48
+
49
+ Similar Anti-Patterns:
50
+ {anti_patterns}
51
+
52
+ Create a consolidated anti-pattern that:
53
+ 1. Generalizes the pattern description
54
+ 2. Combines all reasons why it's bad
55
+ 3. Provides a comprehensive alternative
56
+
57
+ Output a JSON object with exactly these fields:
58
+ {{
59
+ "pattern": "The generalized pattern to avoid",
60
+ "why_bad": "Combined explanation of why this pattern is problematic",
61
+ "better_alternative": "The recommended alternative approach"
62
+ }}
63
+
64
+ Only output the JSON object, no other text."""
65
+
66
+ # Prompt for merging outcomes (typically used for summarization rather than true merge)
67
+ MERGE_OUTCOMES_PROMPT = """You are a memory consolidation agent. Given these similar task outcomes, create a summary that captures the key learnings.
68
+
69
+ Similar Outcomes:
70
+ {outcomes}
71
+
72
+ Create a summary that:
73
+ 1. Identifies the common task type
74
+ 2. Notes the overall success/failure pattern
75
+ 3. Highlights effective strategies
76
+
77
+ Output a JSON object with exactly these fields:
78
+ {{
79
+ "task_type": "The common task type",
80
+ "summary": "Summary of the outcomes and learnings",
81
+ "recommended_strategy": "The most effective strategy based on the outcomes"
82
+ }}
83
+
84
+ Only output the JSON object, no other text."""
alma/core.py CHANGED
@@ -1,27 +1,38 @@
1
1
  """
2
2
  ALMA Core - Main interface for the Agent Learning Memory Architecture.
3
+
4
+ API Return Type Conventions:
5
+ - Create operations: Return created object or raise exception
6
+ - Update operations: Return updated object or raise exception
7
+ - Delete operations: Return bool (success) or int (count), raise on failure
8
+ - Query operations: Return list (empty if none) or object
9
+
10
+ All scope violations raise ScopeViolationError for consistent error handling.
3
11
  """
4
12
 
5
- from typing import Optional, Dict, Any, List
6
- from pathlib import Path
7
- import yaml
8
13
  import logging
14
+ import time
15
+ from typing import Any, Dict, Optional
9
16
 
17
+ from alma.config.loader import ConfigLoader
18
+ from alma.exceptions import ScopeViolationError
19
+ from alma.learning.protocols import LearningProtocol
20
+ from alma.observability.logging import get_logger
21
+ from alma.observability.metrics import get_metrics
22
+ from alma.observability.tracing import SpanKind, get_tracer, trace_method
23
+ from alma.retrieval.engine import RetrievalEngine
24
+ from alma.storage.base import StorageBackend
10
25
  from alma.types import (
11
- MemorySlice,
26
+ DomainKnowledge,
12
27
  MemoryScope,
13
- Heuristic,
28
+ MemorySlice,
14
29
  Outcome,
15
30
  UserPreference,
16
- DomainKnowledge,
17
- AntiPattern,
18
31
  )
19
- from alma.storage.base import StorageBackend
20
- from alma.retrieval.engine import RetrievalEngine
21
- from alma.learning.protocols import LearningProtocol
22
- from alma.config.loader import ConfigLoader
23
32
 
24
33
  logger = logging.getLogger(__name__)
34
+ structured_logger = get_logger(__name__)
35
+ tracer = get_tracer(__name__)
25
36
 
26
37
 
27
38
  class ALMA:
@@ -114,14 +125,22 @@ class ALMA:
114
125
 
115
126
  if storage_type == "azure":
116
127
  from alma.storage.azure_cosmos import AzureCosmosStorage
128
+
117
129
  return AzureCosmosStorage.from_config(config)
130
+ elif storage_type == "postgres":
131
+ from alma.storage.postgresql import PostgreSQLStorage
132
+
133
+ return PostgreSQLStorage.from_config(config)
118
134
  elif storage_type == "sqlite":
119
135
  from alma.storage.sqlite_local import SQLiteStorage
136
+
120
137
  return SQLiteStorage.from_config(config)
121
138
  else:
122
139
  from alma.storage.file_based import FileBasedStorage
140
+
123
141
  return FileBasedStorage.from_config(config)
124
142
 
143
+ @trace_method(name="ALMA.retrieve", kind=SpanKind.INTERNAL)
125
144
  def retrieve(
126
145
  self,
127
146
  task: str,
@@ -141,11 +160,19 @@ class ALMA:
141
160
  Returns:
142
161
  MemorySlice with relevant memories for context injection
143
162
  """
163
+ start_time = time.time()
164
+ metrics = get_metrics()
165
+
144
166
  # Validate agent has a defined scope
145
167
  if agent not in self.scopes:
146
168
  logger.warning(f"Agent '{agent}' has no defined scope, using defaults")
169
+ structured_logger.warning(
170
+ "Agent has no defined scope, using defaults",
171
+ agent=agent,
172
+ project_id=self.project_id,
173
+ )
147
174
 
148
- return self.retrieval.retrieve(
175
+ result = self.retrieval.retrieve(
149
176
  query=task,
150
177
  agent=agent,
151
178
  project_id=self.project_id,
@@ -154,6 +181,30 @@ class ALMA:
154
181
  scope=self.scopes.get(agent),
155
182
  )
156
183
 
184
+ # Record metrics
185
+ duration_ms = (time.time() - start_time) * 1000
186
+ cache_hit = result.retrieval_time_ms < 10 # Approximate cache hit detection
187
+ metrics.record_retrieve_latency(
188
+ duration_ms=duration_ms,
189
+ agent=agent,
190
+ project_id=self.project_id,
191
+ cache_hit=cache_hit,
192
+ items_returned=result.total_items,
193
+ )
194
+
195
+ structured_logger.info(
196
+ "Memory retrieval completed",
197
+ agent=agent,
198
+ project_id=self.project_id,
199
+ task_preview=task[:50] if task else "",
200
+ items_returned=result.total_items,
201
+ duration_ms=duration_ms,
202
+ cache_hit=cache_hit,
203
+ )
204
+
205
+ return result
206
+
207
+ @trace_method(name="ALMA.learn", kind=SpanKind.INTERNAL)
157
208
  def learn(
158
209
  self,
159
210
  agent: str,
@@ -164,7 +215,7 @@ class ALMA:
164
215
  duration_ms: Optional[int] = None,
165
216
  error_message: Optional[str] = None,
166
217
  feedback: Optional[str] = None,
167
- ) -> bool:
218
+ ) -> Outcome:
168
219
  """
169
220
  Learn from a task outcome.
170
221
 
@@ -182,9 +233,15 @@ class ALMA:
182
233
  feedback: User feedback if provided
183
234
 
184
235
  Returns:
185
- True if learning was accepted, False if rejected (scope violation)
236
+ The created Outcome record
237
+
238
+ Raises:
239
+ ScopeViolationError: If learning is outside agent's scope
186
240
  """
187
- result = self.learning.learn(
241
+ start_time = time.time()
242
+ metrics = get_metrics()
243
+
244
+ outcome_record = self.learning.learn(
188
245
  agent=agent,
189
246
  project_id=self.project_id,
190
247
  task=task,
@@ -197,10 +254,28 @@ class ALMA:
197
254
  )
198
255
 
199
256
  # Invalidate cache for this agent/project after learning
200
- if result:
201
- self.retrieval.invalidate_cache(agent=agent, project_id=self.project_id)
257
+ self.retrieval.invalidate_cache(agent=agent, project_id=self.project_id)
202
258
 
203
- return result
259
+ # Record metrics
260
+ learn_duration_ms = (time.time() - start_time) * 1000
261
+ metrics.record_learn_operation(
262
+ duration_ms=learn_duration_ms,
263
+ agent=agent,
264
+ project_id=self.project_id,
265
+ memory_type="outcome",
266
+ success=True,
267
+ )
268
+
269
+ structured_logger.info(
270
+ "Learning operation completed",
271
+ agent=agent,
272
+ project_id=self.project_id,
273
+ task_type=task_type,
274
+ outcome=outcome,
275
+ duration_ms=learn_duration_ms,
276
+ )
277
+
278
+ return outcome_record
204
279
 
205
280
  def add_user_preference(
206
281
  self,
@@ -239,7 +314,7 @@ class ALMA:
239
314
  domain: str,
240
315
  fact: str,
241
316
  source: str = "user_stated",
242
- ) -> Optional[DomainKnowledge]:
317
+ ) -> DomainKnowledge:
243
318
  """
244
319
  Add domain knowledge within agent's scope.
245
320
 
@@ -250,15 +325,17 @@ class ALMA:
250
325
  source: How this was learned
251
326
 
252
327
  Returns:
253
- The created DomainKnowledge or None if scope violation
328
+ The created DomainKnowledge
329
+
330
+ Raises:
331
+ ScopeViolationError: If agent is not allowed to learn in this domain
254
332
  """
255
333
  # Check scope
256
334
  scope = self.scopes.get(agent)
257
335
  if scope and not scope.is_allowed(domain):
258
- logger.warning(
259
- f"Agent '{agent}' not allowed to learn in domain '{domain}'"
336
+ raise ScopeViolationError(
337
+ f"Agent '{agent}' is not allowed to learn in domain '{domain}'"
260
338
  )
261
- return None
262
339
 
263
340
  result = self.learning.add_domain_knowledge(
264
341
  agent=agent,
@@ -269,11 +346,11 @@ class ALMA:
269
346
  )
270
347
 
271
348
  # Invalidate cache for this agent/project after adding knowledge
272
- if result:
273
- self.retrieval.invalidate_cache(agent=agent, project_id=self.project_id)
349
+ self.retrieval.invalidate_cache(agent=agent, project_id=self.project_id)
274
350
 
275
351
  return result
276
352
 
353
+ @trace_method(name="ALMA.forget", kind=SpanKind.INTERNAL)
277
354
  def forget(
278
355
  self,
279
356
  agent: Optional[str] = None,
@@ -283,7 +360,8 @@ class ALMA:
283
360
  """
284
361
  Prune stale or low-confidence memories.
285
362
 
286
- Invalidates cache after pruning to ensure fresh retrieval results.
363
+ This is a delete operation that invalidates cache after pruning
364
+ to ensure fresh retrieval results.
287
365
 
288
366
  Args:
289
367
  agent: Specific agent to prune, or None for all
@@ -291,8 +369,14 @@ class ALMA:
291
369
  below_confidence: Remove heuristics below this confidence
292
370
 
293
371
  Returns:
294
- Number of items pruned
372
+ Number of items pruned (0 if nothing was pruned)
373
+
374
+ Raises:
375
+ StorageError: If the delete operation fails
295
376
  """
377
+ start_time = time.time()
378
+ metrics = get_metrics()
379
+
296
380
  count = self.learning.forget(
297
381
  project_id=self.project_id,
298
382
  agent=agent,
@@ -304,17 +388,41 @@ class ALMA:
304
388
  if count > 0:
305
389
  self.retrieval.invalidate_cache(agent=agent, project_id=self.project_id)
306
390
 
391
+ # Record metrics
392
+ duration_ms = (time.time() - start_time) * 1000
393
+ metrics.record_forget_operation(
394
+ duration_ms=duration_ms,
395
+ agent=agent,
396
+ project_id=self.project_id,
397
+ items_removed=count,
398
+ )
399
+
400
+ structured_logger.info(
401
+ "Forget operation completed",
402
+ agent=agent or "all",
403
+ project_id=self.project_id,
404
+ items_removed=count,
405
+ older_than_days=older_than_days,
406
+ below_confidence=below_confidence,
407
+ duration_ms=duration_ms,
408
+ )
409
+
307
410
  return count
308
411
 
309
412
  def get_stats(self, agent: Optional[str] = None) -> Dict[str, Any]:
310
413
  """
311
414
  Get memory statistics.
312
415
 
416
+ This is a query operation that returns statistics about stored memories.
417
+
313
418
  Args:
314
419
  agent: Specific agent or None for all
315
420
 
316
421
  Returns:
317
- Dict with counts and metadata
422
+ Dict with counts and metadata (always returns a dict, may be empty)
423
+
424
+ Raises:
425
+ StorageError: If the query operation fails
318
426
  """
319
427
  return self.storage.get_stats(
320
428
  project_id=self.project_id,
alma/domains/__init__.py CHANGED
@@ -5,17 +5,17 @@ Provides domain-agnostic memory schemas and factory pattern
5
5
  for creating domain-specific ALMA instances.
6
6
  """
7
7
 
8
- from alma.domains.types import (
9
- DomainSchema,
10
- EntityType,
11
- RelationshipType,
12
- )
13
8
  from alma.domains.factory import DomainMemoryFactory
14
9
  from alma.domains.schemas import (
15
10
  get_coding_schema,
11
+ get_general_schema,
16
12
  get_research_schema,
17
13
  get_sales_schema,
18
- get_general_schema,
14
+ )
15
+ from alma.domains.types import (
16
+ DomainSchema,
17
+ EntityType,
18
+ RelationshipType,
19
19
  )
20
20
 
21
21
  __all__ = [
alma/domains/factory.py CHANGED
@@ -4,18 +4,18 @@ Domain Memory Factory.
4
4
  Factory pattern for creating domain-specific ALMA instances.
5
5
  """
6
6
 
7
- from typing import Dict, Any, Optional, List, Type
8
7
  import logging
8
+ from typing import Any, Dict, List, Optional
9
9
 
10
- from alma.domains.types import DomainSchema, EntityType, RelationshipType
11
10
  from alma.domains.schemas import (
12
11
  get_coding_schema,
12
+ get_content_creation_schema,
13
+ get_customer_support_schema,
14
+ get_general_schema,
13
15
  get_research_schema,
14
16
  get_sales_schema,
15
- get_general_schema,
16
- get_customer_support_schema,
17
- get_content_creation_schema,
18
17
  )
18
+ from alma.domains.types import DomainSchema
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
@@ -124,7 +124,9 @@ class DomainMemoryFactory:
124
124
  description=config.get("description", f"Custom schema: {name}"),
125
125
  learning_categories=config.get("learning_categories", []),
126
126
  excluded_categories=config.get("excluded_categories", []),
127
- min_occurrences_for_heuristic=config.get("min_occurrences_for_heuristic", 3),
127
+ min_occurrences_for_heuristic=config.get(
128
+ "min_occurrences_for_heuristic", 3
129
+ ),
128
130
  confidence_decay_days=config.get("confidence_decay_days", 30.0),
129
131
  )
130
132
 
@@ -200,16 +202,17 @@ class DomainMemoryFactory:
200
202
  - Initialize domain-specific entity tracking
201
203
  """
202
204
  # Import here to avoid circular dependency
203
- from alma.storage.file_based import FileBasedStorage
204
- from alma.retrieval import RetrievalEngine
205
+ from alma import ALMA
205
206
  from alma.learning import LearningProtocol
207
+ from alma.retrieval import RetrievalEngine
208
+ from alma.storage.file_based import FileBasedStorage
206
209
  from alma.types import MemoryScope
207
- from alma import ALMA
208
210
 
209
211
  # Create storage if not provided
210
212
  if storage is None:
211
213
  import tempfile
212
214
  from pathlib import Path
215
+
213
216
  storage_dir = Path(tempfile.mkdtemp()) / ".alma" / project_id
214
217
  storage = FileBasedStorage(storage_dir)
215
218
 
alma/domains/schemas.py CHANGED
@@ -4,7 +4,7 @@ Pre-built Domain Schemas.
4
4
  Standard domain schemas for common use cases.
5
5
  """
6
6
 
7
- from alma.domains.types import DomainSchema, EntityType, RelationshipType
7
+ from alma.domains.types import DomainSchema
8
8
 
9
9
 
10
10
  def get_coding_schema() -> DomainSchema:
@@ -110,12 +110,26 @@ def get_research_schema() -> DomainSchema:
110
110
  schema.add_entity_type(
111
111
  name="paper",
112
112
  description="An academic paper or article",
113
- attributes=["title", "authors", "year", "citations", "abstract", "venue", "doi"],
113
+ attributes=[
114
+ "title",
115
+ "authors",
116
+ "year",
117
+ "citations",
118
+ "abstract",
119
+ "venue",
120
+ "doi",
121
+ ],
114
122
  )
115
123
  schema.add_entity_type(
116
124
  name="hypothesis",
117
125
  description="A research hypothesis",
118
- attributes=["statement", "confidence", "evidence_for", "evidence_against", "status"],
126
+ attributes=[
127
+ "statement",
128
+ "confidence",
129
+ "evidence_for",
130
+ "evidence_against",
131
+ "status",
132
+ ],
119
133
  )
120
134
  schema.add_entity_type(
121
135
  name="experiment",
alma/domains/types.py CHANGED
@@ -4,10 +4,10 @@ Domain Memory Types.
4
4
  Data models for domain-specific memory schemas.
5
5
  """
6
6
 
7
+ import uuid
7
8
  from dataclasses import dataclass, field
8
9
  from datetime import datetime, timezone
9
- from typing import List, Dict, Any, Optional
10
- import uuid
10
+ from typing import Any, Dict, List, Optional
11
11
 
12
12
 
13
13
  @dataclass
@@ -21,11 +21,15 @@ class EntityType:
21
21
 
22
22
  name: str # "feature", "test", "paper", "lead"
23
23
  description: str
24
- attributes: List[str] = field(default_factory=list) # ["status", "priority", "owner"]
24
+ attributes: List[str] = field(
25
+ default_factory=list
26
+ ) # ["status", "priority", "owner"]
25
27
 
26
28
  # Optional schema validation
27
29
  required_attributes: List[str] = field(default_factory=list)
28
- attribute_types: Dict[str, str] = field(default_factory=dict) # attr -> "str", "int", "bool"
30
+ attribute_types: Dict[str, str] = field(
31
+ default_factory=dict
32
+ ) # attr -> "str", "int", "bool"
29
33
 
30
34
  def validate_entity(self, entity: Dict[str, Any]) -> List[str]:
31
35
  """Validate an entity instance against this type."""
@@ -0,0 +1,75 @@
1
+ """
2
+ ALMA Event System.
3
+
4
+ Provides event emission and webhook delivery for memory operations.
5
+
6
+ The event system allows external systems to react to memory changes through:
7
+ 1. In-process callbacks (subscribe to event types)
8
+ 2. Webhooks (HTTP delivery with signatures)
9
+
10
+ Example - In-process subscription:
11
+ ```python
12
+ from alma.events import get_emitter, MemoryEventType
13
+
14
+ def on_memory_created(event):
15
+ print(f"Memory created: {event.memory_id}")
16
+
17
+ emitter = get_emitter()
18
+ emitter.subscribe(MemoryEventType.CREATED, on_memory_created)
19
+ ```
20
+
21
+ Example - Webhook delivery:
22
+ ```python
23
+ from alma.events import WebhookConfig, WebhookManager, get_emitter
24
+
25
+ manager = WebhookManager()
26
+ manager.add_webhook(WebhookConfig(
27
+ url="https://example.com/webhook",
28
+ events=[MemoryEventType.CREATED, MemoryEventType.UPDATED],
29
+ secret="my-webhook-secret"
30
+ ))
31
+ manager.start(get_emitter())
32
+ ```
33
+ """
34
+
35
+ from alma.events.emitter import (
36
+ EventEmitter,
37
+ get_emitter,
38
+ reset_emitter,
39
+ )
40
+ from alma.events.storage_mixin import (
41
+ EventAwareStorageMixin,
42
+ emit_on_save,
43
+ )
44
+ from alma.events.types import (
45
+ MemoryEvent,
46
+ MemoryEventType,
47
+ create_memory_event,
48
+ )
49
+ from alma.events.webhook import (
50
+ WebhookConfig,
51
+ WebhookDelivery,
52
+ WebhookDeliveryResult,
53
+ WebhookDeliveryStatus,
54
+ WebhookManager,
55
+ )
56
+
57
+ __all__ = [
58
+ # Types
59
+ "MemoryEvent",
60
+ "MemoryEventType",
61
+ "create_memory_event",
62
+ # Emitter
63
+ "EventEmitter",
64
+ "get_emitter",
65
+ "reset_emitter",
66
+ # Webhook
67
+ "WebhookConfig",
68
+ "WebhookDelivery",
69
+ "WebhookDeliveryResult",
70
+ "WebhookDeliveryStatus",
71
+ "WebhookManager",
72
+ # Storage Mixin
73
+ "EventAwareStorageMixin",
74
+ "emit_on_save",
75
+ ]