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.
- alma/__init__.py +121 -45
- alma/confidence/__init__.py +1 -1
- alma/confidence/engine.py +92 -58
- alma/confidence/types.py +34 -14
- alma/config/loader.py +3 -2
- alma/consolidation/__init__.py +23 -0
- alma/consolidation/engine.py +678 -0
- alma/consolidation/prompts.py +84 -0
- alma/core.py +136 -28
- alma/domains/__init__.py +6 -6
- alma/domains/factory.py +12 -9
- alma/domains/schemas.py +17 -3
- alma/domains/types.py +8 -4
- alma/events/__init__.py +75 -0
- alma/events/emitter.py +284 -0
- alma/events/storage_mixin.py +246 -0
- alma/events/types.py +126 -0
- alma/events/webhook.py +425 -0
- alma/exceptions.py +49 -0
- alma/extraction/__init__.py +31 -0
- alma/extraction/auto_learner.py +265 -0
- alma/extraction/extractor.py +420 -0
- alma/graph/__init__.py +106 -0
- alma/graph/backends/__init__.py +32 -0
- alma/graph/backends/kuzu.py +624 -0
- alma/graph/backends/memgraph.py +432 -0
- alma/graph/backends/memory.py +236 -0
- alma/graph/backends/neo4j.py +417 -0
- alma/graph/base.py +159 -0
- alma/graph/extraction.py +198 -0
- alma/graph/store.py +860 -0
- alma/harness/__init__.py +4 -4
- alma/harness/base.py +18 -9
- alma/harness/domains.py +27 -11
- alma/initializer/__init__.py +1 -1
- alma/initializer/initializer.py +51 -43
- alma/initializer/types.py +25 -17
- alma/integration/__init__.py +9 -9
- alma/integration/claude_agents.py +32 -20
- alma/integration/helena.py +32 -22
- alma/integration/victor.py +57 -33
- alma/learning/__init__.py +27 -27
- alma/learning/forgetting.py +198 -148
- alma/learning/heuristic_extractor.py +40 -24
- alma/learning/protocols.py +65 -17
- alma/learning/validation.py +7 -2
- alma/mcp/__init__.py +4 -4
- alma/mcp/__main__.py +2 -1
- alma/mcp/resources.py +17 -16
- alma/mcp/server.py +102 -44
- alma/mcp/tools.py +180 -45
- 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/progress/__init__.py +3 -3
- alma/progress/tracker.py +26 -20
- alma/progress/types.py +8 -12
- alma/py.typed +0 -0
- alma/retrieval/__init__.py +11 -11
- alma/retrieval/cache.py +20 -21
- alma/retrieval/embeddings.py +4 -4
- alma/retrieval/engine.py +179 -39
- alma/retrieval/scoring.py +73 -63
- alma/session/__init__.py +2 -2
- alma/session/manager.py +5 -5
- alma/session/types.py +5 -4
- alma/storage/__init__.py +70 -0
- alma/storage/azure_cosmos.py +414 -133
- alma/storage/base.py +215 -4
- alma/storage/chroma.py +1443 -0
- alma/storage/constants.py +103 -0
- alma/storage/file_based.py +59 -28
- 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/pinecone.py +1080 -0
- alma/storage/postgresql.py +1559 -0
- alma/storage/qdrant.py +1306 -0
- alma/storage/sqlite_local.py +504 -60
- alma/testing/__init__.py +46 -0
- alma/testing/factories.py +301 -0
- alma/testing/mocks.py +389 -0
- alma/types.py +62 -14
- alma_memory-0.5.1.dist-info/METADATA +939 -0
- alma_memory-0.5.1.dist-info/RECORD +93 -0
- {alma_memory-0.4.0.dist-info → alma_memory-0.5.1.dist-info}/WHEEL +1 -1
- alma_memory-0.4.0.dist-info/METADATA +0 -488
- alma_memory-0.4.0.dist-info/RECORD +0 -52
- {alma_memory-0.4.0.dist-info → alma_memory-0.5.1.dist-info}/top_level.txt +0 -0
alma/events/emitter.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Event Emitter.
|
|
3
|
+
|
|
4
|
+
Provides a pub/sub mechanism for memory events, allowing components
|
|
5
|
+
and external systems to subscribe to and receive notifications about
|
|
6
|
+
memory changes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
from typing import Awaitable, Callable, Dict, List, Optional, Union
|
|
13
|
+
|
|
14
|
+
from alma.events.types import MemoryEvent, MemoryEventType
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Type aliases for callbacks
|
|
19
|
+
SyncCallback = Callable[[MemoryEvent], None]
|
|
20
|
+
AsyncCallback = Callable[[MemoryEvent], Awaitable[None]]
|
|
21
|
+
EventCallback = Union[SyncCallback, AsyncCallback]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EventEmitter:
|
|
25
|
+
"""
|
|
26
|
+
Event emitter for memory system events.
|
|
27
|
+
|
|
28
|
+
Supports both synchronous and asynchronous callbacks, with options
|
|
29
|
+
to subscribe to specific event types or all events.
|
|
30
|
+
|
|
31
|
+
The emitter is designed to be non-blocking - callbacks are executed
|
|
32
|
+
in a way that doesn't slow down the main storage operations.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
```python
|
|
36
|
+
emitter = EventEmitter()
|
|
37
|
+
|
|
38
|
+
def on_created(event: MemoryEvent):
|
|
39
|
+
print(f"Memory created: {event.memory_id}")
|
|
40
|
+
|
|
41
|
+
emitter.subscribe(MemoryEventType.CREATED, on_created)
|
|
42
|
+
emitter.emit(event)
|
|
43
|
+
```
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, max_workers: int = 4):
|
|
47
|
+
"""
|
|
48
|
+
Initialize the event emitter.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
max_workers: Maximum number of worker threads for async callback execution
|
|
52
|
+
"""
|
|
53
|
+
self._subscribers: Dict[MemoryEventType, List[EventCallback]] = {}
|
|
54
|
+
self._global_subscribers: List[EventCallback] = []
|
|
55
|
+
self._executor = ThreadPoolExecutor(max_workers=max_workers)
|
|
56
|
+
self._enabled = True
|
|
57
|
+
|
|
58
|
+
def subscribe(
|
|
59
|
+
self,
|
|
60
|
+
event_type: MemoryEventType,
|
|
61
|
+
callback: EventCallback,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Subscribe to a specific event type.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
event_type: The type of event to subscribe to
|
|
68
|
+
callback: Function to call when event occurs (sync or async)
|
|
69
|
+
"""
|
|
70
|
+
if event_type not in self._subscribers:
|
|
71
|
+
self._subscribers[event_type] = []
|
|
72
|
+
|
|
73
|
+
if callback not in self._subscribers[event_type]:
|
|
74
|
+
self._subscribers[event_type].append(callback)
|
|
75
|
+
callback_name = getattr(callback, "__name__", repr(callback))
|
|
76
|
+
logger.debug(f"Subscribed to {event_type.value}: {callback_name}")
|
|
77
|
+
|
|
78
|
+
def subscribe_all(self, callback: EventCallback) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Subscribe to all events.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
callback: Function to call for any event
|
|
84
|
+
"""
|
|
85
|
+
if callback not in self._global_subscribers:
|
|
86
|
+
self._global_subscribers.append(callback)
|
|
87
|
+
callback_name = getattr(callback, "__name__", repr(callback))
|
|
88
|
+
logger.debug(f"Subscribed to all events: {callback_name}")
|
|
89
|
+
|
|
90
|
+
def unsubscribe(
|
|
91
|
+
self,
|
|
92
|
+
event_type: MemoryEventType,
|
|
93
|
+
callback: EventCallback,
|
|
94
|
+
) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Unsubscribe from a specific event type.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
event_type: The event type to unsubscribe from
|
|
100
|
+
callback: The callback to remove
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if callback was removed, False if not found
|
|
104
|
+
"""
|
|
105
|
+
if event_type in self._subscribers:
|
|
106
|
+
try:
|
|
107
|
+
self._subscribers[event_type].remove(callback)
|
|
108
|
+
callback_name = getattr(callback, "__name__", repr(callback))
|
|
109
|
+
logger.debug(f"Unsubscribed from {event_type.value}: {callback_name}")
|
|
110
|
+
return True
|
|
111
|
+
except ValueError:
|
|
112
|
+
pass
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
def unsubscribe_all(self, callback: EventCallback) -> bool:
|
|
116
|
+
"""
|
|
117
|
+
Unsubscribe a callback from all events.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
callback: The callback to remove
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if callback was removed, False if not found
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
self._global_subscribers.remove(callback)
|
|
127
|
+
callback_name = getattr(callback, "__name__", repr(callback))
|
|
128
|
+
logger.debug(f"Unsubscribed from all events: {callback_name}")
|
|
129
|
+
return True
|
|
130
|
+
except ValueError:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
def has_subscribers(self, event_type: Optional[MemoryEventType] = None) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Check if there are any subscribers.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
event_type: Optional specific event type to check
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if there are subscribers
|
|
142
|
+
"""
|
|
143
|
+
if event_type is None:
|
|
144
|
+
return bool(self._global_subscribers) or any(
|
|
145
|
+
bool(subs) for subs in self._subscribers.values()
|
|
146
|
+
)
|
|
147
|
+
return bool(self._subscribers.get(event_type)) or bool(self._global_subscribers)
|
|
148
|
+
|
|
149
|
+
def emit(self, event: MemoryEvent) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Emit an event to all matching subscribers (non-blocking).
|
|
152
|
+
|
|
153
|
+
Callbacks are executed in a thread pool to avoid blocking
|
|
154
|
+
the main thread. Any exceptions in callbacks are logged
|
|
155
|
+
but do not propagate.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
event: The event to emit
|
|
159
|
+
"""
|
|
160
|
+
if not self._enabled:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
callbacks = self._get_callbacks(event.event_type)
|
|
164
|
+
if not callbacks:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Execute callbacks in thread pool (non-blocking)
|
|
168
|
+
for callback in callbacks:
|
|
169
|
+
self._executor.submit(self._safe_call, callback, event)
|
|
170
|
+
|
|
171
|
+
async def emit_async(self, event: MemoryEvent) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Emit an event to all matching subscribers asynchronously.
|
|
174
|
+
|
|
175
|
+
For async callbacks, awaits them directly. For sync callbacks,
|
|
176
|
+
runs them in the executor.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
event: The event to emit
|
|
180
|
+
"""
|
|
181
|
+
if not self._enabled:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
callbacks = self._get_callbacks(event.event_type)
|
|
185
|
+
if not callbacks:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
tasks = []
|
|
189
|
+
for callback in callbacks:
|
|
190
|
+
if asyncio.iscoroutinefunction(callback):
|
|
191
|
+
tasks.append(self._safe_call_async(callback, event))
|
|
192
|
+
else:
|
|
193
|
+
# Run sync callbacks in executor
|
|
194
|
+
loop = asyncio.get_event_loop()
|
|
195
|
+
tasks.append(
|
|
196
|
+
loop.run_in_executor(
|
|
197
|
+
self._executor,
|
|
198
|
+
self._safe_call,
|
|
199
|
+
callback,
|
|
200
|
+
event,
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if tasks:
|
|
205
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
206
|
+
|
|
207
|
+
def _get_callbacks(self, event_type: MemoryEventType) -> List[EventCallback]:
|
|
208
|
+
"""Get all callbacks for an event type."""
|
|
209
|
+
callbacks = list(self._global_subscribers)
|
|
210
|
+
callbacks.extend(self._subscribers.get(event_type, []))
|
|
211
|
+
return callbacks
|
|
212
|
+
|
|
213
|
+
def _safe_call(self, callback: SyncCallback, event: MemoryEvent) -> None:
|
|
214
|
+
"""Safely call a sync callback, catching exceptions."""
|
|
215
|
+
try:
|
|
216
|
+
callback(event)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
callback_name = getattr(callback, "__name__", repr(callback))
|
|
219
|
+
logger.error(
|
|
220
|
+
f"Error in event callback {callback_name}: {e}",
|
|
221
|
+
exc_info=True,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
async def _safe_call_async(
|
|
225
|
+
self,
|
|
226
|
+
callback: AsyncCallback,
|
|
227
|
+
event: MemoryEvent,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Safely call an async callback, catching exceptions."""
|
|
230
|
+
try:
|
|
231
|
+
await callback(event)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
callback_name = getattr(callback, "__name__", repr(callback))
|
|
234
|
+
logger.error(
|
|
235
|
+
f"Error in async event callback {callback_name}: {e}",
|
|
236
|
+
exc_info=True,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def enable(self) -> None:
|
|
240
|
+
"""Enable event emission."""
|
|
241
|
+
self._enabled = True
|
|
242
|
+
|
|
243
|
+
def disable(self) -> None:
|
|
244
|
+
"""Disable event emission (events will be silently dropped)."""
|
|
245
|
+
self._enabled = False
|
|
246
|
+
|
|
247
|
+
def clear(self) -> None:
|
|
248
|
+
"""Remove all subscribers."""
|
|
249
|
+
self._subscribers.clear()
|
|
250
|
+
self._global_subscribers.clear()
|
|
251
|
+
|
|
252
|
+
def shutdown(self) -> None:
|
|
253
|
+
"""Shutdown the executor and clear subscribers."""
|
|
254
|
+
self.clear()
|
|
255
|
+
self._executor.shutdown(wait=False)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# Global emitter instance (singleton pattern)
|
|
259
|
+
_emitter: Optional[EventEmitter] = None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def get_emitter() -> EventEmitter:
|
|
263
|
+
"""
|
|
264
|
+
Get the global event emitter instance.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
The singleton EventEmitter instance
|
|
268
|
+
"""
|
|
269
|
+
global _emitter
|
|
270
|
+
if _emitter is None:
|
|
271
|
+
_emitter = EventEmitter()
|
|
272
|
+
return _emitter
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def reset_emitter() -> None:
|
|
276
|
+
"""
|
|
277
|
+
Reset the global emitter (mainly for testing).
|
|
278
|
+
|
|
279
|
+
Creates a fresh emitter instance, clearing all subscriptions.
|
|
280
|
+
"""
|
|
281
|
+
global _emitter
|
|
282
|
+
if _emitter is not None:
|
|
283
|
+
_emitter.shutdown()
|
|
284
|
+
_emitter = EventEmitter()
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Event-Aware Storage Mixin.
|
|
3
|
+
|
|
4
|
+
Provides event emission capabilities for storage backends.
|
|
5
|
+
This is a mixin class that can be used by any storage implementation
|
|
6
|
+
to emit events when memory operations occur.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import asdict
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
from alma.events.emitter import EventEmitter, get_emitter
|
|
14
|
+
from alma.events.types import MemoryEventType, create_memory_event
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EventAwareStorageMixin:
|
|
20
|
+
"""
|
|
21
|
+
Mixin that adds event emission to storage backends.
|
|
22
|
+
|
|
23
|
+
This mixin provides helper methods to emit events for various
|
|
24
|
+
storage operations. Events are only emitted if there are subscribers,
|
|
25
|
+
making the overhead negligible when events are not used.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
class MyStorage(StorageBackend, EventAwareStorageMixin):
|
|
29
|
+
def save_heuristic(self, heuristic):
|
|
30
|
+
result_id = super().save_heuristic(heuristic)
|
|
31
|
+
self._emit_memory_event(
|
|
32
|
+
event_type=MemoryEventType.CREATED,
|
|
33
|
+
agent=heuristic.agent,
|
|
34
|
+
project_id=heuristic.project_id,
|
|
35
|
+
memory_type="heuristics",
|
|
36
|
+
memory_id=result_id,
|
|
37
|
+
payload=self._heuristic_to_dict(heuristic),
|
|
38
|
+
)
|
|
39
|
+
return result_id
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
_events_enabled: bool = True
|
|
43
|
+
_custom_emitter: Optional[EventEmitter] = None
|
|
44
|
+
|
|
45
|
+
def enable_events(self) -> None:
|
|
46
|
+
"""Enable event emission for this storage instance."""
|
|
47
|
+
self._events_enabled = True
|
|
48
|
+
|
|
49
|
+
def disable_events(self) -> None:
|
|
50
|
+
"""Disable event emission for this storage instance."""
|
|
51
|
+
self._events_enabled = False
|
|
52
|
+
|
|
53
|
+
def set_emitter(self, emitter: EventEmitter) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Set a custom event emitter for this storage instance.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
emitter: The event emitter to use
|
|
59
|
+
"""
|
|
60
|
+
self._custom_emitter = emitter
|
|
61
|
+
|
|
62
|
+
def _get_emitter(self) -> EventEmitter:
|
|
63
|
+
"""Get the event emitter to use."""
|
|
64
|
+
if self._custom_emitter is not None:
|
|
65
|
+
return self._custom_emitter
|
|
66
|
+
return get_emitter()
|
|
67
|
+
|
|
68
|
+
def _should_emit(self, event_type: MemoryEventType) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Check if events should be emitted.
|
|
71
|
+
|
|
72
|
+
Returns False if:
|
|
73
|
+
- Events are disabled for this storage instance
|
|
74
|
+
- There are no subscribers for this event type
|
|
75
|
+
|
|
76
|
+
This optimization ensures event creation overhead is avoided
|
|
77
|
+
when no one is listening.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
event_type: The type of event being considered
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if an event should be emitted
|
|
84
|
+
"""
|
|
85
|
+
if not self._events_enabled:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
emitter = self._get_emitter()
|
|
89
|
+
return emitter.has_subscribers(event_type)
|
|
90
|
+
|
|
91
|
+
def _emit_memory_event(
|
|
92
|
+
self,
|
|
93
|
+
event_type: MemoryEventType,
|
|
94
|
+
agent: str,
|
|
95
|
+
project_id: str,
|
|
96
|
+
memory_type: str,
|
|
97
|
+
memory_id: str,
|
|
98
|
+
payload: Dict[str, Any],
|
|
99
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Emit a memory event if there are subscribers.
|
|
103
|
+
|
|
104
|
+
This is a convenience method that checks for subscribers before
|
|
105
|
+
creating and emitting the event.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
event_type: Type of event
|
|
109
|
+
agent: Agent name
|
|
110
|
+
project_id: Project identifier
|
|
111
|
+
memory_type: Type of memory
|
|
112
|
+
memory_id: Memory identifier
|
|
113
|
+
payload: Event-specific data
|
|
114
|
+
metadata: Optional additional context
|
|
115
|
+
"""
|
|
116
|
+
if not self._should_emit(event_type):
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
event = create_memory_event(
|
|
120
|
+
event_type=event_type,
|
|
121
|
+
agent=agent,
|
|
122
|
+
project_id=project_id,
|
|
123
|
+
memory_type=memory_type,
|
|
124
|
+
memory_id=memory_id,
|
|
125
|
+
payload=payload,
|
|
126
|
+
metadata=metadata,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
emitter = self._get_emitter()
|
|
130
|
+
emitter.emit(event)
|
|
131
|
+
|
|
132
|
+
logger.debug(f"Emitted {event_type.value} for {memory_type}/{memory_id}")
|
|
133
|
+
|
|
134
|
+
def _emit_created_event(
|
|
135
|
+
self,
|
|
136
|
+
agent: str,
|
|
137
|
+
project_id: str,
|
|
138
|
+
memory_type: str,
|
|
139
|
+
memory_id: str,
|
|
140
|
+
payload: Dict[str, Any],
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Emit a CREATED event."""
|
|
143
|
+
self._emit_memory_event(
|
|
144
|
+
event_type=MemoryEventType.CREATED,
|
|
145
|
+
agent=agent,
|
|
146
|
+
project_id=project_id,
|
|
147
|
+
memory_type=memory_type,
|
|
148
|
+
memory_id=memory_id,
|
|
149
|
+
payload=payload,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def _emit_updated_event(
|
|
153
|
+
self,
|
|
154
|
+
agent: str,
|
|
155
|
+
project_id: str,
|
|
156
|
+
memory_type: str,
|
|
157
|
+
memory_id: str,
|
|
158
|
+
payload: Dict[str, Any],
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Emit an UPDATED event."""
|
|
161
|
+
self._emit_memory_event(
|
|
162
|
+
event_type=MemoryEventType.UPDATED,
|
|
163
|
+
agent=agent,
|
|
164
|
+
project_id=project_id,
|
|
165
|
+
memory_type=memory_type,
|
|
166
|
+
memory_id=memory_id,
|
|
167
|
+
payload=payload,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _emit_deleted_event(
|
|
171
|
+
self,
|
|
172
|
+
agent: str,
|
|
173
|
+
project_id: str,
|
|
174
|
+
memory_type: str,
|
|
175
|
+
memory_id: str,
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Emit a DELETED event."""
|
|
178
|
+
self._emit_memory_event(
|
|
179
|
+
event_type=MemoryEventType.DELETED,
|
|
180
|
+
agent=agent,
|
|
181
|
+
project_id=project_id,
|
|
182
|
+
memory_type=memory_type,
|
|
183
|
+
memory_id=memory_id,
|
|
184
|
+
payload={"deleted_id": memory_id},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def emit_on_save(
|
|
189
|
+
memory_type: str, event_type: MemoryEventType = MemoryEventType.CREATED
|
|
190
|
+
):
|
|
191
|
+
"""
|
|
192
|
+
Decorator to emit events when save methods are called.
|
|
193
|
+
|
|
194
|
+
This decorator can be used on storage methods that save memories.
|
|
195
|
+
It will emit an event after the save completes successfully.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
memory_type: The type of memory being saved (e.g., "heuristics")
|
|
199
|
+
event_type: The event type to emit (default: CREATED)
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
@emit_on_save("heuristics")
|
|
203
|
+
def save_heuristic(self, heuristic: Heuristic) -> str:
|
|
204
|
+
# Original implementation
|
|
205
|
+
...
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
def decorator(func):
|
|
209
|
+
def wrapper(self, memory_item):
|
|
210
|
+
# Call the original method
|
|
211
|
+
result_id = func(self, memory_item)
|
|
212
|
+
|
|
213
|
+
# Emit event if storage supports events
|
|
214
|
+
if hasattr(self, "_emit_memory_event") and hasattr(self, "_should_emit"):
|
|
215
|
+
if self._should_emit(event_type):
|
|
216
|
+
# Extract common fields
|
|
217
|
+
agent = getattr(memory_item, "agent", "unknown")
|
|
218
|
+
project_id = getattr(memory_item, "project_id", "unknown")
|
|
219
|
+
|
|
220
|
+
# Convert to dict, handling dataclasses
|
|
221
|
+
try:
|
|
222
|
+
if hasattr(memory_item, "__dataclass_fields__"):
|
|
223
|
+
payload = {
|
|
224
|
+
k: v
|
|
225
|
+
for k, v in asdict(memory_item).items()
|
|
226
|
+
if k != "embedding" # Exclude large embedding vectors
|
|
227
|
+
}
|
|
228
|
+
else:
|
|
229
|
+
payload = {"id": result_id}
|
|
230
|
+
except Exception:
|
|
231
|
+
payload = {"id": result_id}
|
|
232
|
+
|
|
233
|
+
self._emit_memory_event(
|
|
234
|
+
event_type=event_type,
|
|
235
|
+
agent=agent,
|
|
236
|
+
project_id=project_id,
|
|
237
|
+
memory_type=memory_type,
|
|
238
|
+
memory_id=result_id,
|
|
239
|
+
payload=payload,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return result_id
|
|
243
|
+
|
|
244
|
+
return wrapper
|
|
245
|
+
|
|
246
|
+
return decorator
|
alma/events/types.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ALMA Event Types.
|
|
3
|
+
|
|
4
|
+
Defines event types and the MemoryEvent dataclass for the event system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MemoryEventType(Enum):
|
|
14
|
+
"""Types of events that can be emitted by the memory system."""
|
|
15
|
+
|
|
16
|
+
# Core memory operations
|
|
17
|
+
CREATED = "memory.created"
|
|
18
|
+
UPDATED = "memory.updated"
|
|
19
|
+
DELETED = "memory.deleted"
|
|
20
|
+
CONSOLIDATED = "memory.consolidated"
|
|
21
|
+
|
|
22
|
+
# Learning-specific events
|
|
23
|
+
HEURISTIC_FORMED = "heuristic.formed"
|
|
24
|
+
ANTIPATTERN_DETECTED = "antipattern.detected"
|
|
25
|
+
PREFERENCE_ADDED = "preference.added"
|
|
26
|
+
KNOWLEDGE_ADDED = "knowledge.added"
|
|
27
|
+
|
|
28
|
+
# Confidence events
|
|
29
|
+
CONFIDENCE_UPDATED = "confidence.updated"
|
|
30
|
+
CONFIDENCE_DECAYED = "confidence.decayed"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class MemoryEvent:
|
|
35
|
+
"""
|
|
36
|
+
Represents an event in the memory system.
|
|
37
|
+
|
|
38
|
+
Events are emitted when memory operations occur, allowing external
|
|
39
|
+
systems to react to changes through callbacks or webhooks.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
event_type: The type of event that occurred
|
|
43
|
+
agent: Name of the agent associated with the event
|
|
44
|
+
project_id: Project identifier
|
|
45
|
+
memory_type: Type of memory (heuristics, outcomes, etc.)
|
|
46
|
+
memory_id: Unique identifier of the affected memory
|
|
47
|
+
timestamp: When the event occurred
|
|
48
|
+
payload: Event-specific data (e.g., the created memory)
|
|
49
|
+
metadata: Optional additional context
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
event_type: MemoryEventType
|
|
53
|
+
agent: str
|
|
54
|
+
project_id: str
|
|
55
|
+
memory_type: (
|
|
56
|
+
str # heuristics, outcomes, preferences, domain_knowledge, anti_patterns
|
|
57
|
+
)
|
|
58
|
+
memory_id: str
|
|
59
|
+
timestamp: datetime
|
|
60
|
+
payload: Dict[str, Any]
|
|
61
|
+
metadata: Optional[Dict[str, Any]] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
64
|
+
"""Convert event to dictionary for serialization."""
|
|
65
|
+
return {
|
|
66
|
+
"event_type": self.event_type.value,
|
|
67
|
+
"agent": self.agent,
|
|
68
|
+
"project_id": self.project_id,
|
|
69
|
+
"memory_type": self.memory_type,
|
|
70
|
+
"memory_id": self.memory_id,
|
|
71
|
+
"timestamp": self.timestamp.isoformat(),
|
|
72
|
+
"payload": self.payload,
|
|
73
|
+
"metadata": self.metadata or {},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_dict(cls, data: Dict[str, Any]) -> "MemoryEvent":
|
|
78
|
+
"""Create event from dictionary."""
|
|
79
|
+
return cls(
|
|
80
|
+
event_type=MemoryEventType(data["event_type"]),
|
|
81
|
+
agent=data["agent"],
|
|
82
|
+
project_id=data["project_id"],
|
|
83
|
+
memory_type=data["memory_type"],
|
|
84
|
+
memory_id=data["memory_id"],
|
|
85
|
+
timestamp=datetime.fromisoformat(data["timestamp"]),
|
|
86
|
+
payload=data["payload"],
|
|
87
|
+
metadata=data.get("metadata"),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def create_memory_event(
|
|
92
|
+
event_type: MemoryEventType,
|
|
93
|
+
agent: str,
|
|
94
|
+
project_id: str,
|
|
95
|
+
memory_type: str,
|
|
96
|
+
memory_id: str,
|
|
97
|
+
payload: Dict[str, Any],
|
|
98
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
99
|
+
*,
|
|
100
|
+
default_metadata: bool = True,
|
|
101
|
+
) -> MemoryEvent:
|
|
102
|
+
"""
|
|
103
|
+
Factory function to create a MemoryEvent with current timestamp.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
event_type: Type of event
|
|
107
|
+
agent: Agent name
|
|
108
|
+
project_id: Project identifier
|
|
109
|
+
memory_type: Type of memory
|
|
110
|
+
memory_id: Memory identifier
|
|
111
|
+
payload: Event-specific data
|
|
112
|
+
metadata: Optional additional context
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
A new MemoryEvent instance
|
|
116
|
+
"""
|
|
117
|
+
return MemoryEvent(
|
|
118
|
+
event_type=event_type,
|
|
119
|
+
agent=agent,
|
|
120
|
+
project_id=project_id,
|
|
121
|
+
memory_type=memory_type,
|
|
122
|
+
memory_id=memory_id,
|
|
123
|
+
timestamp=datetime.now(timezone.utc),
|
|
124
|
+
payload=payload,
|
|
125
|
+
metadata=metadata if metadata is not None else {},
|
|
126
|
+
)
|