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.
- alma/__init__.py +33 -1
- alma/core.py +124 -16
- alma/extraction/auto_learner.py +4 -3
- alma/graph/__init__.py +26 -1
- alma/graph/backends/__init__.py +14 -0
- alma/graph/backends/kuzu.py +624 -0
- alma/graph/backends/memgraph.py +432 -0
- alma/integration/claude_agents.py +22 -10
- alma/learning/protocols.py +3 -3
- alma/mcp/tools.py +9 -11
- 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/retrieval/engine.py +65 -4
- alma/storage/__init__.py +29 -0
- alma/storage/azure_cosmos.py +343 -132
- alma/storage/base.py +58 -0
- alma/storage/constants.py +103 -0
- alma/storage/file_based.py +3 -8
- 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/postgresql.py +185 -78
- alma/storage/sqlite_local.py +149 -50
- alma/testing/__init__.py +46 -0
- alma/testing/factories.py +301 -0
- alma/testing/mocks.py +389 -0
- {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/METADATA +42 -8
- {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/RECORD +36 -19
- {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/WHEEL +0 -0
- {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/top_level.txt +0 -0
alma/storage/azure_cosmos.py
CHANGED
|
@@ -22,6 +22,7 @@ from datetime import datetime, timezone
|
|
|
22
22
|
from typing import Any, Dict, List, Optional
|
|
23
23
|
|
|
24
24
|
from alma.storage.base import StorageBackend
|
|
25
|
+
from alma.storage.constants import AZURE_COSMOS_CONTAINER_NAMES, MemoryType
|
|
25
26
|
from alma.types import (
|
|
26
27
|
AntiPattern,
|
|
27
28
|
DomainKnowledge,
|
|
@@ -61,21 +62,19 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
61
62
|
- DiskANN vector indexing for similarity search
|
|
62
63
|
- Partition key: project_id for efficient queries
|
|
63
64
|
|
|
64
|
-
Container structure:
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
65
|
+
Container structure (uses canonical memory type names with alma_ prefix):
|
|
66
|
+
- alma_heuristics: Heuristics with vector embeddings
|
|
67
|
+
- alma_outcomes: Task outcomes with vector embeddings
|
|
68
|
+
- alma_preferences: User preferences (no vectors)
|
|
69
|
+
- alma_domain_knowledge: Domain knowledge with vector embeddings
|
|
70
|
+
- alma_anti_patterns: Anti-patterns with vector embeddings
|
|
71
|
+
|
|
72
|
+
Container names are derived from alma.storage.constants.AZURE_COSMOS_CONTAINER_NAMES
|
|
73
|
+
for consistency across all storage backends.
|
|
70
74
|
"""
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"outcomes": "alma-outcomes",
|
|
75
|
-
"preferences": "alma-preferences",
|
|
76
|
-
"knowledge": "alma-knowledge",
|
|
77
|
-
"antipatterns": "alma-antipatterns",
|
|
78
|
-
}
|
|
76
|
+
# Use canonical container names from constants
|
|
77
|
+
CONTAINER_NAMES = AZURE_COSMOS_CONTAINER_NAMES
|
|
79
78
|
|
|
80
79
|
def __init__(
|
|
81
80
|
self,
|
|
@@ -121,6 +120,14 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
121
120
|
container_name
|
|
122
121
|
)
|
|
123
122
|
|
|
123
|
+
# Cache for partition key mappings: {container_key: {doc_id: partition_key}}
|
|
124
|
+
# This reduces RU consumption by avoiding cross-partition queries
|
|
125
|
+
self._partition_key_cache: Dict[str, Dict[str, str]] = {
|
|
126
|
+
mt: {} for mt in MemoryType.ALL
|
|
127
|
+
}
|
|
128
|
+
# Maximum cache size per container to prevent memory issues
|
|
129
|
+
self._cache_max_size = 1000
|
|
130
|
+
|
|
124
131
|
logger.info(f"Connected to Azure Cosmos DB: {database_name}")
|
|
125
132
|
|
|
126
133
|
@classmethod
|
|
@@ -146,29 +153,29 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
146
153
|
|
|
147
154
|
def _init_containers(self):
|
|
148
155
|
"""Initialize containers with vector search indexing."""
|
|
149
|
-
# Container configs with indexing policies
|
|
156
|
+
# Container configs with indexing policies (using canonical memory types)
|
|
150
157
|
container_configs = {
|
|
151
|
-
|
|
158
|
+
MemoryType.HEURISTICS: {
|
|
152
159
|
"partition_key": "/project_id",
|
|
153
160
|
"vector_path": "/embedding",
|
|
154
161
|
"vector_indexes": True,
|
|
155
162
|
},
|
|
156
|
-
|
|
163
|
+
MemoryType.OUTCOMES: {
|
|
157
164
|
"partition_key": "/project_id",
|
|
158
165
|
"vector_path": "/embedding",
|
|
159
166
|
"vector_indexes": True,
|
|
160
167
|
},
|
|
161
|
-
|
|
168
|
+
MemoryType.PREFERENCES: {
|
|
162
169
|
"partition_key": "/user_id",
|
|
163
170
|
"vector_path": None,
|
|
164
171
|
"vector_indexes": False,
|
|
165
172
|
},
|
|
166
|
-
|
|
173
|
+
MemoryType.DOMAIN_KNOWLEDGE: {
|
|
167
174
|
"partition_key": "/project_id",
|
|
168
175
|
"vector_path": "/embedding",
|
|
169
176
|
"vector_indexes": True,
|
|
170
177
|
},
|
|
171
|
-
|
|
178
|
+
MemoryType.ANTI_PATTERNS: {
|
|
172
179
|
"partition_key": "/project_id",
|
|
173
180
|
"vector_path": "/embedding",
|
|
174
181
|
"vector_indexes": True,
|
|
@@ -231,11 +238,123 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
231
238
|
"""Get container client by key."""
|
|
232
239
|
return self._containers[container_key]
|
|
233
240
|
|
|
241
|
+
def _cache_partition_key(
|
|
242
|
+
self, container_key: str, doc_id: str, partition_key: str
|
|
243
|
+
) -> None:
|
|
244
|
+
"""
|
|
245
|
+
Cache the partition key mapping for a document.
|
|
246
|
+
|
|
247
|
+
This enables point reads for future operations, reducing RU consumption
|
|
248
|
+
by avoiding expensive cross-partition queries.
|
|
249
|
+
"""
|
|
250
|
+
cache = self._partition_key_cache[container_key]
|
|
251
|
+
|
|
252
|
+
# Evict oldest entries if cache is full (simple FIFO eviction)
|
|
253
|
+
if len(cache) >= self._cache_max_size:
|
|
254
|
+
# Remove first 10% of entries
|
|
255
|
+
keys_to_remove = list(cache.keys())[: self._cache_max_size // 10]
|
|
256
|
+
for key in keys_to_remove:
|
|
257
|
+
del cache[key]
|
|
258
|
+
|
|
259
|
+
cache[doc_id] = partition_key
|
|
260
|
+
|
|
261
|
+
def _get_cached_partition_key(
|
|
262
|
+
self, container_key: str, doc_id: str
|
|
263
|
+
) -> Optional[str]:
|
|
264
|
+
"""
|
|
265
|
+
Get cached partition key for a document if available.
|
|
266
|
+
|
|
267
|
+
Returns None if the partition key is not cached.
|
|
268
|
+
"""
|
|
269
|
+
return self._partition_key_cache.get(container_key, {}).get(doc_id)
|
|
270
|
+
|
|
271
|
+
def _invalidate_partition_key_cache(self, container_key: str, doc_id: str) -> None:
|
|
272
|
+
"""Remove a document from the partition key cache."""
|
|
273
|
+
cache = self._partition_key_cache.get(container_key, {})
|
|
274
|
+
cache.pop(doc_id, None)
|
|
275
|
+
|
|
276
|
+
def _point_read_document(
|
|
277
|
+
self,
|
|
278
|
+
container_key: str,
|
|
279
|
+
doc_id: str,
|
|
280
|
+
partition_key: Optional[str] = None,
|
|
281
|
+
) -> Optional[Dict[str, Any]]:
|
|
282
|
+
"""
|
|
283
|
+
Attempt to read a document using a point read (1 RU) instead of a query.
|
|
284
|
+
|
|
285
|
+
If partition_key is provided, performs a direct point read.
|
|
286
|
+
If partition_key is not provided but is cached, uses the cached value.
|
|
287
|
+
If neither is available, falls back to a cross-partition query.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
container_key: The container key (e.g., 'heuristics', 'knowledge')
|
|
291
|
+
doc_id: The document ID
|
|
292
|
+
partition_key: Optional partition key for direct point read
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
The document if found, None otherwise
|
|
296
|
+
"""
|
|
297
|
+
container = self._get_container(container_key)
|
|
298
|
+
|
|
299
|
+
# Try to get partition key from cache if not provided
|
|
300
|
+
if partition_key is None:
|
|
301
|
+
partition_key = self._get_cached_partition_key(container_key, doc_id)
|
|
302
|
+
|
|
303
|
+
# If we have a partition key, use point read (1 RU)
|
|
304
|
+
if partition_key is not None:
|
|
305
|
+
try:
|
|
306
|
+
doc = container.read_item(item=doc_id, partition_key=partition_key)
|
|
307
|
+
# Refresh cache on successful read
|
|
308
|
+
self._cache_partition_key(container_key, doc_id, partition_key)
|
|
309
|
+
return doc
|
|
310
|
+
except exceptions.CosmosResourceNotFoundError:
|
|
311
|
+
# Document not found or partition key was wrong
|
|
312
|
+
self._invalidate_partition_key_cache(container_key, doc_id)
|
|
313
|
+
# Fall through to cross-partition query
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.warning(f"Point read failed for {doc_id}: {e}")
|
|
316
|
+
# Fall through to cross-partition query
|
|
317
|
+
|
|
318
|
+
# Fallback: Cross-partition query (expensive but necessary without partition key)
|
|
319
|
+
logger.debug(
|
|
320
|
+
f"Using cross-partition query for {doc_id} in {container_key} "
|
|
321
|
+
"(consider providing project_id for better performance)"
|
|
322
|
+
)
|
|
323
|
+
query = "SELECT * FROM c WHERE c.id = @id"
|
|
324
|
+
items = list(
|
|
325
|
+
container.query_items(
|
|
326
|
+
query=query,
|
|
327
|
+
parameters=[{"name": "@id", "value": doc_id}],
|
|
328
|
+
enable_cross_partition_query=True,
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if items:
|
|
333
|
+
doc = items[0]
|
|
334
|
+
# Cache the partition key for future operations
|
|
335
|
+
pk_field = self._get_partition_key_field(container_key)
|
|
336
|
+
if pk_field and pk_field in doc:
|
|
337
|
+
self._cache_partition_key(container_key, doc_id, doc[pk_field])
|
|
338
|
+
return doc
|
|
339
|
+
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
def _get_partition_key_field(self, container_key: str) -> Optional[str]:
|
|
343
|
+
"""Get the partition key field name for a container."""
|
|
344
|
+
partition_key_fields = {
|
|
345
|
+
MemoryType.HEURISTICS: "project_id",
|
|
346
|
+
MemoryType.OUTCOMES: "project_id",
|
|
347
|
+
MemoryType.PREFERENCES: "user_id",
|
|
348
|
+
MemoryType.DOMAIN_KNOWLEDGE: "project_id",
|
|
349
|
+
MemoryType.ANTI_PATTERNS: "project_id",
|
|
350
|
+
}
|
|
351
|
+
return partition_key_fields.get(container_key)
|
|
352
|
+
|
|
234
353
|
# ==================== WRITE OPERATIONS ====================
|
|
235
354
|
|
|
236
355
|
def save_heuristic(self, heuristic: Heuristic) -> str:
|
|
237
356
|
"""Save a heuristic."""
|
|
238
|
-
container = self._get_container(
|
|
357
|
+
container = self._get_container(MemoryType.HEURISTICS)
|
|
239
358
|
|
|
240
359
|
doc = {
|
|
241
360
|
"id": heuristic.id,
|
|
@@ -260,12 +379,16 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
260
379
|
}
|
|
261
380
|
|
|
262
381
|
container.upsert_item(doc)
|
|
382
|
+
# Cache partition key for efficient future updates
|
|
383
|
+
self._cache_partition_key(
|
|
384
|
+
MemoryType.HEURISTICS, heuristic.id, heuristic.project_id
|
|
385
|
+
)
|
|
263
386
|
logger.debug(f"Saved heuristic: {heuristic.id}")
|
|
264
387
|
return heuristic.id
|
|
265
388
|
|
|
266
389
|
def save_outcome(self, outcome: Outcome) -> str:
|
|
267
390
|
"""Save an outcome."""
|
|
268
|
-
container = self._get_container(
|
|
391
|
+
container = self._get_container(MemoryType.OUTCOMES)
|
|
269
392
|
|
|
270
393
|
doc = {
|
|
271
394
|
"id": outcome.id,
|
|
@@ -285,12 +408,14 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
285
408
|
}
|
|
286
409
|
|
|
287
410
|
container.upsert_item(doc)
|
|
411
|
+
# Cache partition key for efficient future updates
|
|
412
|
+
self._cache_partition_key(MemoryType.OUTCOMES, outcome.id, outcome.project_id)
|
|
288
413
|
logger.debug(f"Saved outcome: {outcome.id}")
|
|
289
414
|
return outcome.id
|
|
290
415
|
|
|
291
416
|
def save_user_preference(self, preference: UserPreference) -> str:
|
|
292
417
|
"""Save a user preference."""
|
|
293
|
-
container = self._get_container(
|
|
418
|
+
container = self._get_container(MemoryType.PREFERENCES)
|
|
294
419
|
|
|
295
420
|
doc = {
|
|
296
421
|
"id": preference.id,
|
|
@@ -307,12 +432,16 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
307
432
|
}
|
|
308
433
|
|
|
309
434
|
container.upsert_item(doc)
|
|
435
|
+
# Cache partition key for efficient future updates
|
|
436
|
+
self._cache_partition_key(
|
|
437
|
+
MemoryType.PREFERENCES, preference.id, preference.user_id
|
|
438
|
+
)
|
|
310
439
|
logger.debug(f"Saved preference: {preference.id}")
|
|
311
440
|
return preference.id
|
|
312
441
|
|
|
313
442
|
def save_domain_knowledge(self, knowledge: DomainKnowledge) -> str:
|
|
314
443
|
"""Save domain knowledge."""
|
|
315
|
-
container = self._get_container(
|
|
444
|
+
container = self._get_container(MemoryType.DOMAIN_KNOWLEDGE)
|
|
316
445
|
|
|
317
446
|
doc = {
|
|
318
447
|
"id": knowledge.id,
|
|
@@ -331,12 +460,16 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
331
460
|
}
|
|
332
461
|
|
|
333
462
|
container.upsert_item(doc)
|
|
463
|
+
# Cache partition key for efficient future updates
|
|
464
|
+
self._cache_partition_key(
|
|
465
|
+
MemoryType.DOMAIN_KNOWLEDGE, knowledge.id, knowledge.project_id
|
|
466
|
+
)
|
|
334
467
|
logger.debug(f"Saved domain knowledge: {knowledge.id}")
|
|
335
468
|
return knowledge.id
|
|
336
469
|
|
|
337
470
|
def save_anti_pattern(self, anti_pattern: AntiPattern) -> str:
|
|
338
471
|
"""Save an anti-pattern."""
|
|
339
|
-
container = self._get_container(
|
|
472
|
+
container = self._get_container(MemoryType.ANTI_PATTERNS)
|
|
340
473
|
|
|
341
474
|
doc = {
|
|
342
475
|
"id": anti_pattern.id,
|
|
@@ -358,6 +491,10 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
358
491
|
}
|
|
359
492
|
|
|
360
493
|
container.upsert_item(doc)
|
|
494
|
+
# Cache partition key for efficient future updates
|
|
495
|
+
self._cache_partition_key(
|
|
496
|
+
MemoryType.ANTI_PATTERNS, anti_pattern.id, anti_pattern.project_id
|
|
497
|
+
)
|
|
361
498
|
logger.debug(f"Saved anti-pattern: {anti_pattern.id}")
|
|
362
499
|
return anti_pattern.id
|
|
363
500
|
|
|
@@ -372,7 +509,7 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
372
509
|
min_confidence: float = 0.0,
|
|
373
510
|
) -> List[Heuristic]:
|
|
374
511
|
"""Get heuristics with optional vector search."""
|
|
375
|
-
container = self._get_container(
|
|
512
|
+
container = self._get_container(MemoryType.HEURISTICS)
|
|
376
513
|
|
|
377
514
|
if embedding:
|
|
378
515
|
# Vector search query
|
|
@@ -424,6 +561,12 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
424
561
|
)
|
|
425
562
|
)
|
|
426
563
|
|
|
564
|
+
# Cache partition keys for efficient future updates
|
|
565
|
+
for doc in items:
|
|
566
|
+
self._cache_partition_key(
|
|
567
|
+
MemoryType.HEURISTICS, doc["id"], doc["project_id"]
|
|
568
|
+
)
|
|
569
|
+
|
|
427
570
|
return [self._doc_to_heuristic(doc) for doc in items]
|
|
428
571
|
|
|
429
572
|
def get_outcomes(
|
|
@@ -436,7 +579,7 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
436
579
|
success_only: bool = False,
|
|
437
580
|
) -> List[Outcome]:
|
|
438
581
|
"""Get outcomes with optional vector search."""
|
|
439
|
-
container = self._get_container(
|
|
582
|
+
container = self._get_container(MemoryType.OUTCOMES)
|
|
440
583
|
|
|
441
584
|
if embedding:
|
|
442
585
|
# Vector search query
|
|
@@ -494,6 +637,10 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
494
637
|
)
|
|
495
638
|
)
|
|
496
639
|
|
|
640
|
+
# Cache partition keys for efficient future updates
|
|
641
|
+
for doc in items:
|
|
642
|
+
self._cache_partition_key(MemoryType.OUTCOMES, doc["id"], doc["project_id"])
|
|
643
|
+
|
|
497
644
|
return [self._doc_to_outcome(doc) for doc in items]
|
|
498
645
|
|
|
499
646
|
def get_user_preferences(
|
|
@@ -502,7 +649,7 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
502
649
|
category: Optional[str] = None,
|
|
503
650
|
) -> List[UserPreference]:
|
|
504
651
|
"""Get user preferences."""
|
|
505
|
-
container = self._get_container(
|
|
652
|
+
container = self._get_container(MemoryType.PREFERENCES)
|
|
506
653
|
|
|
507
654
|
query = "SELECT * FROM c WHERE c.user_id = @user_id"
|
|
508
655
|
parameters = [{"name": "@user_id", "value": user_id}]
|
|
@@ -520,6 +667,10 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
520
667
|
)
|
|
521
668
|
)
|
|
522
669
|
|
|
670
|
+
# Cache partition keys for efficient future updates
|
|
671
|
+
for doc in items:
|
|
672
|
+
self._cache_partition_key(MemoryType.PREFERENCES, doc["id"], doc["user_id"])
|
|
673
|
+
|
|
523
674
|
return [self._doc_to_preference(doc) for doc in items]
|
|
524
675
|
|
|
525
676
|
def get_domain_knowledge(
|
|
@@ -531,7 +682,7 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
531
682
|
top_k: int = 5,
|
|
532
683
|
) -> List[DomainKnowledge]:
|
|
533
684
|
"""Get domain knowledge with optional vector search."""
|
|
534
|
-
container = self._get_container(
|
|
685
|
+
container = self._get_container(MemoryType.DOMAIN_KNOWLEDGE)
|
|
535
686
|
|
|
536
687
|
if embedding:
|
|
537
688
|
query = """
|
|
@@ -583,6 +734,12 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
583
734
|
)
|
|
584
735
|
)
|
|
585
736
|
|
|
737
|
+
# Cache partition keys for efficient future updates
|
|
738
|
+
for doc in items:
|
|
739
|
+
self._cache_partition_key(
|
|
740
|
+
MemoryType.DOMAIN_KNOWLEDGE, doc["id"], doc["project_id"]
|
|
741
|
+
)
|
|
742
|
+
|
|
586
743
|
return [self._doc_to_domain_knowledge(doc) for doc in items]
|
|
587
744
|
|
|
588
745
|
def get_anti_patterns(
|
|
@@ -593,7 +750,7 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
593
750
|
top_k: int = 5,
|
|
594
751
|
) -> List[AntiPattern]:
|
|
595
752
|
"""Get anti-patterns with optional vector search."""
|
|
596
|
-
container = self._get_container(
|
|
753
|
+
container = self._get_container(MemoryType.ANTI_PATTERNS)
|
|
597
754
|
|
|
598
755
|
if embedding:
|
|
599
756
|
query = """
|
|
@@ -639,6 +796,12 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
639
796
|
)
|
|
640
797
|
)
|
|
641
798
|
|
|
799
|
+
# Cache partition keys for efficient future updates
|
|
800
|
+
for doc in items:
|
|
801
|
+
self._cache_partition_key(
|
|
802
|
+
MemoryType.ANTI_PATTERNS, doc["id"], doc["project_id"]
|
|
803
|
+
)
|
|
804
|
+
|
|
642
805
|
return [self._doc_to_anti_pattern(doc) for doc in items]
|
|
643
806
|
|
|
644
807
|
# ==================== UPDATE OPERATIONS ====================
|
|
@@ -647,26 +810,28 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
647
810
|
self,
|
|
648
811
|
heuristic_id: str,
|
|
649
812
|
updates: Dict[str, Any],
|
|
813
|
+
project_id: Optional[str] = None,
|
|
650
814
|
) -> bool:
|
|
651
|
-
"""
|
|
652
|
-
|
|
815
|
+
"""
|
|
816
|
+
Update a heuristic's fields.
|
|
653
817
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
818
|
+
Args:
|
|
819
|
+
heuristic_id: The heuristic document ID
|
|
820
|
+
updates: Dictionary of fields to update
|
|
821
|
+
project_id: Optional partition key for efficient point read (1 RU).
|
|
822
|
+
If not provided, will attempt cache lookup, then
|
|
823
|
+
fall back to cross-partition query (more expensive).
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
True if update succeeded, False if document not found
|
|
827
|
+
"""
|
|
828
|
+
container = self._get_container(MemoryType.HEURISTICS)
|
|
664
829
|
|
|
665
|
-
|
|
666
|
-
|
|
830
|
+
# Use optimized point read with cache fallback
|
|
831
|
+
doc = self._point_read_document(MemoryType.HEURISTICS, heuristic_id, project_id)
|
|
667
832
|
|
|
668
|
-
|
|
669
|
-
|
|
833
|
+
if not doc:
|
|
834
|
+
return False
|
|
670
835
|
|
|
671
836
|
# Apply updates
|
|
672
837
|
for key, value in updates.items():
|
|
@@ -682,24 +847,29 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
682
847
|
self,
|
|
683
848
|
heuristic_id: str,
|
|
684
849
|
success: bool,
|
|
850
|
+
project_id: Optional[str] = None,
|
|
685
851
|
) -> bool:
|
|
686
|
-
"""
|
|
687
|
-
|
|
852
|
+
"""
|
|
853
|
+
Increment heuristic occurrence count.
|
|
688
854
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
855
|
+
Args:
|
|
856
|
+
heuristic_id: The heuristic document ID
|
|
857
|
+
success: Whether this occurrence was successful
|
|
858
|
+
project_id: Optional partition key for efficient point read (1 RU).
|
|
859
|
+
If not provided, will attempt cache lookup, then
|
|
860
|
+
fall back to cross-partition query (more expensive).
|
|
861
|
+
|
|
862
|
+
Returns:
|
|
863
|
+
True if update succeeded, False if document not found
|
|
864
|
+
"""
|
|
865
|
+
container = self._get_container(MemoryType.HEURISTICS)
|
|
866
|
+
|
|
867
|
+
# Use optimized point read with cache fallback
|
|
868
|
+
doc = self._point_read_document(MemoryType.HEURISTICS, heuristic_id, project_id)
|
|
698
869
|
|
|
699
|
-
if not
|
|
870
|
+
if not doc:
|
|
700
871
|
return False
|
|
701
872
|
|
|
702
|
-
doc = items[0]
|
|
703
873
|
doc["occurrence_count"] = doc.get("occurrence_count", 0) + 1
|
|
704
874
|
if success:
|
|
705
875
|
doc["success_count"] = doc.get("success_count", 0) + 1
|
|
@@ -712,30 +882,34 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
712
882
|
self,
|
|
713
883
|
heuristic_id: str,
|
|
714
884
|
new_confidence: float,
|
|
885
|
+
project_id: Optional[str] = None,
|
|
715
886
|
) -> bool:
|
|
716
887
|
"""
|
|
717
888
|
Update confidence score for a heuristic.
|
|
718
889
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
890
|
+
Args:
|
|
891
|
+
heuristic_id: The heuristic document ID
|
|
892
|
+
new_confidence: The new confidence value
|
|
893
|
+
project_id: Optional partition key for efficient point read (1 RU).
|
|
894
|
+
If not provided, will attempt cache lookup, then
|
|
895
|
+
fall back to cross-partition query (more expensive).
|
|
896
|
+
|
|
897
|
+
Returns:
|
|
898
|
+
True if update succeeded, False if document not found
|
|
899
|
+
|
|
900
|
+
Performance Note:
|
|
901
|
+
- With project_id: 1 RU for point read + write cost
|
|
902
|
+
- With cached partition key: 1 RU for point read + write cost
|
|
903
|
+
- Without either: Cross-partition query (variable, higher RUs)
|
|
722
904
|
"""
|
|
723
|
-
container = self._get_container(
|
|
905
|
+
container = self._get_container(MemoryType.HEURISTICS)
|
|
724
906
|
|
|
725
|
-
#
|
|
726
|
-
|
|
727
|
-
items = list(
|
|
728
|
-
container.query_items(
|
|
729
|
-
query=query,
|
|
730
|
-
parameters=[{"name": "@id", "value": heuristic_id}],
|
|
731
|
-
enable_cross_partition_query=True,
|
|
732
|
-
)
|
|
733
|
-
)
|
|
907
|
+
# Use optimized point read with cache fallback
|
|
908
|
+
doc = self._point_read_document(MemoryType.HEURISTICS, heuristic_id, project_id)
|
|
734
909
|
|
|
735
|
-
if not
|
|
910
|
+
if not doc:
|
|
736
911
|
return False
|
|
737
912
|
|
|
738
|
-
doc = items[0]
|
|
739
913
|
doc["confidence"] = new_confidence
|
|
740
914
|
|
|
741
915
|
container.replace_item(item=heuristic_id, body=doc)
|
|
@@ -748,30 +922,36 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
748
922
|
self,
|
|
749
923
|
knowledge_id: str,
|
|
750
924
|
new_confidence: float,
|
|
925
|
+
project_id: Optional[str] = None,
|
|
751
926
|
) -> bool:
|
|
752
927
|
"""
|
|
753
928
|
Update confidence score for domain knowledge.
|
|
754
929
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
930
|
+
Args:
|
|
931
|
+
knowledge_id: The knowledge document ID
|
|
932
|
+
new_confidence: The new confidence value
|
|
933
|
+
project_id: Optional partition key for efficient point read (1 RU).
|
|
934
|
+
If not provided, will attempt cache lookup, then
|
|
935
|
+
fall back to cross-partition query (more expensive).
|
|
936
|
+
|
|
937
|
+
Returns:
|
|
938
|
+
True if update succeeded, False if document not found
|
|
939
|
+
|
|
940
|
+
Performance Note:
|
|
941
|
+
- With project_id: 1 RU for point read + write cost
|
|
942
|
+
- With cached partition key: 1 RU for point read + write cost
|
|
943
|
+
- Without either: Cross-partition query (variable, higher RUs)
|
|
758
944
|
"""
|
|
759
|
-
container = self._get_container(
|
|
945
|
+
container = self._get_container(MemoryType.DOMAIN_KNOWLEDGE)
|
|
760
946
|
|
|
761
|
-
#
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
container.query_items(
|
|
765
|
-
query=query,
|
|
766
|
-
parameters=[{"name": "@id", "value": knowledge_id}],
|
|
767
|
-
enable_cross_partition_query=True,
|
|
768
|
-
)
|
|
947
|
+
# Use optimized point read with cache fallback
|
|
948
|
+
doc = self._point_read_document(
|
|
949
|
+
MemoryType.DOMAIN_KNOWLEDGE, knowledge_id, project_id
|
|
769
950
|
)
|
|
770
951
|
|
|
771
|
-
if not
|
|
952
|
+
if not doc:
|
|
772
953
|
return False
|
|
773
954
|
|
|
774
|
-
doc = items[0]
|
|
775
955
|
doc["confidence"] = new_confidence
|
|
776
956
|
|
|
777
957
|
container.replace_item(item=knowledge_id, body=doc)
|
|
@@ -789,7 +969,7 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
789
969
|
agent: Optional[str] = None,
|
|
790
970
|
) -> int:
|
|
791
971
|
"""Delete old outcomes."""
|
|
792
|
-
container = self._get_container(
|
|
972
|
+
container = self._get_container(MemoryType.OUTCOMES)
|
|
793
973
|
|
|
794
974
|
query = """
|
|
795
975
|
SELECT c.id FROM c
|
|
@@ -832,7 +1012,7 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
832
1012
|
agent: Optional[str] = None,
|
|
833
1013
|
) -> int:
|
|
834
1014
|
"""Delete low-confidence heuristics."""
|
|
835
|
-
container = self._get_container(
|
|
1015
|
+
container = self._get_container(MemoryType.HEURISTICS)
|
|
836
1016
|
|
|
837
1017
|
query = """
|
|
838
1018
|
SELECT c.id FROM c
|
|
@@ -868,27 +1048,59 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
868
1048
|
logger.info(f"Deleted {deleted} low-confidence heuristics")
|
|
869
1049
|
return deleted
|
|
870
1050
|
|
|
871
|
-
def delete_heuristic(
|
|
872
|
-
|
|
873
|
-
|
|
1051
|
+
def delete_heuristic(
|
|
1052
|
+
self, heuristic_id: str, project_id: Optional[str] = None
|
|
1053
|
+
) -> bool:
|
|
1054
|
+
"""
|
|
1055
|
+
Delete a specific heuristic by ID.
|
|
874
1056
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1057
|
+
Args:
|
|
1058
|
+
heuristic_id: The heuristic document ID
|
|
1059
|
+
project_id: Optional partition key for efficient point read (1 RU).
|
|
1060
|
+
If not provided, will attempt cache lookup, then
|
|
1061
|
+
fall back to cross-partition query (more expensive).
|
|
1062
|
+
|
|
1063
|
+
Returns:
|
|
1064
|
+
True if deletion succeeded, False if document not found
|
|
1065
|
+
"""
|
|
1066
|
+
container = self._get_container(MemoryType.HEURISTICS)
|
|
1067
|
+
|
|
1068
|
+
# Try to get partition key from cache if not provided
|
|
1069
|
+
if project_id is None:
|
|
1070
|
+
project_id = self._get_cached_partition_key(
|
|
1071
|
+
MemoryType.HEURISTICS, heuristic_id
|
|
882
1072
|
)
|
|
1073
|
+
|
|
1074
|
+
# If we have a partition key, try direct delete
|
|
1075
|
+
if project_id is not None:
|
|
1076
|
+
try:
|
|
1077
|
+
container.delete_item(item=heuristic_id, partition_key=project_id)
|
|
1078
|
+
self._invalidate_partition_key_cache(
|
|
1079
|
+
MemoryType.HEURISTICS, heuristic_id
|
|
1080
|
+
)
|
|
1081
|
+
return True
|
|
1082
|
+
except exceptions.CosmosResourceNotFoundError:
|
|
1083
|
+
# Document not found or partition key was wrong
|
|
1084
|
+
self._invalidate_partition_key_cache(
|
|
1085
|
+
MemoryType.HEURISTICS, heuristic_id
|
|
1086
|
+
)
|
|
1087
|
+
# Fall through to cross-partition lookup
|
|
1088
|
+
|
|
1089
|
+
# Fallback: Cross-partition query to find the document
|
|
1090
|
+
logger.debug(
|
|
1091
|
+
f"Using cross-partition query for delete {heuristic_id} "
|
|
1092
|
+
"(consider providing project_id for better performance)"
|
|
883
1093
|
)
|
|
1094
|
+
doc = self._point_read_document(MemoryType.HEURISTICS, heuristic_id, None)
|
|
884
1095
|
|
|
885
|
-
if not
|
|
1096
|
+
if not doc:
|
|
886
1097
|
return False
|
|
887
1098
|
|
|
888
|
-
project_id =
|
|
1099
|
+
project_id = doc["project_id"]
|
|
889
1100
|
|
|
890
1101
|
try:
|
|
891
1102
|
container.delete_item(item=heuristic_id, partition_key=project_id)
|
|
1103
|
+
self._invalidate_partition_key_cache(MemoryType.HEURISTICS, heuristic_id)
|
|
892
1104
|
return True
|
|
893
1105
|
except exceptions.CosmosResourceNotFoundError:
|
|
894
1106
|
return False
|
|
@@ -908,39 +1120,38 @@ class AzureCosmosStorage(StorageBackend):
|
|
|
908
1120
|
"database": self.database_name,
|
|
909
1121
|
}
|
|
910
1122
|
|
|
911
|
-
# Count items in each container
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
container = self._get_container(key)
|
|
915
|
-
query = "SELECT VALUE COUNT(1) FROM c WHERE c.project_id = @project_id"
|
|
916
|
-
parameters = [{"name": "@project_id", "value": project_id}]
|
|
917
|
-
|
|
918
|
-
if agent and key != "preferences":
|
|
919
|
-
query = """
|
|
920
|
-
SELECT VALUE COUNT(1) FROM c
|
|
921
|
-
WHERE c.project_id = @project_id AND c.agent = @agent
|
|
922
|
-
"""
|
|
923
|
-
parameters.append({"name": "@agent", "value": agent})
|
|
1123
|
+
# Count items in each container using canonical memory types
|
|
1124
|
+
for memory_type in MemoryType.ALL:
|
|
1125
|
+
container = self._get_container(memory_type)
|
|
924
1126
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1127
|
+
if memory_type == MemoryType.PREFERENCES:
|
|
1128
|
+
# Preferences use user_id, not project_id
|
|
1129
|
+
result = list(
|
|
1130
|
+
container.query_items(
|
|
1131
|
+
query="SELECT VALUE COUNT(1) FROM c",
|
|
1132
|
+
enable_cross_partition_query=True,
|
|
1133
|
+
)
|
|
931
1134
|
)
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1135
|
+
else:
|
|
1136
|
+
query = "SELECT VALUE COUNT(1) FROM c WHERE c.project_id = @project_id"
|
|
1137
|
+
parameters = [{"name": "@project_id", "value": project_id}]
|
|
1138
|
+
|
|
1139
|
+
if agent:
|
|
1140
|
+
query = """
|
|
1141
|
+
SELECT VALUE COUNT(1) FROM c
|
|
1142
|
+
WHERE c.project_id = @project_id AND c.agent = @agent
|
|
1143
|
+
"""
|
|
1144
|
+
parameters.append({"name": "@agent", "value": agent})
|
|
1145
|
+
|
|
1146
|
+
result = list(
|
|
1147
|
+
container.query_items(
|
|
1148
|
+
query=query,
|
|
1149
|
+
parameters=parameters,
|
|
1150
|
+
enable_cross_partition_query=False,
|
|
1151
|
+
partition_key=project_id,
|
|
1152
|
+
)
|
|
1153
|
+
)
|
|
1154
|
+
stats[f"{memory_type}_count"] = result[0] if result else 0
|
|
944
1155
|
|
|
945
1156
|
stats["total_count"] = sum(
|
|
946
1157
|
stats.get(k, 0) for k in stats if k.endswith("_count")
|