bedrock-agentcore-starter-toolkit 0.1.13__py3-none-any.whl → 0.1.15__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/runtime/commands.py +35 -2
- bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +127 -1
- bedrock_agentcore_starter_toolkit/notebook/runtime/bedrock_agentcore.py +3 -0
- bedrock_agentcore_starter_toolkit/operations/memory/README.md +1109 -0
- bedrock_agentcore_starter_toolkit/operations/memory/constants.py +1 -9
- bedrock_agentcore_starter_toolkit/operations/memory/manager.py +248 -57
- bedrock_agentcore_starter_toolkit/operations/memory/models/__init__.py +106 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/__init__.py +52 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/base.py +77 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/custom.py +194 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/semantic.py +35 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/summary.py +35 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/user_preference.py +34 -0
- bedrock_agentcore_starter_toolkit/operations/memory/strategy_validator.py +395 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/configure.py +78 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/destroy.py +43 -3
- bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +45 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +164 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/models.py +7 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/status.py +62 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/container.py +4 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/schema.py +27 -1
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/Dockerfile.j2 +10 -3
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/execution_role_policy.json.j2 +31 -0
- {bedrock_agentcore_starter_toolkit-0.1.13.dist-info → bedrock_agentcore_starter_toolkit-0.1.15.dist-info}/METADATA +1 -1
- {bedrock_agentcore_starter_toolkit-0.1.13.dist-info → bedrock_agentcore_starter_toolkit-0.1.15.dist-info}/RECORD +30 -21
- {bedrock_agentcore_starter_toolkit-0.1.13.dist-info → bedrock_agentcore_starter_toolkit-0.1.15.dist-info}/WHEEL +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.13.dist-info → bedrock_agentcore_starter_toolkit-0.1.15.dist-info}/entry_points.txt +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.13.dist-info → bedrock_agentcore_starter_toolkit-0.1.15.dist-info}/licenses/LICENSE.txt +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.13.dist-info → bedrock_agentcore_starter_toolkit-0.1.15.dist-info}/licenses/NOTICE.txt +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""Strategy validation utilities for memory operations."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Dict, List, Union
|
|
6
|
+
|
|
7
|
+
from .constants import StrategyType
|
|
8
|
+
from .models import convert_strategies_to_dicts
|
|
9
|
+
from .models.strategies import BaseStrategy
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UniversalComparator:
|
|
15
|
+
"""Universal comparison utility for deep strategy validation."""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def _camel_to_snake(name: str) -> str:
|
|
19
|
+
"""Convert camelCase to snake_case."""
|
|
20
|
+
# Handle sequences of uppercase letters (like XMLHttpRequest -> xml_http_request)
|
|
21
|
+
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
|
22
|
+
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def normalize_field_names(data: Any) -> Any:
|
|
26
|
+
"""Recursively normalize field names from camelCase to snake_case."""
|
|
27
|
+
if isinstance(data, dict):
|
|
28
|
+
normalized = {}
|
|
29
|
+
for key, value in data.items():
|
|
30
|
+
normalized_key = UniversalComparator._camel_to_snake(key)
|
|
31
|
+
normalized[normalized_key] = UniversalComparator.normalize_field_names(value)
|
|
32
|
+
return normalized
|
|
33
|
+
elif isinstance(data, list):
|
|
34
|
+
return [UniversalComparator.normalize_field_names(item) for item in data]
|
|
35
|
+
else:
|
|
36
|
+
return data
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def deep_compare(dict1: Dict[str, Any], dict2: Dict[str, Any], path: str = "") -> tuple[bool, str]:
|
|
40
|
+
"""Deep compare two dictionaries with detailed error reporting."""
|
|
41
|
+
# Normalize both dictionaries
|
|
42
|
+
norm1 = UniversalComparator.normalize_field_names(dict1)
|
|
43
|
+
norm2 = UniversalComparator.normalize_field_names(dict2)
|
|
44
|
+
|
|
45
|
+
return UniversalComparator._deep_compare_normalized(norm1, norm2, path)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _deep_compare_normalized(obj1: Any, obj2: Any, path: str = "") -> tuple[bool, str]:
|
|
49
|
+
"""Compare normalized objects recursively."""
|
|
50
|
+
# Handle None equivalence - treat None and empty values as equivalent
|
|
51
|
+
if obj1 is None and obj2 is None:
|
|
52
|
+
return True, ""
|
|
53
|
+
if obj1 is None and (obj2 == "" or obj2 == [] or obj2 == {}):
|
|
54
|
+
return True, ""
|
|
55
|
+
if obj2 is None and (obj1 == "" or obj1 == [] or obj1 == {}):
|
|
56
|
+
return True, ""
|
|
57
|
+
|
|
58
|
+
# Type comparison
|
|
59
|
+
if type(obj1) is not type(obj2):
|
|
60
|
+
return False, f"{path}: type mismatch ({type(obj1).__name__} vs {type(obj2).__name__})"
|
|
61
|
+
|
|
62
|
+
if isinstance(obj1, dict):
|
|
63
|
+
# Get all keys from both dictionaries
|
|
64
|
+
all_keys = set(obj1.keys()) | set(obj2.keys())
|
|
65
|
+
|
|
66
|
+
for key in all_keys:
|
|
67
|
+
key_path = f"{path}.{key}" if path else key
|
|
68
|
+
|
|
69
|
+
val1 = obj1.get(key)
|
|
70
|
+
val2 = obj2.get(key)
|
|
71
|
+
|
|
72
|
+
# Special handling for namespaces - compare as sets (order-independent)
|
|
73
|
+
if key == "namespaces" and isinstance(val1, list) and isinstance(val2, list):
|
|
74
|
+
set1 = set(val1) if val1 else set()
|
|
75
|
+
set2 = set(val2) if val2 else set()
|
|
76
|
+
if set1 != set2:
|
|
77
|
+
return False, f"{key_path}: mismatch ({sorted(set1)} vs {sorted(set2)})"
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
matches, error = UniversalComparator._deep_compare_normalized(val1, val2, key_path)
|
|
81
|
+
if not matches:
|
|
82
|
+
return False, error
|
|
83
|
+
|
|
84
|
+
return True, ""
|
|
85
|
+
|
|
86
|
+
elif isinstance(obj1, list):
|
|
87
|
+
# Special case: if this is a namespaces list at the root level, compare as sets
|
|
88
|
+
if path == "namespaces":
|
|
89
|
+
set1 = set(obj1) if obj1 else set()
|
|
90
|
+
set2 = set(obj2) if obj2 else set()
|
|
91
|
+
if set1 != set2:
|
|
92
|
+
return False, f"{path}: mismatch ({sorted(set1)} vs {sorted(set2)})"
|
|
93
|
+
return True, ""
|
|
94
|
+
|
|
95
|
+
if len(obj1) != len(obj2):
|
|
96
|
+
return False, f"{path}: list length mismatch ({len(obj1)} vs {len(obj2)})"
|
|
97
|
+
|
|
98
|
+
for i, (item1, item2) in enumerate(zip(obj1, obj2, strict=False)):
|
|
99
|
+
item_path = f"{path}[{i}]" if path else f"[{i}]"
|
|
100
|
+
matches, error = UniversalComparator._deep_compare_normalized(item1, item2, item_path)
|
|
101
|
+
if not matches:
|
|
102
|
+
return False, error
|
|
103
|
+
|
|
104
|
+
return True, ""
|
|
105
|
+
|
|
106
|
+
else:
|
|
107
|
+
# Direct value comparison
|
|
108
|
+
if obj1 != obj2:
|
|
109
|
+
return False, f"{path}: value mismatch ('{obj1}' vs '{obj2}')"
|
|
110
|
+
return True, ""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class StrategyComparator:
|
|
114
|
+
"""Utility class for comparing memory strategies in detail."""
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def normalize_strategy(strategy: Union[Dict[str, Any], Dict[str, Dict[str, Any]]]) -> Dict[str, Any]:
|
|
118
|
+
"""Normalize a strategy to a standard format with universal field normalization.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
strategy: Strategy dictionary (either from memory response or request format)
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Normalized strategy dictionary with snake_case field names
|
|
125
|
+
"""
|
|
126
|
+
# Check if this is already a normalized strategy (from memory response)
|
|
127
|
+
if "type" in strategy or "memoryStrategyType" in strategy:
|
|
128
|
+
return StrategyComparator._normalize_memory_strategy(strategy)
|
|
129
|
+
|
|
130
|
+
# Otherwise, it's a request format strategy
|
|
131
|
+
return StrategyComparator._normalize_request_strategy(strategy)
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _normalize_memory_strategy(strategy: Dict[str, Any]) -> Dict[str, Any]:
|
|
135
|
+
"""Normalize a strategy from memory response, including only fields relevant for comparison."""
|
|
136
|
+
# Handle different field name variations
|
|
137
|
+
strategy_type = strategy.get("type", strategy.get("memoryStrategyType"))
|
|
138
|
+
|
|
139
|
+
# Only include the core fields that should be compared
|
|
140
|
+
normalized = {
|
|
141
|
+
"type": strategy_type,
|
|
142
|
+
"name": strategy.get("name"),
|
|
143
|
+
"description": strategy.get("description"),
|
|
144
|
+
"namespaces": strategy.get("namespaces", []),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Add configuration if present and normalize it
|
|
148
|
+
if "configuration" in strategy and strategy["configuration"]:
|
|
149
|
+
config = strategy["configuration"]
|
|
150
|
+
normalized_config = StrategyComparator._transform_memory_configuration(config, strategy_type)
|
|
151
|
+
normalized["configuration"] = UniversalComparator.normalize_field_names(normalized_config)
|
|
152
|
+
|
|
153
|
+
# Don't include any other fields from memory responses (like status, strategyId, etc.)
|
|
154
|
+
# as they are not relevant for strategy comparison
|
|
155
|
+
|
|
156
|
+
return normalized
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _transform_memory_configuration(config: Dict[str, Any], strategy_type: str) -> Dict[str, Any]:
|
|
160
|
+
"""Transform memory configuration from stored format to match requested format.
|
|
161
|
+
|
|
162
|
+
This handles the structural differences between how configurations are stored
|
|
163
|
+
in memory vs how they're provided through typed strategy objects.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
config: Configuration from memory response
|
|
167
|
+
strategy_type: Strategy type (e.g., 'CUSTOM', 'SEMANTIC', etc.)
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Transformed configuration matching the requested format
|
|
171
|
+
"""
|
|
172
|
+
if not config:
|
|
173
|
+
return config
|
|
174
|
+
|
|
175
|
+
# Handle CUSTOM strategy configurations that need transformation
|
|
176
|
+
if strategy_type == "CUSTOM" and config.get("type") in [
|
|
177
|
+
"SEMANTIC_OVERRIDE",
|
|
178
|
+
"USER_PREFERENCE_OVERRIDE",
|
|
179
|
+
"SUMMARY_OVERRIDE",
|
|
180
|
+
]:
|
|
181
|
+
override_type = config.get("type")
|
|
182
|
+
transformed_config = {}
|
|
183
|
+
|
|
184
|
+
# Determine the override key name based on type
|
|
185
|
+
if override_type == "SEMANTIC_OVERRIDE":
|
|
186
|
+
override_key = "semanticOverride"
|
|
187
|
+
elif override_type == "USER_PREFERENCE_OVERRIDE":
|
|
188
|
+
override_key = "userPreferenceOverride"
|
|
189
|
+
elif override_type == "SUMMARY_OVERRIDE":
|
|
190
|
+
override_key = "summaryOverride"
|
|
191
|
+
else:
|
|
192
|
+
# Fallback - return original config
|
|
193
|
+
return config
|
|
194
|
+
|
|
195
|
+
transformed_config[override_key] = {}
|
|
196
|
+
|
|
197
|
+
# Transform extraction configuration
|
|
198
|
+
if "extraction" in config:
|
|
199
|
+
extraction = config["extraction"]
|
|
200
|
+
if "customExtractionConfiguration" in extraction:
|
|
201
|
+
custom_extraction = extraction["customExtractionConfiguration"]
|
|
202
|
+
|
|
203
|
+
# Find the override key and extract the actual config
|
|
204
|
+
for key, value in custom_extraction.items():
|
|
205
|
+
if key.endswith("Override"):
|
|
206
|
+
transformed_config[override_key]["extraction"] = value
|
|
207
|
+
break
|
|
208
|
+
elif "custom_extraction_configuration" in extraction:
|
|
209
|
+
# Handle snake_case version
|
|
210
|
+
custom_extraction = extraction["custom_extraction_configuration"]
|
|
211
|
+
|
|
212
|
+
# Find the override key and extract the actual config
|
|
213
|
+
for key, value in custom_extraction.items():
|
|
214
|
+
if key.endswith("_override"):
|
|
215
|
+
transformed_config[override_key]["extraction"] = value
|
|
216
|
+
break
|
|
217
|
+
else:
|
|
218
|
+
# Direct extraction config (no wrapper)
|
|
219
|
+
transformed_config[override_key]["extraction"] = extraction
|
|
220
|
+
|
|
221
|
+
# Transform consolidation configuration
|
|
222
|
+
if "consolidation" in config:
|
|
223
|
+
consolidation = config["consolidation"]
|
|
224
|
+
if "customConsolidationConfiguration" in consolidation:
|
|
225
|
+
custom_consolidation = consolidation["customConsolidationConfiguration"]
|
|
226
|
+
|
|
227
|
+
# Find the override key and extract the actual config
|
|
228
|
+
for key, value in custom_consolidation.items():
|
|
229
|
+
if key.endswith("Override"):
|
|
230
|
+
transformed_config[override_key]["consolidation"] = value
|
|
231
|
+
break
|
|
232
|
+
elif "custom_consolidation_configuration" in consolidation:
|
|
233
|
+
# Handle snake_case version
|
|
234
|
+
custom_consolidation = consolidation["custom_consolidation_configuration"]
|
|
235
|
+
|
|
236
|
+
# Find the override key and extract the actual config
|
|
237
|
+
for key, value in custom_consolidation.items():
|
|
238
|
+
if key.endswith("_override"):
|
|
239
|
+
transformed_config[override_key]["consolidation"] = value
|
|
240
|
+
break
|
|
241
|
+
else:
|
|
242
|
+
# Direct consolidation config (no wrapper)
|
|
243
|
+
transformed_config[override_key]["consolidation"] = consolidation
|
|
244
|
+
|
|
245
|
+
# Copy any other fields that don't need transformation
|
|
246
|
+
for key, value in config.items():
|
|
247
|
+
if key not in ["type", "extraction", "consolidation"]:
|
|
248
|
+
transformed_config[key] = value
|
|
249
|
+
|
|
250
|
+
return transformed_config
|
|
251
|
+
|
|
252
|
+
# For non-CUSTOM strategies or configurations that don't need transformation, return as-is
|
|
253
|
+
return config
|
|
254
|
+
|
|
255
|
+
@staticmethod
|
|
256
|
+
def _normalize_request_strategy(strategy_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
257
|
+
"""Normalize a strategy from request format."""
|
|
258
|
+
# Find the strategy type key in the dictionary
|
|
259
|
+
strategy_type = None
|
|
260
|
+
strategy_config = None
|
|
261
|
+
|
|
262
|
+
for key, config in strategy_dict.items():
|
|
263
|
+
if key.endswith("MemoryStrategy") or key in [
|
|
264
|
+
StrategyType.SEMANTIC.value,
|
|
265
|
+
StrategyType.SUMMARY.value,
|
|
266
|
+
StrategyType.USER_PREFERENCE.value,
|
|
267
|
+
StrategyType.CUSTOM.value,
|
|
268
|
+
]:
|
|
269
|
+
strategy_config = config
|
|
270
|
+
# Map strategy keys to standard types using the constants
|
|
271
|
+
if key == "semanticMemoryStrategy" or key == StrategyType.SEMANTIC.value:
|
|
272
|
+
strategy_type = StrategyType.SEMANTIC.get_memory_strategy()
|
|
273
|
+
elif key == "summaryMemoryStrategy" or key == StrategyType.SUMMARY.value:
|
|
274
|
+
strategy_type = StrategyType.SUMMARY.get_memory_strategy()
|
|
275
|
+
elif key == "userPreferenceMemoryStrategy" or key == StrategyType.USER_PREFERENCE.value:
|
|
276
|
+
strategy_type = StrategyType.USER_PREFERENCE.get_memory_strategy()
|
|
277
|
+
elif key == "customMemoryStrategy" or key == StrategyType.CUSTOM.value:
|
|
278
|
+
strategy_type = StrategyType.CUSTOM.get_memory_strategy()
|
|
279
|
+
elif key.endswith("MemoryStrategy"):
|
|
280
|
+
# Handle future strategy types following naming convention
|
|
281
|
+
# e.g., "newTypeMemoryStrategy" -> "NEW_TYPE"
|
|
282
|
+
type_name = key.replace("MemoryStrategy", "")
|
|
283
|
+
strategy_type = UniversalComparator._camel_to_snake(type_name).upper()
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
if not strategy_config:
|
|
287
|
+
raise ValueError(f"Invalid strategy format: {strategy_dict}")
|
|
288
|
+
|
|
289
|
+
normalized = {
|
|
290
|
+
"type": strategy_type,
|
|
291
|
+
"name": strategy_config.get("name"),
|
|
292
|
+
"description": strategy_config.get("description"),
|
|
293
|
+
"namespaces": strategy_config.get("namespaces", []),
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# Add configuration if present and normalize it
|
|
297
|
+
if "configuration" in strategy_config and strategy_config["configuration"]:
|
|
298
|
+
normalized["configuration"] = UniversalComparator.normalize_field_names(strategy_config["configuration"])
|
|
299
|
+
|
|
300
|
+
# Normalize any additional fields in the strategy config (but exclude common metadata fields)
|
|
301
|
+
excluded_fields = {"name", "description", "namespaces", "configuration", "status", "strategyId"}
|
|
302
|
+
for key, value in strategy_config.items():
|
|
303
|
+
if key not in excluded_fields:
|
|
304
|
+
normalized_key = UniversalComparator._camel_to_snake(key)
|
|
305
|
+
normalized[normalized_key] = UniversalComparator.normalize_field_names(value)
|
|
306
|
+
|
|
307
|
+
return normalized
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def compare_strategies(
|
|
311
|
+
existing_strategies: List[Dict[str, Any]], requested_strategies: List[Union[BaseStrategy, Dict[str, Any]]]
|
|
312
|
+
) -> tuple[bool, str]:
|
|
313
|
+
"""Compare existing memory strategies with requested strategies using universal comparison.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
existing_strategies: List of strategy dictionaries from memory response
|
|
317
|
+
requested_strategies: List of requested strategy objects or dictionaries
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Tuple of (matches, error_message). If matches is False, error_message contains details.
|
|
321
|
+
"""
|
|
322
|
+
# Convert requested strategies to dictionaries for comparison
|
|
323
|
+
requested_dict_strategies = convert_strategies_to_dicts(requested_strategies)
|
|
324
|
+
|
|
325
|
+
# Normalize both sets of strategies
|
|
326
|
+
normalized_existing = []
|
|
327
|
+
for strategy in existing_strategies:
|
|
328
|
+
try:
|
|
329
|
+
normalized_existing.append(StrategyComparator.normalize_strategy(strategy))
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.warning("Failed to normalize existing strategy: %s, error: %s", strategy, e)
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
normalized_requested = []
|
|
335
|
+
for strategy in requested_dict_strategies:
|
|
336
|
+
try:
|
|
337
|
+
normalized_requested.append(StrategyComparator.normalize_strategy(strategy))
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.warning("Failed to normalize requested strategy: %s, error: %s", strategy, e)
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
# Sort both lists by type and name for consistent comparison
|
|
343
|
+
normalized_existing.sort(key=lambda x: (x.get("type", ""), x.get("name", "")))
|
|
344
|
+
normalized_requested.sort(key=lambda x: (x.get("type", ""), x.get("name", "")))
|
|
345
|
+
|
|
346
|
+
# Check if counts match
|
|
347
|
+
if len(normalized_existing) != len(normalized_requested):
|
|
348
|
+
existing_types = [s.get("type") for s in normalized_existing]
|
|
349
|
+
requested_types = [s.get("type") for s in normalized_requested]
|
|
350
|
+
return False, (
|
|
351
|
+
f"Strategy count mismatch. "
|
|
352
|
+
f"Existing memory has {len(normalized_existing)} strategies: {existing_types}, "
|
|
353
|
+
f"but {len(normalized_requested)} strategies were requested: {requested_types}."
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Use universal comparison for each strategy pair
|
|
357
|
+
for i, (existing, requested) in enumerate(zip(normalized_existing, normalized_requested, strict=False)):
|
|
358
|
+
logger.info("Existing %s\nRequested %s", existing, requested)
|
|
359
|
+
matches, error = UniversalComparator.deep_compare(existing, requested)
|
|
360
|
+
if not matches:
|
|
361
|
+
return False, f"Strategy {i + 1} mismatch: {error}"
|
|
362
|
+
|
|
363
|
+
return True, ""
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def validate_existing_memory_strategies(
|
|
367
|
+
memory_strategies: List[Dict[str, Any]],
|
|
368
|
+
requested_strategies: List[Union[BaseStrategy, Dict[str, Any]]],
|
|
369
|
+
memory_name: str,
|
|
370
|
+
) -> None:
|
|
371
|
+
"""Validate that existing memory strategies match the requested strategies using universal comparison.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
memory_strategies: List of strategy dictionaries from memory response
|
|
375
|
+
requested_strategies: List of requested strategy objects or dictionaries
|
|
376
|
+
memory_name: Memory name for error messages
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
ValueError: If the strategies don't match with detailed explanation
|
|
380
|
+
"""
|
|
381
|
+
matches, error_message = StrategyComparator.compare_strategies(memory_strategies, requested_strategies)
|
|
382
|
+
|
|
383
|
+
if not matches:
|
|
384
|
+
raise ValueError(
|
|
385
|
+
f"Strategy mismatch for memory '{memory_name}'. {error_message} "
|
|
386
|
+
f"Cannot use existing memory with different strategy configuration."
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Log successful validation
|
|
390
|
+
strategy_types = [s.get("type", s.get("memoryStrategyType", "unknown")) for s in memory_strategies]
|
|
391
|
+
logger.info(
|
|
392
|
+
"Universal strategy validation passed for memory %s. Strategies match: [%s]",
|
|
393
|
+
memory_name,
|
|
394
|
+
", ".join(strategy_types),
|
|
395
|
+
)
|
|
@@ -5,6 +5,7 @@ import re
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, Dict, Optional, Tuple
|
|
7
7
|
|
|
8
|
+
from ...cli.runtime.configuration_manager import ConfigurationManager
|
|
8
9
|
from ...services.ecr import get_account_id, get_region
|
|
9
10
|
from ...utils.runtime.config import merge_agent_config, save_config
|
|
10
11
|
from ...utils.runtime.container import ContainerRuntime
|
|
@@ -12,6 +13,8 @@ from ...utils.runtime.schema import (
|
|
|
12
13
|
AWSConfig,
|
|
13
14
|
BedrockAgentCoreAgentSchema,
|
|
14
15
|
BedrockAgentCoreDeploymentInfo,
|
|
16
|
+
CodeBuildConfig,
|
|
17
|
+
MemoryConfig,
|
|
15
18
|
NetworkConfiguration,
|
|
16
19
|
ObservabilityConfig,
|
|
17
20
|
ProtocolConfiguration,
|
|
@@ -25,6 +28,7 @@ def configure_bedrock_agentcore(
|
|
|
25
28
|
agent_name: str,
|
|
26
29
|
entrypoint_path: Path,
|
|
27
30
|
execution_role: Optional[str] = None,
|
|
31
|
+
code_build_execution_role: Optional[str] = None,
|
|
28
32
|
ecr_repository: Optional[str] = None,
|
|
29
33
|
container_runtime: Optional[str] = None,
|
|
30
34
|
auto_create_ecr: bool = True,
|
|
@@ -36,6 +40,7 @@ def configure_bedrock_agentcore(
|
|
|
36
40
|
verbose: bool = False,
|
|
37
41
|
region: Optional[str] = None,
|
|
38
42
|
protocol: Optional[str] = None,
|
|
43
|
+
non_interactive: bool = False,
|
|
39
44
|
) -> ConfigureResult:
|
|
40
45
|
"""Configure Bedrock AgentCore application with deployment settings.
|
|
41
46
|
|
|
@@ -43,6 +48,7 @@ def configure_bedrock_agentcore(
|
|
|
43
48
|
agent_name: name of the agent,
|
|
44
49
|
entrypoint_path: Path to the entrypoint file
|
|
45
50
|
execution_role: AWS execution role ARN or name (auto-created if not provided)
|
|
51
|
+
code_build_execution_role: CodeBuild execution role ARN or name (uses execution_role if not provided)
|
|
46
52
|
ecr_repository: ECR repository URI
|
|
47
53
|
container_runtime: Container runtime to use
|
|
48
54
|
auto_create_ecr: Whether to auto-create ECR repository
|
|
@@ -54,6 +60,7 @@ def configure_bedrock_agentcore(
|
|
|
54
60
|
verbose: Whether to provide verbose output during configuration
|
|
55
61
|
region: AWS region for deployment
|
|
56
62
|
protocol: agent server protocol, must be either HTTP or MCP
|
|
63
|
+
non_interactive: Skip interactive prompts and use defaults
|
|
57
64
|
|
|
58
65
|
Returns:
|
|
59
66
|
ConfigureResult model with configuration details
|
|
@@ -109,6 +116,69 @@ def configure_bedrock_agentcore(
|
|
|
109
116
|
else:
|
|
110
117
|
log.debug("No execution role provided and auto-create disabled")
|
|
111
118
|
|
|
119
|
+
if verbose:
|
|
120
|
+
log.debug("Prompting for memory configuration")
|
|
121
|
+
|
|
122
|
+
config_manager = ConfigurationManager(build_dir / ".bedrock_agentcore.yaml")
|
|
123
|
+
|
|
124
|
+
# New memory selection flow
|
|
125
|
+
action, value = config_manager.prompt_memory_selection()
|
|
126
|
+
|
|
127
|
+
memory_config = MemoryConfig()
|
|
128
|
+
if action == "USE_EXISTING":
|
|
129
|
+
# Using existing memory - just store the ID
|
|
130
|
+
memory_config.memory_id = value
|
|
131
|
+
memory_config.mode = "STM_AND_LTM" # Assume existing has strategies
|
|
132
|
+
memory_config.memory_name = f"{agent_name}_memory"
|
|
133
|
+
log.info("Using existing memory resource: %s", value)
|
|
134
|
+
elif action == "CREATE_NEW":
|
|
135
|
+
# Create new with specified mode
|
|
136
|
+
memory_config.mode = value # This is the mode (STM_ONLY, STM_AND_LTM, NO_MEMORY)
|
|
137
|
+
memory_config.event_expiry_days = 30
|
|
138
|
+
memory_config.memory_name = f"{agent_name}_memory"
|
|
139
|
+
log.info("Will create new memory with mode: %s", value)
|
|
140
|
+
|
|
141
|
+
if memory_config.mode == "STM_AND_LTM":
|
|
142
|
+
log.info("Memory configuration: Short-term + Long-term memory enabled")
|
|
143
|
+
elif memory_config.mode == "STM_ONLY":
|
|
144
|
+
log.info("Memory configuration: Short-term memory only")
|
|
145
|
+
|
|
146
|
+
# Check for existing memory configuration from previous launch
|
|
147
|
+
config_path = build_dir / ".bedrock_agentcore.yaml"
|
|
148
|
+
memory_id = None
|
|
149
|
+
memory_name = None
|
|
150
|
+
|
|
151
|
+
if config_path.exists():
|
|
152
|
+
try:
|
|
153
|
+
from ...utils.runtime.config import load_config
|
|
154
|
+
|
|
155
|
+
existing_config = load_config(config_path)
|
|
156
|
+
existing_agent = existing_config.get_agent_config(agent_name)
|
|
157
|
+
if existing_agent and existing_agent.memory and existing_agent.memory.memory_id:
|
|
158
|
+
memory_id = existing_agent.memory.memory_id
|
|
159
|
+
memory_name = existing_agent.memory.memory_name
|
|
160
|
+
log.info("Found existing memory ID from previous launch: %s", memory_id)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
log.debug("Unable to read existing memory configuration: %s", e)
|
|
163
|
+
|
|
164
|
+
# Handle CodeBuild execution role - use separate role if provided, otherwise use execution_role
|
|
165
|
+
codebuild_execution_role_arn = None
|
|
166
|
+
if code_build_execution_role:
|
|
167
|
+
# User provided a separate CodeBuild role
|
|
168
|
+
if code_build_execution_role.startswith("arn:aws:iam::"):
|
|
169
|
+
codebuild_execution_role_arn = code_build_execution_role
|
|
170
|
+
else:
|
|
171
|
+
codebuild_execution_role_arn = f"arn:aws:iam::{account_id}:role/{code_build_execution_role}"
|
|
172
|
+
|
|
173
|
+
if verbose:
|
|
174
|
+
log.debug("Using separate CodeBuild execution role: %s", codebuild_execution_role_arn)
|
|
175
|
+
else:
|
|
176
|
+
# No separate CodeBuild role provided - use None
|
|
177
|
+
codebuild_execution_role_arn = None
|
|
178
|
+
|
|
179
|
+
if verbose and execution_role_arn:
|
|
180
|
+
log.debug("Using same role for CodeBuild: %s", codebuild_execution_role_arn)
|
|
181
|
+
|
|
112
182
|
# Generate Dockerfile and .dockerignore
|
|
113
183
|
bedrock_agentcore_name = None
|
|
114
184
|
# Try to find the variable name for the Bedrock AgentCore instance in the file
|
|
@@ -123,6 +193,8 @@ def configure_bedrock_agentcore(
|
|
|
123
193
|
log.debug(" Region: %s", region)
|
|
124
194
|
log.debug(" Enable observability: %s", enable_observability)
|
|
125
195
|
log.debug(" Requirements file: %s", requirements_file)
|
|
196
|
+
if memory_id:
|
|
197
|
+
log.debug(" Memory ID: %s", memory_id)
|
|
126
198
|
|
|
127
199
|
dockerfile_path = runtime.generate_dockerfile(
|
|
128
200
|
entrypoint_path,
|
|
@@ -131,6 +203,8 @@ def configure_bedrock_agentcore(
|
|
|
131
203
|
region,
|
|
132
204
|
enable_observability,
|
|
133
205
|
requirements_file,
|
|
206
|
+
memory_id,
|
|
207
|
+
memory_name,
|
|
134
208
|
)
|
|
135
209
|
|
|
136
210
|
# Check if .dockerignore was created
|
|
@@ -195,8 +269,12 @@ def configure_bedrock_agentcore(
|
|
|
195
269
|
observability=ObservabilityConfig(enabled=enable_observability),
|
|
196
270
|
),
|
|
197
271
|
bedrock_agentcore=BedrockAgentCoreDeploymentInfo(),
|
|
272
|
+
codebuild=CodeBuildConfig(
|
|
273
|
+
execution_role=codebuild_execution_role_arn,
|
|
274
|
+
),
|
|
198
275
|
authorizer_configuration=authorizer_configuration,
|
|
199
276
|
request_header_configuration=request_header_configuration,
|
|
277
|
+
memory=memory_config,
|
|
200
278
|
)
|
|
201
279
|
|
|
202
280
|
# Use simplified config merging
|
|
@@ -7,6 +7,7 @@ from typing import Optional
|
|
|
7
7
|
import boto3
|
|
8
8
|
from botocore.exceptions import ClientError
|
|
9
9
|
|
|
10
|
+
from ...operations.memory.manager import MemoryManager
|
|
10
11
|
from ...services.runtime import BedrockAgentCoreClient
|
|
11
12
|
from ...utils.runtime.config import load_config, save_config
|
|
12
13
|
from ...utils.runtime.schema import BedrockAgentCoreAgentSchema, BedrockAgentCoreConfigSchema
|
|
@@ -77,13 +78,16 @@ def destroy_bedrock_agentcore(
|
|
|
77
78
|
# 4. Remove CodeBuild project
|
|
78
79
|
_destroy_codebuild_project(session, agent_config, result, dry_run)
|
|
79
80
|
|
|
80
|
-
# 5. Remove
|
|
81
|
+
# 5. Remove memory resource
|
|
82
|
+
_destroy_memory(session, agent_config, result, dry_run)
|
|
83
|
+
|
|
84
|
+
# 6. Remove CodeBuild IAM Role
|
|
81
85
|
_destroy_codebuild_iam_role(session, agent_config, result, dry_run)
|
|
82
86
|
|
|
83
|
-
#
|
|
87
|
+
# 7. Remove IAM execution role (if not used by other agents)
|
|
84
88
|
_destroy_iam_role(session, project_config, agent_config, result, dry_run)
|
|
85
89
|
|
|
86
|
-
#
|
|
90
|
+
# 8. Clean up configuration
|
|
87
91
|
if not dry_run and not result.errors:
|
|
88
92
|
_cleanup_agent_config(config_path, project_config, agent_config.name, result)
|
|
89
93
|
|
|
@@ -386,6 +390,42 @@ def _destroy_codebuild_project(
|
|
|
386
390
|
log.warning("Error during CodeBuild cleanup: %s", e)
|
|
387
391
|
|
|
388
392
|
|
|
393
|
+
def _destroy_memory(
|
|
394
|
+
session: boto3.Session,
|
|
395
|
+
agent_config: BedrockAgentCoreAgentSchema,
|
|
396
|
+
result: DestroyResult,
|
|
397
|
+
dry_run: bool,
|
|
398
|
+
) -> None:
|
|
399
|
+
"""Remove memory resource for this agent."""
|
|
400
|
+
if not agent_config.memory or not agent_config.memory.memory_id:
|
|
401
|
+
result.warnings.append("No memory configured, skipping memory cleanup")
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
memory_manager = MemoryManager(region_name=agent_config.aws.region)
|
|
406
|
+
memory_id = agent_config.memory.memory_id
|
|
407
|
+
|
|
408
|
+
if dry_run:
|
|
409
|
+
result.resources_removed.append(f"Memory: {memory_id} (DRY RUN)")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
# Use the manager's delete method which handles the deletion properly
|
|
414
|
+
memory_manager.delete_memory(memory_id=memory_id)
|
|
415
|
+
result.resources_removed.append(f"Memory: {memory_id}")
|
|
416
|
+
log.info("Deleted memory: %s", memory_id)
|
|
417
|
+
except ClientError as e:
|
|
418
|
+
if e.response["Error"]["Code"] not in ["ResourceNotFoundException", "NotFound"]:
|
|
419
|
+
result.warnings.append(f"Failed to delete memory {memory_id}: {e}")
|
|
420
|
+
log.warning("Failed to delete memory: %s", e)
|
|
421
|
+
else:
|
|
422
|
+
result.warnings.append(f"Memory {memory_id} not found (may have been deleted already)")
|
|
423
|
+
|
|
424
|
+
except Exception as e:
|
|
425
|
+
result.warnings.append(f"Error during memory cleanup: {e}")
|
|
426
|
+
log.warning("Error during memory cleanup: %s", e)
|
|
427
|
+
|
|
428
|
+
|
|
389
429
|
def _destroy_codebuild_iam_role(
|
|
390
430
|
session: boto3.Session,
|
|
391
431
|
agent_config: BedrockAgentCoreAgentSchema,
|
|
@@ -29,6 +29,51 @@ def invoke_bedrock_agentcore(
|
|
|
29
29
|
# Load project configuration
|
|
30
30
|
project_config = load_config(config_path)
|
|
31
31
|
agent_config = project_config.get_agent_config(agent_name)
|
|
32
|
+
|
|
33
|
+
# Check memory status on first invoke if LTM is enabled
|
|
34
|
+
if (
|
|
35
|
+
agent_config.memory
|
|
36
|
+
and agent_config.memory.has_ltm
|
|
37
|
+
and agent_config.memory.memory_id
|
|
38
|
+
and not agent_config.memory.first_invoke_memory_check_done
|
|
39
|
+
):
|
|
40
|
+
try:
|
|
41
|
+
from ...operations.memory.constants import MemoryStatus
|
|
42
|
+
from ...operations.memory.manager import MemoryManager
|
|
43
|
+
|
|
44
|
+
memory_manager = MemoryManager(region_name=agent_config.aws.region)
|
|
45
|
+
memory_status = memory_manager.get_memory_status(agent_config.memory.memory_id)
|
|
46
|
+
|
|
47
|
+
if memory_status != MemoryStatus.ACTIVE.value:
|
|
48
|
+
# Provide graceful error message
|
|
49
|
+
error_message = (
|
|
50
|
+
f"Memory is still provisioning (current status: {memory_status}). "
|
|
51
|
+
f"Long-term memory extraction takes 60-180 seconds to activate.\n\n"
|
|
52
|
+
f"Please wait and check status with:\n"
|
|
53
|
+
f" agentcore status{f' --agent {agent_name}' if agent_name else ''}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Log the message for visibility
|
|
57
|
+
log.warning("Memory not yet active for agent '%s': %s", agent_config.name, memory_status)
|
|
58
|
+
|
|
59
|
+
raise ValueError(error_message)
|
|
60
|
+
|
|
61
|
+
# Memory is active, mark check as done
|
|
62
|
+
agent_config.memory.first_invoke_memory_check_done = True
|
|
63
|
+
project_config.agents[agent_config.name] = agent_config
|
|
64
|
+
save_config(project_config, config_path)
|
|
65
|
+
log.info("Memory is active, proceeding with invoke")
|
|
66
|
+
|
|
67
|
+
except ImportError as e:
|
|
68
|
+
log.error("Failed to import MemoryManager: %s", e)
|
|
69
|
+
# Continue without check if import fails
|
|
70
|
+
except Exception as e:
|
|
71
|
+
# If it's our ValueError, re-raise it
|
|
72
|
+
if "Memory is still provisioning" in str(e):
|
|
73
|
+
raise
|
|
74
|
+
# For other errors, log but continue
|
|
75
|
+
log.warning("Could not check memory status: %s", e)
|
|
76
|
+
|
|
32
77
|
# Log which agent is being invoked
|
|
33
78
|
mode = "locally" if local_mode else "via cloud endpoint"
|
|
34
79
|
log.debug("Invoking BedrockAgentCore agent '%s' %s", agent_config.name, mode)
|