bedrock-agentcore-starter-toolkit 0.1.12__py3-none-any.whl → 0.1.14__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.

Potentially problematic release.


This version of bedrock-agentcore-starter-toolkit might be problematic. Click here for more details.

Files changed (25) hide show
  1. bedrock_agentcore_starter_toolkit/cli/import_agent/agent_info.py +6 -1
  2. bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +88 -1
  3. bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +53 -0
  4. bedrock_agentcore_starter_toolkit/notebook/runtime/bedrock_agentcore.py +3 -0
  5. bedrock_agentcore_starter_toolkit/operations/memory/__init__.py +1 -0
  6. bedrock_agentcore_starter_toolkit/operations/memory/constants.py +98 -0
  7. bedrock_agentcore_starter_toolkit/operations/memory/manager.py +890 -0
  8. bedrock_agentcore_starter_toolkit/operations/memory/models/DictWrapper.py +51 -0
  9. bedrock_agentcore_starter_toolkit/operations/memory/models/Memory.py +17 -0
  10. bedrock_agentcore_starter_toolkit/operations/memory/models/MemoryStrategy.py +17 -0
  11. bedrock_agentcore_starter_toolkit/operations/memory/models/MemorySummary.py +17 -0
  12. bedrock_agentcore_starter_toolkit/operations/runtime/configure.py +28 -0
  13. bedrock_agentcore_starter_toolkit/operations/runtime/create_role.py +3 -2
  14. bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +8 -2
  15. bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +1 -0
  16. bedrock_agentcore_starter_toolkit/services/codebuild.py +17 -6
  17. bedrock_agentcore_starter_toolkit/services/runtime.py +71 -5
  18. bedrock_agentcore_starter_toolkit/utils/runtime/schema.py +1 -0
  19. bedrock_agentcore_starter_toolkit/utils/runtime/templates/Dockerfile.j2 +25 -11
  20. {bedrock_agentcore_starter_toolkit-0.1.12.dist-info → bedrock_agentcore_starter_toolkit-0.1.14.dist-info}/METADATA +3 -4
  21. {bedrock_agentcore_starter_toolkit-0.1.12.dist-info → bedrock_agentcore_starter_toolkit-0.1.14.dist-info}/RECORD +25 -18
  22. {bedrock_agentcore_starter_toolkit-0.1.12.dist-info → bedrock_agentcore_starter_toolkit-0.1.14.dist-info}/WHEEL +0 -0
  23. {bedrock_agentcore_starter_toolkit-0.1.12.dist-info → bedrock_agentcore_starter_toolkit-0.1.14.dist-info}/entry_points.txt +0 -0
  24. {bedrock_agentcore_starter_toolkit-0.1.12.dist-info → bedrock_agentcore_starter_toolkit-0.1.14.dist-info}/licenses/LICENSE.txt +0 -0
  25. {bedrock_agentcore_starter_toolkit-0.1.12.dist-info → bedrock_agentcore_starter_toolkit-0.1.14.dist-info}/licenses/NOTICE.txt +0 -0
@@ -0,0 +1,890 @@
1
+ """Memory Manager for AgentCore Memory resources."""
2
+
3
+ import copy
4
+ import logging
5
+ import time
6
+ import uuid
7
+ import warnings
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import boto3
11
+ from botocore.exceptions import ClientError
12
+
13
+ from .constants import DEFAULT_NAMESPACES, MemoryStatus, MemoryStrategyStatus, OverrideType, StrategyType
14
+ from .models.Memory import Memory
15
+ from .models.MemoryStrategy import MemoryStrategy
16
+ from .models.MemorySummary import MemorySummary
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class MemoryManager:
22
+ """A high-level client for managing the lifecycle of AgentCore Memory resources.
23
+
24
+ This class handles all CONTROL PLANE CRUD operations.
25
+ """
26
+
27
+ def __init__(self, region_name: str):
28
+ """Initialize MemoryManager with AWS region.
29
+
30
+ Args:
31
+ region_name: AWS region name for the bedrock-agentcore-control client.
32
+ """
33
+ self.region_name = region_name
34
+ self._control_plane_client = boto3.client("bedrock-agentcore-control", region_name=region_name)
35
+
36
+ # AgentCore Memory control plane methods
37
+ self._ALLOWED_CONTROL_PLANE_METHODS = {
38
+ "create_memory",
39
+ "list_memories",
40
+ "update_memory",
41
+ "delete_memory",
42
+ }
43
+ logger.info("✅ MemoryManager initialized for region: %s", region_name)
44
+
45
+ def __getattr__(self, name: str):
46
+ """Dynamically forward method calls to the appropriate boto3 client.
47
+
48
+ This method enables access to all control_plane boto3 client methods without explicitly
49
+ defining them. Methods are looked up in the following order:
50
+ _control_plane_client (bedrock-agentcore-control) - for control plane operations
51
+
52
+ Args:
53
+ name: The method name being accessed
54
+
55
+ Returns:
56
+ A callable method from the control_plane boto3 client
57
+
58
+ Raises:
59
+ AttributeError: If the method doesn't exist on control_plane_client
60
+
61
+ Example:
62
+ # Access any boto3 method directly
63
+ manager = MemoryManager(region_name="us-east-1")
64
+
65
+ # These calls are forwarded to the appropriate boto3 functions
66
+ response = manager.list_memories()
67
+ memory = manager.get_memory(memoryId="mem-123")
68
+ """
69
+ if name in self._ALLOWED_CONTROL_PLANE_METHODS and hasattr(self._control_plane_client, name):
70
+ method = getattr(self._control_plane_client, name)
71
+ logger.debug("Forwarding method '%s' to control_plane_client", name)
72
+ return method
73
+
74
+ # Method not found on client
75
+ raise AttributeError(
76
+ f"'{self.__class__.__name__}' object has no attribute '{name}'. "
77
+ f"Method not found on control_plane_client. "
78
+ f"Available methods can be found in the boto3 documentation for "
79
+ f"'bedrock-agentcore-control' services."
80
+ )
81
+
82
+ def _validate_namespace(self, namespace: str) -> bool:
83
+ """Validate namespace format - basic check only."""
84
+ # Only check for template variables in namespace definition
85
+ if "{" in namespace and not (
86
+ "{actorId}" in namespace or "{sessionId}" in namespace or "{strategyId}" in namespace
87
+ ):
88
+ logger.warning("Namespace with templates should contain valid variables: %s", namespace)
89
+
90
+ return True
91
+
92
+ def _validate_strategy_config(self, strategy: Dict[str, Any], strategy_type: str) -> None:
93
+ """Validate strategy configuration parameters."""
94
+ strategy_config = strategy[strategy_type]
95
+
96
+ namespaces = strategy_config.get("namespaces", [])
97
+ for namespace in namespaces:
98
+ self._validate_namespace(namespace)
99
+
100
+ def _wrap_configuration(
101
+ self, config: Dict[str, Any], strategy_type: str, override_type: Optional[str] = None
102
+ ) -> Dict[str, Any]:
103
+ """Wrap configuration based on strategy type using new enum methods."""
104
+ wrapped_config = {}
105
+
106
+ if "extraction" in config:
107
+ extraction = config["extraction"]
108
+
109
+ if any(key in extraction for key in ["triggerEveryNMessages", "historicalContextWindowSize"]):
110
+ if strategy_type == "SEMANTIC":
111
+ wrapper_key = StrategyType.SEMANTIC.extraction_wrapper_key()
112
+ if wrapper_key:
113
+ wrapped_config["extraction"] = {wrapper_key: extraction}
114
+ elif strategy_type == "USER_PREFERENCE":
115
+ wrapper_key = StrategyType.USER_PREFERENCE.extraction_wrapper_key()
116
+ if wrapper_key:
117
+ wrapped_config["extraction"] = {wrapper_key: extraction}
118
+ elif strategy_type == "CUSTOM" and override_type:
119
+ override_enum = OverrideType(override_type)
120
+ wrapper_key = override_enum.extraction_wrapper_key()
121
+ if wrapper_key and override_type in ["SEMANTIC_OVERRIDE", "USER_PREFERENCE_OVERRIDE"]:
122
+ wrapped_config["extraction"] = {"customExtractionConfiguration": {wrapper_key: extraction}}
123
+ else:
124
+ wrapped_config["extraction"] = extraction
125
+
126
+ if "consolidation" in config:
127
+ consolidation = config["consolidation"]
128
+
129
+ raw_keys = ["triggerEveryNMessages", "appendToPrompt", "modelId"]
130
+ if any(key in consolidation for key in raw_keys):
131
+ if strategy_type == "SUMMARIZATION":
132
+ wrapper_key = StrategyType.SUMMARY.consolidation_wrapper_key()
133
+ if wrapper_key and "triggerEveryNMessages" in consolidation:
134
+ wrapped_config["consolidation"] = {
135
+ wrapper_key: {"triggerEveryNMessages": consolidation["triggerEveryNMessages"]}
136
+ }
137
+ elif strategy_type == "CUSTOM" and override_type:
138
+ override_enum = OverrideType(override_type)
139
+ wrapper_key = override_enum.consolidation_wrapper_key()
140
+ if wrapper_key:
141
+ wrapped_config["consolidation"] = {
142
+ "customConsolidationConfiguration": {wrapper_key: consolidation}
143
+ }
144
+ else:
145
+ wrapped_config["consolidation"] = consolidation
146
+
147
+ return wrapped_config
148
+
149
+ def _create_memory(
150
+ self,
151
+ name: str,
152
+ strategies: Optional[List[Dict[str, Any]]] = None,
153
+ description: Optional[str] = None,
154
+ event_expiry_days: int = 90,
155
+ memory_execution_role_arn: Optional[str] = None,
156
+ ) -> Memory:
157
+ """Create a memory resource and return the raw response.
158
+
159
+ Maps to: bedrock-agentcore-control.create_memory.
160
+ """
161
+ if strategies is None:
162
+ strategies = []
163
+
164
+ try:
165
+ processed_strategies = self._add_default_namespaces(strategies)
166
+
167
+ params = {
168
+ "name": name,
169
+ "eventExpiryDuration": event_expiry_days,
170
+ "memoryStrategies": processed_strategies,
171
+ "clientToken": str(uuid.uuid4()),
172
+ }
173
+
174
+ if description is not None:
175
+ params["description"] = description
176
+
177
+ if memory_execution_role_arn is not None:
178
+ params["memoryExecutionRoleArn"] = memory_execution_role_arn
179
+
180
+ response = self._control_plane_client.create_memory(**params)
181
+
182
+ memory = response["memory"]
183
+
184
+ # Handle field name normalization
185
+ memory_id = memory.get("id", memory.get("memoryId", "unknown"))
186
+ logger.info("Created memory: %s", memory_id)
187
+ return Memory(memory)
188
+
189
+ except ClientError as e:
190
+ logger.error("Failed to create memory: %s", e)
191
+ raise
192
+
193
+ def _create_memory_and_wait(
194
+ self,
195
+ name: str,
196
+ strategies: List[Dict[str, Any]],
197
+ description: Optional[str] = None,
198
+ event_expiry_days: int = 90,
199
+ memory_execution_role_arn: Optional[str] = None,
200
+ max_wait: int = 300,
201
+ poll_interval: int = 10,
202
+ ) -> Memory:
203
+ """Create a memory and wait for it to become ACTIVE.
204
+
205
+ This method creates a memory and polls until it reaches ACTIVE status,
206
+ providing a convenient way to ensure the memory is ready for use.
207
+
208
+ Args:
209
+ name: Name for the memory resource
210
+ strategies: List of strategy configurations
211
+ description: Optional description
212
+ event_expiry_days: How long to retain events (default: 90 days)
213
+ memory_execution_role_arn: IAM role ARN for memory execution
214
+ max_wait: Maximum seconds to wait (default: 300)
215
+ poll_interval: Seconds between status checks (default: 10)
216
+
217
+ Returns:
218
+ Created memory object in ACTIVE status
219
+
220
+ Raises:
221
+ TimeoutError: If memory doesn't become ACTIVE within max_wait
222
+ RuntimeError: If memory creation fails
223
+ """
224
+ # Create the memory
225
+ memory = self._create_memory(
226
+ name=name,
227
+ strategies=strategies,
228
+ description=description,
229
+ event_expiry_days=event_expiry_days,
230
+ memory_execution_role_arn=memory_execution_role_arn,
231
+ )
232
+
233
+ memory_id = memory.id
234
+ if memory_id is None:
235
+ memory_id = ""
236
+ logger.info("Created memory %s, waiting for ACTIVE status...", memory_id)
237
+
238
+ start_time = time.time()
239
+ while time.time() - start_time < max_wait:
240
+ elapsed = int(time.time() - start_time)
241
+
242
+ try:
243
+ status = self.get_memory_status(memory_id)
244
+
245
+ if status == MemoryStatus.ACTIVE.value:
246
+ logger.info("Memory %s is now ACTIVE (took %d seconds)", memory_id, elapsed)
247
+ return memory
248
+ elif status == MemoryStatus.FAILED.value:
249
+ # Get failure reason if available
250
+ response = self._control_plane_client.get_memory(memoryId=memory_id)
251
+ failure_reason = response["memory"].get("failureReason", "Unknown")
252
+ raise RuntimeError("Memory creation failed: %s" % failure_reason)
253
+ else:
254
+ logger.debug("Memory status: %s (%d seconds elapsed)", status, elapsed)
255
+
256
+ except ClientError as e:
257
+ logger.error("Error checking memory status: %s", e)
258
+ raise
259
+
260
+ time.sleep(poll_interval)
261
+
262
+ raise TimeoutError(f"Memory {memory_id} did not become ACTIVE within {max_wait} seconds")
263
+
264
+ def create_memory_and_wait(
265
+ self,
266
+ name: str,
267
+ strategies: List[Dict[str, Any]],
268
+ description: Optional[str] = None,
269
+ event_expiry_days: int = 90,
270
+ memory_execution_role_arn: Optional[str] = None,
271
+ max_wait: int = 300,
272
+ poll_interval: int = 10,
273
+ ) -> Memory:
274
+ """Create a memory and wait for it to become ACTIVE - public method."""
275
+ return self._create_memory_and_wait(
276
+ name=name,
277
+ strategies=strategies,
278
+ description=description,
279
+ event_expiry_days=event_expiry_days,
280
+ memory_execution_role_arn=memory_execution_role_arn,
281
+ max_wait=max_wait,
282
+ poll_interval=poll_interval,
283
+ )
284
+
285
+ def get_or_create_memory(
286
+ self,
287
+ name: str,
288
+ strategies: Optional[List[Dict[str, Any]]] = None,
289
+ description: Optional[str] = None,
290
+ event_expiry_days: int = 90,
291
+ memory_execution_role_arn: Optional[str] = None,
292
+ ) -> Memory:
293
+ """Fetch an existing memory resource or create the memory.
294
+
295
+ Returns:
296
+ Memory object, either newly created or existing
297
+ """
298
+ memory: Memory = None
299
+ try:
300
+ memory_summaries = self.list_memories()
301
+ memory_summary = next((m for m in memory_summaries if m.id.startswith(f"{name}-")), None)
302
+
303
+ # Create Memory if it doesn't exist
304
+ if memory_summary is None:
305
+ memory = self._create_memory_and_wait(
306
+ name=name,
307
+ strategies=strategies,
308
+ description=description,
309
+ event_expiry_days=event_expiry_days,
310
+ memory_execution_role_arn=memory_execution_role_arn,
311
+ )
312
+ else:
313
+ logger.info("Memory already exists. Using existing memory ID: %s", memory_summary.id)
314
+ memory = self.get_memory(memory_summary.id)
315
+ return memory
316
+ except ClientError as e:
317
+ # Failed to create memory
318
+ logger.error("ClientError: Failed to create or get memory: %s", e)
319
+ raise
320
+ except Exception:
321
+ raise
322
+
323
+ def get_memory(self, memory_id: str) -> Memory:
324
+ """Retrieves an existing memory resource as a Memory object.
325
+
326
+ Maps to: bedrock-agentcore-control.get_memory.
327
+ """
328
+ logger.info("🔎 Retrieving memory resource with ID: %s...", memory_id)
329
+ try:
330
+ response = self._control_plane_client.get_memory(memoryId=memory_id).get("memory", {})
331
+ logger.info(" ✅ Found memory: %s", memory_id)
332
+ return Memory(response)
333
+ except ClientError as e:
334
+ logger.error(" ❌ Error retrieving memory: %s", e)
335
+ raise
336
+
337
+ def get_memory_status(self, memory_id: str) -> str:
338
+ """Get current memory status."""
339
+ try:
340
+ response = self._control_plane_client.get_memory(memoryId=memory_id)
341
+ return response["memory"]["status"]
342
+ except ClientError as e:
343
+ logger.error(" ❌ Error retrieving memory status: %s", e)
344
+ raise
345
+
346
+ def get_memory_strategies(self, memory_id: str) -> List[MemoryStrategy]:
347
+ """Get all strategies for a memory."""
348
+ try:
349
+ response = self._control_plane_client.get_memory(memoryId=memory_id)
350
+ memory = response["memory"]
351
+
352
+ # Handle both old and new field names in response
353
+ strategies = memory.get("strategies", memory.get("memoryStrategies", []))
354
+ return [MemoryStrategy(strategy) for strategy in strategies]
355
+ except ClientError as e:
356
+ logger.error("Failed to get memory strategies: %s", e)
357
+ raise
358
+
359
+ def list_memories(self, max_results: int = 100) -> list[MemorySummary]:
360
+ """Lists all available memory resources.
361
+
362
+ Maps to: bedrock-agentcore-control.list_memories.
363
+ """
364
+ try:
365
+ # Ensure max_results doesn't exceed API limit per request
366
+ results_per_request = min(max_results, 100)
367
+
368
+ response = self._control_plane_client.list_memories(maxResults=results_per_request)
369
+ memory_summaries = response.get("memories", [])
370
+
371
+ next_token = response.get("nextToken")
372
+ while next_token and len(memory_summaries) < max_results:
373
+ remaining = max_results - len(memory_summaries)
374
+ results_per_request = min(remaining, 100)
375
+
376
+ response = self._control_plane_client.list_memories(
377
+ maxResults=results_per_request, nextToken=next_token
378
+ )
379
+ memory_summaries.extend(response.get("memories", []))
380
+ next_token = response.get("nextToken")
381
+
382
+ # Normalize field names for backward compatibility
383
+ for memory_summary in memory_summaries:
384
+ if "memoryId" in memory_summary and "id" not in memory_summary:
385
+ memory_summary["id"] = memory_summary["memoryId"]
386
+ elif "id" in memory_summary and "memoryId" not in memory_summary:
387
+ memory_summary["memoryId"] = memory_summary["id"]
388
+
389
+ response = [MemorySummary(memory_summary=memory_summary) for memory_summary in memory_summaries]
390
+ return response
391
+
392
+ except ClientError as e:
393
+ logger.error(" ❌ Error listing memories: %s", e)
394
+ raise
395
+
396
+ def delete_memory(self, memory_id: str) -> Dict[str, Any]:
397
+ """Delete a memory resource.
398
+
399
+ Maps to: bedrock-agentcore-control.delete_memory.
400
+ """
401
+ try:
402
+ response = self._control_plane_client.delete_memory(memoryId=memory_id, clientToken=str(uuid.uuid4()))
403
+ logger.info("Deleted memory: %s", memory_id)
404
+ return response
405
+ except ClientError as e:
406
+ logger.error(" ❌ Error deleting memory: %s", e)
407
+ raise
408
+
409
+ def delete_memory_and_wait(self, memory_id: str, max_wait: int = 300, poll_interval: int = 10) -> Dict[str, Any]:
410
+ """Delete a memory and wait for deletion to complete.
411
+
412
+ This method deletes a memory and polls until it's fully deleted,
413
+ ensuring clean resource cleanup.
414
+
415
+ Args:
416
+ memory_id: Memory resource ID to delete
417
+ max_wait: Maximum seconds to wait (default: 300)
418
+ poll_interval: Seconds between checks (default: 10)
419
+
420
+ Returns:
421
+ Final deletion response
422
+
423
+ Raises:
424
+ TimeoutError: If deletion doesn't complete within max_wait
425
+ """
426
+ # Initiate deletion
427
+ response = self.delete_memory(memory_id)
428
+ logger.info("Initiated deletion of memory %s", memory_id)
429
+
430
+ start_time = time.time()
431
+ while time.time() - start_time < max_wait:
432
+ elapsed = int(time.time() - start_time)
433
+
434
+ try:
435
+ # Try to get the memory - if it doesn't exist, deletion is complete
436
+ self._control_plane_client.get_memory(memoryId=memory_id)
437
+ logger.debug("Memory still exists, waiting... (%d seconds elapsed)", elapsed)
438
+
439
+ except ClientError as e:
440
+ if e.response["Error"]["Code"] == "ResourceNotFoundException":
441
+ logger.info("Memory %s successfully deleted (took %d seconds)", memory_id, elapsed)
442
+ return response
443
+ else:
444
+ logger.error("Error checking memory status: %s", e)
445
+ raise
446
+
447
+ time.sleep(poll_interval)
448
+
449
+ raise TimeoutError("Memory %s was not deleted within %d seconds" % (memory_id, max_wait))
450
+
451
+ def add_semantic_strategy(
452
+ self,
453
+ memory_id: str,
454
+ name: str,
455
+ description: Optional[str] = None,
456
+ namespaces: Optional[List[str]] = None,
457
+ ) -> Memory:
458
+ """Add a semantic memory strategy.
459
+
460
+ Note: Configuration is no longer provided for built-in strategies as per API changes.
461
+ """
462
+ strategy: Dict = {
463
+ StrategyType.SEMANTIC.value: {
464
+ "name": name,
465
+ }
466
+ }
467
+
468
+ if description:
469
+ strategy[StrategyType.SEMANTIC.value]["description"] = description
470
+ if namespaces:
471
+ strategy[StrategyType.SEMANTIC.value]["namespaces"] = namespaces
472
+
473
+ return self._add_strategy(memory_id, strategy)
474
+
475
+ def add_semantic_strategy_and_wait(
476
+ self,
477
+ memory_id: str,
478
+ name: str,
479
+ description: Optional[str] = None,
480
+ namespaces: Optional[List[str]] = None,
481
+ max_wait: int = 300,
482
+ poll_interval: int = 10,
483
+ ) -> Memory:
484
+ """Add a semantic strategy and wait for memory to return to ACTIVE state.
485
+
486
+ This addresses the issue where adding a strategy puts the memory into
487
+ CREATING state temporarily, preventing subsequent operations.
488
+ """
489
+ # Add the strategy
490
+ self.add_semantic_strategy(memory_id, name, description, namespaces)
491
+
492
+ # Wait for memory to return to ACTIVE
493
+ return self._wait_for_memory_active(memory_id, max_wait, poll_interval)
494
+
495
+ def add_summary_strategy(
496
+ self,
497
+ memory_id: str,
498
+ name: str,
499
+ description: Optional[str] = None,
500
+ namespaces: Optional[List[str]] = None,
501
+ ) -> Memory:
502
+ """Add a summary memory strategy.
503
+
504
+ Note: Configuration is no longer provided for built-in strategies as per API changes.
505
+ """
506
+ strategy: Dict = {
507
+ StrategyType.SUMMARY.value: {
508
+ "name": name,
509
+ }
510
+ }
511
+
512
+ if description:
513
+ strategy[StrategyType.SUMMARY.value]["description"] = description
514
+ if namespaces:
515
+ strategy[StrategyType.SUMMARY.value]["namespaces"] = namespaces
516
+
517
+ return self._add_strategy(memory_id, strategy)
518
+
519
+ def add_summary_strategy_and_wait(
520
+ self,
521
+ memory_id: str,
522
+ name: str,
523
+ description: Optional[str] = None,
524
+ namespaces: Optional[List[str]] = None,
525
+ max_wait: int = 300,
526
+ poll_interval: int = 10,
527
+ ) -> Memory:
528
+ """Add a summary strategy and wait for memory to return to ACTIVE state."""
529
+ self.add_summary_strategy(memory_id, name, description, namespaces)
530
+ return self._wait_for_memory_active(memory_id, max_wait, poll_interval)
531
+
532
+ def add_user_preference_strategy(
533
+ self,
534
+ memory_id: str,
535
+ name: str,
536
+ description: Optional[str] = None,
537
+ namespaces: Optional[List[str]] = None,
538
+ ) -> Memory:
539
+ """Add a user preference memory strategy.
540
+
541
+ Note: Configuration is no longer provided for built-in strategies as per API changes.
542
+ """
543
+ strategy: Dict = {
544
+ StrategyType.USER_PREFERENCE.value: {
545
+ "name": name,
546
+ }
547
+ }
548
+
549
+ if description:
550
+ strategy[StrategyType.USER_PREFERENCE.value]["description"] = description
551
+ if namespaces:
552
+ strategy[StrategyType.USER_PREFERENCE.value]["namespaces"] = namespaces
553
+
554
+ return self._add_strategy(memory_id, strategy)
555
+
556
+ def add_user_preference_strategy_and_wait(
557
+ self,
558
+ memory_id: str,
559
+ name: str,
560
+ description: Optional[str] = None,
561
+ namespaces: Optional[List[str]] = None,
562
+ max_wait: int = 300,
563
+ poll_interval: int = 10,
564
+ ) -> Memory:
565
+ """Add a user preference strategy and wait for memory to return to ACTIVE state."""
566
+ self.add_user_preference_strategy(memory_id, name, description, namespaces)
567
+ return self._wait_for_memory_active(memory_id, max_wait, poll_interval)
568
+
569
+ def add_custom_semantic_strategy(
570
+ self,
571
+ memory_id: str,
572
+ name: str,
573
+ extraction_config: Dict[str, Any],
574
+ consolidation_config: Dict[str, Any],
575
+ description: Optional[str] = None,
576
+ namespaces: Optional[List[str]] = None,
577
+ ) -> Memory:
578
+ """Add a custom semantic strategy with prompts.
579
+
580
+ Args:
581
+ memory_id: Memory resource ID
582
+ name: Strategy name
583
+ extraction_config: Extraction configuration with prompt and model:
584
+ {"prompt": "...", "modelId": "..."}
585
+ consolidation_config: Consolidation configuration with prompt and model:
586
+ {"prompt": "...", "modelId": "..."}
587
+ description: Optional description
588
+ namespaces: Optional namespaces list
589
+ """
590
+ strategy = {
591
+ StrategyType.CUSTOM.value: {
592
+ "name": name,
593
+ "configuration": {
594
+ "semanticOverride": {
595
+ "extraction": {
596
+ "appendToPrompt": extraction_config["prompt"],
597
+ "modelId": extraction_config["modelId"],
598
+ },
599
+ "consolidation": {
600
+ "appendToPrompt": consolidation_config["prompt"],
601
+ "modelId": consolidation_config["modelId"],
602
+ },
603
+ }
604
+ },
605
+ }
606
+ }
607
+
608
+ if description:
609
+ strategy[StrategyType.CUSTOM.value]["description"] = description
610
+ if namespaces:
611
+ strategy[StrategyType.CUSTOM.value]["namespaces"] = namespaces
612
+
613
+ return self._add_strategy(memory_id, strategy)
614
+
615
+ def add_custom_semantic_strategy_and_wait(
616
+ self,
617
+ memory_id: str,
618
+ name: str,
619
+ extraction_config: Dict[str, Any],
620
+ consolidation_config: Dict[str, Any],
621
+ description: Optional[str] = None,
622
+ namespaces: Optional[List[str]] = None,
623
+ max_wait: int = 300,
624
+ poll_interval: int = 10,
625
+ ) -> Memory:
626
+ """Add a custom semantic strategy and wait for memory to return to ACTIVE state."""
627
+ self.add_custom_semantic_strategy(
628
+ memory_id, name, extraction_config, consolidation_config, description, namespaces
629
+ )
630
+ return self._wait_for_memory_active(memory_id, max_wait, poll_interval)
631
+
632
+ def modify_strategy(
633
+ self,
634
+ memory_id: str,
635
+ strategy_id: str,
636
+ description: Optional[str] = None,
637
+ namespaces: Optional[List[str]] = None,
638
+ configuration: Optional[Dict[str, Any]] = None,
639
+ ) -> Memory:
640
+ """Modify a strategy with full control over configuration."""
641
+ modify_config: Dict = {"strategyId": strategy_id}
642
+
643
+ if description is not None:
644
+ modify_config["description"] = description
645
+ if namespaces is not None:
646
+ modify_config["namespaces"] = namespaces
647
+ if configuration is not None:
648
+ modify_config["configuration"] = configuration
649
+
650
+ return self.update_memory_strategies(memory_id=memory_id, modify_strategies=[modify_config])
651
+
652
+ def delete_strategy(self, memory_id: str, strategy_id: str) -> Memory:
653
+ """Delete a strategy from a memory."""
654
+ return self.update_memory_strategies(memory_id=memory_id, delete_strategy_ids=[strategy_id])
655
+
656
+ def update_memory_strategies(
657
+ self,
658
+ memory_id: str,
659
+ add_strategies: Optional[List[Dict[str, Any]]] = None,
660
+ modify_strategies: Optional[List[Dict[str, Any]]] = None,
661
+ delete_strategy_ids: Optional[List[str]] = None,
662
+ ) -> Memory:
663
+ """Update memory strategies - add, modify, or delete."""
664
+ try:
665
+ memory_strategies = {}
666
+
667
+ if add_strategies:
668
+ processed_add = self._add_default_namespaces(add_strategies)
669
+ memory_strategies["addMemoryStrategies"] = processed_add
670
+
671
+ if modify_strategies:
672
+ current_strategies = self.get_memory_strategies(memory_id)
673
+ strategy_map = {s["strategyId"]: s for s in current_strategies}
674
+
675
+ modify_list = []
676
+ for strategy in modify_strategies:
677
+ if "strategyId" not in strategy:
678
+ raise ValueError("Each modify strategy must include strategyId")
679
+
680
+ strategy_id = strategy["strategyId"]
681
+ strategy_info = strategy_map.get(strategy_id)
682
+
683
+ if not strategy_info:
684
+ raise ValueError("Strategy %s not found in memory %s" % (strategy_id, memory_id))
685
+
686
+ # Handle field name variations for strategy type
687
+ strategy_type = strategy_info.get("type", strategy_info.get("memoryStrategyType", "SEMANTIC"))
688
+ override_type = strategy_info.get("configuration", {}).get("type")
689
+
690
+ strategy_copy = copy.deepcopy(strategy)
691
+
692
+ if "configuration" in strategy_copy:
693
+ wrapped_config = self._wrap_configuration(
694
+ strategy_copy["configuration"], strategy_type, override_type
695
+ )
696
+ strategy_copy["configuration"] = wrapped_config
697
+
698
+ modify_list.append(strategy_copy)
699
+
700
+ memory_strategies["modifyMemoryStrategies"] = modify_list
701
+
702
+ if delete_strategy_ids:
703
+ delete_list = [{"memoryStrategyId": sid} for sid in delete_strategy_ids]
704
+ memory_strategies["deleteMemoryStrategies"] = delete_list
705
+
706
+ if not memory_strategies:
707
+ raise ValueError("No strategy operations provided")
708
+
709
+ response = self._control_plane_client.update_memory(
710
+ memoryId=memory_id,
711
+ memoryStrategies=memory_strategies,
712
+ clientToken=str(uuid.uuid4()),
713
+ )
714
+
715
+ logger.info("Updated memory strategies for: %s", memory_id)
716
+ return Memory(response["memory"])
717
+
718
+ except ClientError as e:
719
+ logger.error("Failed to update memory strategies: %s", e)
720
+ raise
721
+
722
+ def update_memory_strategies_and_wait(
723
+ self,
724
+ memory_id: str,
725
+ add_strategies: Optional[List[Dict[str, Any]]] = None,
726
+ modify_strategies: Optional[List[Dict[str, Any]]] = None,
727
+ delete_strategy_ids: Optional[List[str]] = None,
728
+ max_wait: int = 300,
729
+ poll_interval: int = 10,
730
+ ) -> Memory:
731
+ """Update memory strategies and wait for memory to return to ACTIVE state.
732
+
733
+ This method handles the temporary CREATING state that occurs when
734
+ updating strategies, preventing subsequent update errors.
735
+ """
736
+ # Update strategies
737
+ self.update_memory_strategies(memory_id, add_strategies, modify_strategies, delete_strategy_ids)
738
+
739
+ # Wait for memory to return to ACTIVE
740
+ return self._wait_for_memory_active(memory_id, max_wait, poll_interval)
741
+
742
+ def add_strategy(self, memory_id: str, strategy: Dict[str, Any]) -> Memory:
743
+ """Add a strategy to a memory (without waiting).
744
+
745
+ WARNING: After adding a strategy, the memory enters CREATING state temporarily.
746
+ Use add_*_strategy_and_wait() methods instead to avoid errors.
747
+
748
+ Args:
749
+ memory_id: Memory resource ID
750
+ strategy: Strategy configuration dictionary
751
+
752
+ Returns:
753
+ Updated memory response
754
+ """
755
+ warnings.warn(
756
+ "add_strategy() may leave memory in CREATING state. "
757
+ "Use add_*_strategy_and_wait() methods to avoid subsequent errors.",
758
+ UserWarning,
759
+ stacklevel=2,
760
+ )
761
+ return self._add_strategy(memory_id, strategy)
762
+
763
+ def _add_strategy(self, memory_id: str, strategy: Dict[str, Any]) -> Memory:
764
+ """Internal method to add a single strategy."""
765
+ return self.update_memory_strategies(memory_id=memory_id, add_strategies=[strategy])
766
+
767
+ def _check_strategies_terminal_state(self, strategies: List[Dict[str, Any]]) -> tuple[bool, List[str], List[str]]:
768
+ """Check if all strategies are in terminal states.
769
+
770
+ Args:
771
+ strategies: List of strategy dictionaries
772
+
773
+ Returns:
774
+ Tuple of (all_terminal, strategy_statuses, failed_strategy_names)
775
+ """
776
+ all_strategies_terminal = True
777
+ strategy_statuses = []
778
+ failed_strategy_names = []
779
+
780
+ for strategy in strategies:
781
+ strategy_status = strategy.get("status", "UNKNOWN")
782
+ strategy_statuses.append(strategy_status)
783
+
784
+ # Check if strategy is in a terminal state
785
+ if strategy_status not in [MemoryStrategyStatus.ACTIVE.value, MemoryStrategyStatus.FAILED.value]:
786
+ all_strategies_terminal = False
787
+ elif strategy_status == MemoryStrategyStatus.FAILED.value:
788
+ strategy_name = strategy.get("name", strategy.get("strategyId", "unknown"))
789
+ failed_strategy_names.append(strategy_name)
790
+
791
+ return all_strategies_terminal, strategy_statuses, failed_strategy_names
792
+
793
+ def _wait_for_memory_active(self, memory_id: str, max_wait: int, poll_interval: int) -> Memory:
794
+ """Wait for memory to return to ACTIVE state and all strategies to reach terminal states."""
795
+ logger.info(
796
+ "Waiting for memory %s to return to ACTIVE state and strategies to reach terminal states...", memory_id
797
+ )
798
+
799
+ start_time = time.time()
800
+
801
+ while time.time() - start_time < max_wait:
802
+ elapsed = int(time.time() - start_time)
803
+
804
+ try:
805
+ # Get full memory details including strategies
806
+ response = self._control_plane_client.get_memory(memoryId=memory_id)
807
+ memory = response["memory"]
808
+ memory_status = memory["status"]
809
+
810
+ # Check if memory itself has failed
811
+ if memory_status == MemoryStatus.FAILED.value:
812
+ failure_reason = memory.get("failureReason", "Unknown")
813
+ raise RuntimeError("Memory update failed: %s" % failure_reason)
814
+
815
+ # Get strategies and check their statuses
816
+ strategies = memory.get("strategies", memory.get("memoryStrategies", []))
817
+ all_strategies_terminal, strategy_statuses, failed_strategy_names = (
818
+ self._check_strategies_terminal_state(strategies)
819
+ )
820
+
821
+ # Log current status
822
+ logger.debug(
823
+ "Memory status: %s, Strategy statuses: %s (%d seconds elapsed)",
824
+ memory_status,
825
+ strategy_statuses,
826
+ elapsed,
827
+ )
828
+
829
+ # Check if memory is ACTIVE and all strategies are in terminal states
830
+ if memory_status == MemoryStatus.ACTIVE.value and all_strategies_terminal:
831
+ # Check if any strategy failed
832
+ if failed_strategy_names:
833
+ raise RuntimeError("Memory strategy(ies) failed: %s" % ", ".join(failed_strategy_names))
834
+
835
+ logger.info(
836
+ "Memory %s is ACTIVE and all strategies are in terminal states (took %d seconds)",
837
+ memory_id,
838
+ elapsed,
839
+ )
840
+ return Memory(memory)
841
+
842
+ # Wait before next check
843
+ time.sleep(poll_interval)
844
+
845
+ except ClientError as e:
846
+ logger.error("Error checking memory status: %s", e)
847
+ raise
848
+
849
+ raise TimeoutError(
850
+ "Memory %s did not return to ACTIVE state with all strategies in terminal states within %d seconds"
851
+ % (memory_id, max_wait)
852
+ )
853
+
854
+ def _add_default_namespaces(self, strategies: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
855
+ """Add default namespaces to strategies that don't have them."""
856
+ processed = []
857
+
858
+ for strategy in strategies:
859
+ strategy_copy = copy.deepcopy(strategy)
860
+
861
+ strategy_type_key = list(strategy.keys())[0]
862
+ strategy_config = strategy_copy[strategy_type_key]
863
+
864
+ if "namespaces" not in strategy_config:
865
+ strategy_type = StrategyType(strategy_type_key)
866
+ strategy_config["namespaces"] = DEFAULT_NAMESPACES.get(strategy_type, ["custom/{actorId}/{sessionId}"])
867
+
868
+ self._validate_strategy_config(strategy_copy, strategy_type_key)
869
+
870
+ processed.append(strategy_copy)
871
+
872
+ return processed
873
+
874
+ def _validate_namespace(self, namespace: str) -> bool:
875
+ """Validate namespace format - basic check only."""
876
+ # Only check for template variables in namespace definition
877
+ if "{" in namespace and not (
878
+ "{actorId}" in namespace or "{sessionId}" in namespace or "{strategyId}" in namespace
879
+ ):
880
+ logger.warning("Namespace with templates should contain valid variables: %s", namespace)
881
+
882
+ return True
883
+
884
+ def _validate_strategy_config(self, strategy: Dict[str, Any], strategy_type: str) -> None:
885
+ """Validate strategy configuration parameters."""
886
+ strategy_config = strategy[strategy_type]
887
+
888
+ namespaces = strategy_config.get("namespaces", [])
889
+ for namespace in namespaces:
890
+ self._validate_namespace(namespace)