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
alma/events/webhook.py ADDED
@@ -0,0 +1,425 @@
1
+ """
2
+ ALMA Webhook Delivery.
3
+
4
+ Provides webhook delivery capabilities for external system integration.
5
+ Webhooks are delivered asynchronously with retry logic and signature
6
+ verification support.
7
+ """
8
+
9
+ import asyncio
10
+ import hashlib
11
+ import hmac
12
+ import json
13
+ import logging
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum
17
+
18
+ # TYPE_CHECKING import for forward references
19
+ from typing import TYPE_CHECKING, Dict, List, Optional
20
+
21
+ from alma.events.types import MemoryEvent, MemoryEventType
22
+
23
+ if TYPE_CHECKING:
24
+ from alma.events.emitter import EventEmitter
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Try to import aiohttp, provide fallback warning
29
+ try:
30
+ import aiohttp
31
+
32
+ AIOHTTP_AVAILABLE = True
33
+ except ImportError:
34
+ AIOHTTP_AVAILABLE = False
35
+ logger.warning("aiohttp not installed - webhook delivery will be unavailable")
36
+
37
+
38
+ class WebhookDeliveryStatus(Enum):
39
+ """Status of webhook delivery attempt."""
40
+
41
+ SUCCESS = "success"
42
+ FAILED = "failed"
43
+ RETRYING = "retrying"
44
+ PENDING = "pending"
45
+
46
+
47
+ @dataclass
48
+ class WebhookConfig:
49
+ """
50
+ Configuration for a webhook endpoint.
51
+
52
+ Attributes:
53
+ url: The URL to send webhook payloads to
54
+ events: List of event types to deliver (empty = all)
55
+ secret: Optional secret for HMAC signature generation
56
+ max_retries: Maximum number of retry attempts
57
+ timeout_seconds: Request timeout in seconds
58
+ headers: Optional additional headers to include
59
+ """
60
+
61
+ url: str
62
+ events: List[MemoryEventType] = field(default_factory=list)
63
+ secret: Optional[str] = None
64
+ max_retries: int = 3
65
+ timeout_seconds: int = 10
66
+ headers: Dict[str, str] = field(default_factory=dict)
67
+
68
+ def matches_event(self, event_type: MemoryEventType) -> bool:
69
+ """Check if this webhook should receive the given event type."""
70
+ if not self.events:
71
+ return True # Empty list means all events
72
+ return event_type in self.events
73
+
74
+
75
+ @dataclass
76
+ class WebhookDeliveryResult:
77
+ """Result of a webhook delivery attempt."""
78
+
79
+ config: WebhookConfig
80
+ event: MemoryEvent
81
+ status: WebhookDeliveryStatus
82
+ status_code: Optional[int] = None
83
+ attempts: int = 0
84
+ error: Optional[str] = None
85
+ response_body: Optional[str] = None
86
+
87
+
88
+ class WebhookDelivery:
89
+ """
90
+ Handles webhook delivery with retry logic and signature generation.
91
+
92
+ Features:
93
+ - HMAC-SHA256 signature for payload verification
94
+ - Exponential backoff retry logic
95
+ - Async delivery to avoid blocking
96
+ - Configurable timeouts
97
+
98
+ Example:
99
+ ```python
100
+ configs = [
101
+ WebhookConfig(
102
+ url="https://example.com/webhook",
103
+ events=[MemoryEventType.CREATED],
104
+ secret="my-secret"
105
+ )
106
+ ]
107
+ delivery = WebhookDelivery(configs)
108
+ results = await delivery.deliver(event)
109
+ ```
110
+ """
111
+
112
+ def __init__(self, configs: List[WebhookConfig]):
113
+ """
114
+ Initialize webhook delivery.
115
+
116
+ Args:
117
+ configs: List of webhook configurations
118
+ """
119
+ self.configs = configs
120
+
121
+ async def deliver(self, event: MemoryEvent) -> List[WebhookDeliveryResult]:
122
+ """
123
+ Deliver event to all matching webhooks.
124
+
125
+ Args:
126
+ event: The event to deliver
127
+
128
+ Returns:
129
+ List of delivery results for each webhook
130
+ """
131
+ if not AIOHTTP_AVAILABLE:
132
+ logger.error("Cannot deliver webhooks: aiohttp not installed")
133
+ return []
134
+
135
+ # Filter to matching webhooks
136
+ matching_configs = [
137
+ config for config in self.configs if config.matches_event(event.event_type)
138
+ ]
139
+
140
+ if not matching_configs:
141
+ return []
142
+
143
+ # Deliver to all matching webhooks concurrently
144
+ tasks = [self._send_webhook(config, event) for config in matching_configs]
145
+
146
+ results = await asyncio.gather(*tasks, return_exceptions=True)
147
+
148
+ # Handle any exceptions that occurred
149
+ delivery_results = []
150
+ for i, result in enumerate(results):
151
+ if isinstance(result, Exception):
152
+ delivery_results.append(
153
+ WebhookDeliveryResult(
154
+ config=matching_configs[i],
155
+ event=event,
156
+ status=WebhookDeliveryStatus.FAILED,
157
+ error=str(result),
158
+ )
159
+ )
160
+ else:
161
+ delivery_results.append(result)
162
+
163
+ return delivery_results
164
+
165
+ def _sign_payload(self, payload: str, secret: str) -> str:
166
+ """
167
+ Sign payload with HMAC-SHA256.
168
+
169
+ Args:
170
+ payload: JSON payload string
171
+ secret: Secret key for signing
172
+
173
+ Returns:
174
+ Hexadecimal signature string
175
+ """
176
+ return hmac.new(
177
+ secret.encode("utf-8"),
178
+ payload.encode("utf-8"),
179
+ hashlib.sha256,
180
+ ).hexdigest()
181
+
182
+ def _build_headers(
183
+ self,
184
+ config: WebhookConfig,
185
+ payload: str,
186
+ timestamp: int,
187
+ ) -> Dict[str, str]:
188
+ """Build headers for webhook request."""
189
+ headers = {
190
+ "Content-Type": "application/json",
191
+ "X-ALMA-Event-Type": payload, # Will be replaced with actual event type
192
+ "X-ALMA-Timestamp": str(timestamp),
193
+ **config.headers,
194
+ }
195
+
196
+ if config.secret:
197
+ # Create signature from timestamp + payload
198
+ signature_payload = f"{timestamp}.{payload}"
199
+ signature = self._sign_payload(signature_payload, config.secret)
200
+ headers["X-ALMA-Signature"] = f"sha256={signature}"
201
+
202
+ return headers
203
+
204
+ async def _send_webhook(
205
+ self,
206
+ config: WebhookConfig,
207
+ event: MemoryEvent,
208
+ ) -> WebhookDeliveryResult:
209
+ """
210
+ Send a single webhook with retry logic.
211
+
212
+ Uses exponential backoff for retries:
213
+ - Attempt 1: immediate
214
+ - Attempt 2: 1 second delay
215
+ - Attempt 3: 2 second delay
216
+ - Attempt 4: 4 second delay
217
+
218
+ Args:
219
+ config: Webhook configuration
220
+ event: Event to deliver
221
+
222
+ Returns:
223
+ Delivery result
224
+ """
225
+ payload = json.dumps(event.to_dict(), default=str)
226
+ timestamp = int(time.time())
227
+
228
+ headers = {
229
+ "Content-Type": "application/json",
230
+ "X-ALMA-Event-Type": event.event_type.value,
231
+ "X-ALMA-Timestamp": str(timestamp),
232
+ **config.headers,
233
+ }
234
+
235
+ if config.secret:
236
+ signature_payload = f"{timestamp}.{payload}"
237
+ signature = self._sign_payload(signature_payload, config.secret)
238
+ headers["X-ALMA-Signature"] = f"sha256={signature}"
239
+
240
+ attempts = 0
241
+ last_error = None
242
+ last_status_code = None
243
+
244
+ async with aiohttp.ClientSession() as session:
245
+ for attempt in range(config.max_retries + 1):
246
+ attempts = attempt + 1
247
+
248
+ try:
249
+ async with session.post(
250
+ config.url,
251
+ data=payload,
252
+ headers=headers,
253
+ timeout=aiohttp.ClientTimeout(total=config.timeout_seconds),
254
+ ) as response:
255
+ last_status_code = response.status
256
+
257
+ if 200 <= response.status < 300:
258
+ logger.info(
259
+ f"Webhook delivered successfully to {config.url} "
260
+ f"(attempt {attempts})"
261
+ )
262
+ return WebhookDeliveryResult(
263
+ config=config,
264
+ event=event,
265
+ status=WebhookDeliveryStatus.SUCCESS,
266
+ status_code=response.status,
267
+ attempts=attempts,
268
+ )
269
+
270
+ # Non-2xx response
271
+ response_body = await response.text()
272
+ last_error = f"HTTP {response.status}: {response_body[:200]}"
273
+ logger.warning(
274
+ f"Webhook delivery failed to {config.url}: {last_error}"
275
+ )
276
+
277
+ except asyncio.TimeoutError:
278
+ last_error = f"Timeout after {config.timeout_seconds} seconds"
279
+ logger.warning(
280
+ f"Webhook delivery timeout to {config.url} (attempt {attempts})"
281
+ )
282
+
283
+ except aiohttp.ClientError as e:
284
+ last_error = str(e)
285
+ logger.warning(
286
+ f"Webhook delivery error to {config.url}: {e} (attempt {attempts})"
287
+ )
288
+
289
+ # Calculate backoff for next attempt
290
+ if attempt < config.max_retries:
291
+ backoff = 2**attempt # 1, 2, 4 seconds
292
+ await asyncio.sleep(backoff)
293
+
294
+ # All retries exhausted
295
+ logger.error(
296
+ f"Webhook delivery failed after {attempts} attempts to {config.url}"
297
+ )
298
+ return WebhookDeliveryResult(
299
+ config=config,
300
+ event=event,
301
+ status=WebhookDeliveryStatus.FAILED,
302
+ status_code=last_status_code,
303
+ attempts=attempts,
304
+ error=last_error,
305
+ )
306
+
307
+ def add_config(self, config: WebhookConfig) -> None:
308
+ """Add a webhook configuration."""
309
+ self.configs.append(config)
310
+
311
+ def remove_config(self, url: str) -> bool:
312
+ """
313
+ Remove a webhook configuration by URL.
314
+
315
+ Args:
316
+ url: URL of the webhook to remove
317
+
318
+ Returns:
319
+ True if removed, False if not found
320
+ """
321
+ for i, config in enumerate(self.configs):
322
+ if config.url == url:
323
+ self.configs.pop(i)
324
+ return True
325
+ return False
326
+
327
+
328
+ class WebhookManager:
329
+ """
330
+ High-level manager for webhook delivery integrated with the event emitter.
331
+
332
+ Automatically subscribes to the event emitter and delivers webhooks
333
+ for configured events.
334
+
335
+ Example:
336
+ ```python
337
+ from alma.events import get_emitter
338
+ from alma.events.webhook import WebhookManager, WebhookConfig
339
+
340
+ manager = WebhookManager()
341
+ manager.add_webhook(WebhookConfig(
342
+ url="https://example.com/webhook",
343
+ secret="my-secret"
344
+ ))
345
+ manager.start(get_emitter())
346
+ ```
347
+ """
348
+
349
+ def __init__(self):
350
+ """Initialize the webhook manager."""
351
+ self._configs: List[WebhookConfig] = []
352
+ self._delivery: Optional[WebhookDelivery] = None
353
+ self._running = False
354
+
355
+ def add_webhook(self, config: WebhookConfig) -> None:
356
+ """Add a webhook configuration."""
357
+ self._configs.append(config)
358
+ if self._delivery:
359
+ self._delivery.add_config(config)
360
+
361
+ def remove_webhook(self, url: str) -> bool:
362
+ """Remove a webhook by URL."""
363
+ for i, config in enumerate(self._configs):
364
+ if config.url == url:
365
+ self._configs.pop(i)
366
+ if self._delivery:
367
+ self._delivery.remove_config(url)
368
+ return True
369
+ return False
370
+
371
+ def start(self, emitter: "EventEmitter") -> None:
372
+ """
373
+ Start the webhook manager.
374
+
375
+ Subscribes to all events from the emitter.
376
+
377
+ Args:
378
+ emitter: The event emitter to subscribe to
379
+ """
380
+
381
+ if self._running:
382
+ return
383
+
384
+ self._delivery = WebhookDelivery(self._configs)
385
+ emitter.subscribe_all(self._on_event)
386
+ self._running = True
387
+ logger.info(f"Webhook manager started with {len(self._configs)} webhooks")
388
+
389
+ def stop(self, emitter: "EventEmitter") -> None:
390
+ """
391
+ Stop the webhook manager.
392
+
393
+ Args:
394
+ emitter: The event emitter to unsubscribe from
395
+ """
396
+
397
+ if not self._running:
398
+ return
399
+
400
+ emitter.unsubscribe_all(self._on_event)
401
+ self._running = False
402
+ logger.info("Webhook manager stopped")
403
+
404
+ def _on_event(self, event: MemoryEvent) -> None:
405
+ """Handle incoming events by delivering webhooks."""
406
+ if not self._delivery or not AIOHTTP_AVAILABLE:
407
+ return
408
+
409
+ # Run async delivery in a new event loop if needed
410
+ try:
411
+ asyncio.get_running_loop()
412
+ asyncio.create_task(self._delivery.deliver(event))
413
+ except RuntimeError:
414
+ # No running event loop, create one
415
+ asyncio.run(self._delivery.deliver(event))
416
+
417
+ @property
418
+ def webhook_count(self) -> int:
419
+ """Get the number of configured webhooks."""
420
+ return len(self._configs)
421
+
422
+ @property
423
+ def is_running(self) -> bool:
424
+ """Check if the manager is running."""
425
+ return self._running
alma/exceptions.py ADDED
@@ -0,0 +1,49 @@
1
+ """
2
+ ALMA Exception Hierarchy
3
+
4
+ Custom exceptions for the ALMA memory system, providing clear error
5
+ categorization for configuration, storage, embedding, retrieval,
6
+ and extraction operations.
7
+ """
8
+
9
+
10
+ class ALMAError(Exception):
11
+ """Base exception for all ALMA errors."""
12
+
13
+ pass
14
+
15
+
16
+ class ConfigurationError(ALMAError):
17
+ """Raised when configuration is invalid or missing."""
18
+
19
+ pass
20
+
21
+
22
+ class ScopeViolationError(ALMAError):
23
+ """Raised when an agent attempts to learn outside its scope."""
24
+
25
+ pass
26
+
27
+
28
+ class StorageError(ALMAError):
29
+ """Raised when storage operations fail."""
30
+
31
+ pass
32
+
33
+
34
+ class EmbeddingError(ALMAError):
35
+ """Raised when embedding generation fails."""
36
+
37
+ pass
38
+
39
+
40
+ class RetrievalError(ALMAError):
41
+ """Raised when memory retrieval fails."""
42
+
43
+ pass
44
+
45
+
46
+ class ExtractionError(ALMAError):
47
+ """Raised when fact extraction fails."""
48
+
49
+ pass
@@ -0,0 +1,31 @@
1
+ """
2
+ ALMA Extraction Module.
3
+
4
+ LLM-powered and rule-based fact extraction from conversations.
5
+ """
6
+
7
+ from alma.extraction.auto_learner import (
8
+ AutoLearner,
9
+ add_auto_learning_to_alma,
10
+ )
11
+ from alma.extraction.extractor import (
12
+ ExtractedFact,
13
+ ExtractionResult,
14
+ FactExtractor,
15
+ FactType,
16
+ LLMFactExtractor,
17
+ RuleBasedExtractor,
18
+ create_extractor,
19
+ )
20
+
21
+ __all__ = [
22
+ "FactExtractor",
23
+ "LLMFactExtractor",
24
+ "RuleBasedExtractor",
25
+ "ExtractedFact",
26
+ "ExtractionResult",
27
+ "FactType",
28
+ "create_extractor",
29
+ "AutoLearner",
30
+ "add_auto_learning_to_alma",
31
+ ]