alma-memory 0.5.0__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. alma/__init__.py +33 -1
  2. alma/core.py +124 -16
  3. alma/extraction/auto_learner.py +4 -3
  4. alma/graph/__init__.py +26 -1
  5. alma/graph/backends/__init__.py +14 -0
  6. alma/graph/backends/kuzu.py +624 -0
  7. alma/graph/backends/memgraph.py +432 -0
  8. alma/integration/claude_agents.py +22 -10
  9. alma/learning/protocols.py +3 -3
  10. alma/mcp/tools.py +9 -11
  11. alma/observability/__init__.py +84 -0
  12. alma/observability/config.py +302 -0
  13. alma/observability/logging.py +424 -0
  14. alma/observability/metrics.py +583 -0
  15. alma/observability/tracing.py +440 -0
  16. alma/retrieval/engine.py +65 -4
  17. alma/storage/__init__.py +29 -0
  18. alma/storage/azure_cosmos.py +343 -132
  19. alma/storage/base.py +58 -0
  20. alma/storage/constants.py +103 -0
  21. alma/storage/file_based.py +3 -8
  22. alma/storage/migrations/__init__.py +21 -0
  23. alma/storage/migrations/base.py +321 -0
  24. alma/storage/migrations/runner.py +323 -0
  25. alma/storage/migrations/version_stores.py +337 -0
  26. alma/storage/migrations/versions/__init__.py +11 -0
  27. alma/storage/migrations/versions/v1_0_0.py +373 -0
  28. alma/storage/postgresql.py +185 -78
  29. alma/storage/sqlite_local.py +149 -50
  30. alma/testing/__init__.py +46 -0
  31. alma/testing/factories.py +301 -0
  32. alma/testing/mocks.py +389 -0
  33. {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/METADATA +42 -8
  34. {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/RECORD +36 -19
  35. {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/WHEEL +0 -0
  36. {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/top_level.txt +0 -0
@@ -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
- - alma-heuristics: Heuristics with vector embeddings
66
- - alma-outcomes: Task outcomes with vector embeddings
67
- - alma-preferences: User preferences (no vectors)
68
- - alma-knowledge: Domain knowledge with vector embeddings
69
- - alma-antipatterns: Anti-patterns with vector embeddings
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
- CONTAINER_NAMES = {
73
- "heuristics": "alma-heuristics",
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
- "heuristics": {
158
+ MemoryType.HEURISTICS: {
152
159
  "partition_key": "/project_id",
153
160
  "vector_path": "/embedding",
154
161
  "vector_indexes": True,
155
162
  },
156
- "outcomes": {
163
+ MemoryType.OUTCOMES: {
157
164
  "partition_key": "/project_id",
158
165
  "vector_path": "/embedding",
159
166
  "vector_indexes": True,
160
167
  },
161
- "preferences": {
168
+ MemoryType.PREFERENCES: {
162
169
  "partition_key": "/user_id",
163
170
  "vector_path": None,
164
171
  "vector_indexes": False,
165
172
  },
166
- "knowledge": {
173
+ MemoryType.DOMAIN_KNOWLEDGE: {
167
174
  "partition_key": "/project_id",
168
175
  "vector_path": "/embedding",
169
176
  "vector_indexes": True,
170
177
  },
171
- "antipatterns": {
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("heuristics")
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("outcomes")
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("preferences")
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("knowledge")
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("antipatterns")
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("heuristics")
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("outcomes")
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("preferences")
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("knowledge")
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("antipatterns")
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
- """Update a heuristic's fields."""
652
- container = self._get_container("heuristics")
815
+ """
816
+ Update a heuristic's fields.
653
817
 
654
- # We need project_id to read the item (partition key)
655
- # First try to find the heuristic
656
- query = "SELECT * FROM c WHERE c.id = @id"
657
- items = list(
658
- container.query_items(
659
- query=query,
660
- parameters=[{"name": "@id", "value": heuristic_id}],
661
- enable_cross_partition_query=True,
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
- if not items:
666
- return False
830
+ # Use optimized point read with cache fallback
831
+ doc = self._point_read_document(MemoryType.HEURISTICS, heuristic_id, project_id)
667
832
 
668
- doc = items[0]
669
- doc["project_id"]
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
- """Increment heuristic occurrence count."""
687
- container = self._get_container("heuristics")
852
+ """
853
+ Increment heuristic occurrence count.
688
854
 
689
- # Find the heuristic
690
- query = "SELECT * FROM c WHERE c.id = @id"
691
- items = list(
692
- container.query_items(
693
- query=query,
694
- parameters=[{"name": "@id", "value": heuristic_id}],
695
- enable_cross_partition_query=True,
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 items:
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
- Note: This requires a cross-partition query since we only have the ID.
720
- For better performance, consider using update_heuristic() with the
721
- project_id if available, which enables point reads.
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("heuristics")
905
+ container = self._get_container(MemoryType.HEURISTICS)
724
906
 
725
- # Find the heuristic (cross-partition query required without project_id)
726
- query = "SELECT * FROM c WHERE c.id = @id"
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 items:
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
- Note: This requires a cross-partition query since we only have the ID.
756
- For better performance when project_id is known, fetch the document
757
- directly using point read and update via save_domain_knowledge().
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("knowledge")
945
+ container = self._get_container(MemoryType.DOMAIN_KNOWLEDGE)
760
946
 
761
- # Find the knowledge item (cross-partition query required without project_id)
762
- query = "SELECT * FROM c WHERE c.id = @id"
763
- items = list(
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 items:
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("outcomes")
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("heuristics")
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(self, heuristic_id: str) -> bool:
872
- """Delete a specific heuristic by ID."""
873
- container = self._get_container("heuristics")
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
- # Find the heuristic to get project_id
876
- query = "SELECT c.project_id FROM c WHERE c.id = @id"
877
- items = list(
878
- container.query_items(
879
- query=query,
880
- parameters=[{"name": "@id", "value": heuristic_id}],
881
- enable_cross_partition_query=True,
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 items:
1096
+ if not doc:
886
1097
  return False
887
1098
 
888
- project_id = items[0]["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
- container_keys = ["heuristics", "outcomes", "knowledge", "antipatterns"]
913
- for key in container_keys:
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
- result = list(
926
- container.query_items(
927
- query=query,
928
- parameters=parameters,
929
- enable_cross_partition_query=False,
930
- partition_key=project_id,
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
- stats[f"{key}_count"] = result[0] if result else 0
934
-
935
- # Preferences count (no project_id filter)
936
- container = self._get_container("preferences")
937
- result = list(
938
- container.query_items(
939
- query="SELECT VALUE COUNT(1) FROM c",
940
- enable_cross_partition_query=True,
941
- )
942
- )
943
- stats["preferences_count"] = result[0] if result else 0
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")