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.
- bedrock_agentcore_starter_toolkit/cli/import_agent/agent_info.py +6 -1
- bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +88 -1
- bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +53 -0
- bedrock_agentcore_starter_toolkit/notebook/runtime/bedrock_agentcore.py +3 -0
- bedrock_agentcore_starter_toolkit/operations/memory/__init__.py +1 -0
- bedrock_agentcore_starter_toolkit/operations/memory/constants.py +98 -0
- bedrock_agentcore_starter_toolkit/operations/memory/manager.py +890 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/DictWrapper.py +51 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/Memory.py +17 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/MemoryStrategy.py +17 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/MemorySummary.py +17 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/configure.py +28 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/create_role.py +3 -2
- bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +8 -2
- bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +1 -0
- bedrock_agentcore_starter_toolkit/services/codebuild.py +17 -6
- bedrock_agentcore_starter_toolkit/services/runtime.py +71 -5
- bedrock_agentcore_starter_toolkit/utils/runtime/schema.py +1 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/Dockerfile.j2 +25 -11
- {bedrock_agentcore_starter_toolkit-0.1.12.dist-info → bedrock_agentcore_starter_toolkit-0.1.14.dist-info}/METADATA +3 -4
- {bedrock_agentcore_starter_toolkit-0.1.12.dist-info → bedrock_agentcore_starter_toolkit-0.1.14.dist-info}/RECORD +25 -18
- {bedrock_agentcore_starter_toolkit-0.1.12.dist-info → bedrock_agentcore_starter_toolkit-0.1.14.dist-info}/WHEEL +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.12.dist-info → bedrock_agentcore_starter_toolkit-0.1.14.dist-info}/entry_points.txt +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.12.dist-info → bedrock_agentcore_starter_toolkit-0.1.14.dist-info}/licenses/LICENSE.txt +0 -0
- {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)
|