From a1e3acc37744be40fd5f76ed34238c9a1e5c39be Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Fri, 1 May 2026 13:44:31 -0400 Subject: [PATCH 1/2] chore: update CP APIs to support namespace re-design --- src/bedrock_agentcore/_utils/namespace.py | 75 ++++++ src/bedrock_agentcore/memory/client.py | 228 ++++++++++++++---- src/bedrock_agentcore/memory/controlplane.py | 11 +- .../memory/integrations/strands/README.md | 6 +- tests/bedrock_agentcore/memory/test_client.py | 14 +- .../memory/test_controlplane.py | 2 +- tests/unit/test_utils.py | 56 +++++ .../integrations/test_session_manager.py | 6 +- 8 files changed, 339 insertions(+), 59 deletions(-) create mode 100644 src/bedrock_agentcore/_utils/namespace.py diff --git a/src/bedrock_agentcore/_utils/namespace.py b/src/bedrock_agentcore/_utils/namespace.py new file mode 100644 index 00000000..112d0186 --- /dev/null +++ b/src/bedrock_agentcore/_utils/namespace.py @@ -0,0 +1,75 @@ +"""Namespace utilities for data plane and control plane API calls.""" + +import warnings +from typing import Dict, List, Optional + + +def build_namespace_params(namespace: Optional[str] = None, namespace_path: Optional[str] = None) -> Dict[str, str]: + """Build the namespace kwargs for a data plane API call. + + Exactly one of ``namespace`` (exact match) or ``namespace_path`` + (hierarchical path prefix) must be provided. Wildcards (``*``) are not + supported in either field. + + Raises: + ValueError: if both arguments are provided, neither is provided, or + the provided value contains a wildcard. + """ + if namespace is not None and namespace_path is not None: + raise ValueError("'namespace' and 'namespace_path' are mutually exclusive.") + if namespace is None and namespace_path is None: + raise ValueError("At least one of 'namespace' or 'namespace_path' must be provided.") + + value = namespace if namespace is not None else namespace_path + if "*" in value: + raise ValueError("Wildcards (*) are not supported in namespaces.") + + if namespace is not None: + return {"namespace": namespace} + return {"namespacePath": namespace_path} + + +def resolve_namespace_templates( + namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, + param_name: str = "namespaces", + new_param_name: Optional[str] = None, +) -> Optional[List[str]]: + """Resolve the deprecated ``namespaces`` kwarg and the new ``namespace_templates`` kwarg. + + Used by control-plane strategy methods. Exactly one (or neither) may be provided. + If the deprecated form is used, a ``DeprecationWarning`` is emitted. Returns the + resolved list, or ``None`` if neither was provided. + + Args: + namespaces: The deprecated parameter value, if any. + namespace_templates: The new parameter value, if any. + param_name: Base name used in error/warning messages for the deprecated form + (e.g. "namespaces" or "reflection_namespaces"). + new_param_name: Name of the replacement form to reference in messages. Defaults + to ``param_name`` with ``"namespaces"`` replaced by ``"namespace_templates"``. + Override when the replacement identifier doesn't follow that pattern (for + example, a dict key like ``reflection_config['namespaceTemplates']``). + + Raises: + ValueError: if both arguments are provided. + """ + if new_param_name is None: + new_param_name = param_name.replace("namespaces", "namespace_templates") + + if namespaces is not None and namespace_templates is not None: + raise ValueError( + f"'{param_name}' and '{new_param_name}' are mutually exclusive. " + f"Prefer '{new_param_name}' ('{param_name}' is deprecated)." + ) + + if namespaces is not None: + warnings.warn( + f"The '{param_name}' parameter is deprecated and will be removed in a future release. " + f"Use '{new_param_name}' instead.", + DeprecationWarning, + stacklevel=3, + ) + return namespaces + + return namespace_templates diff --git a/src/bedrock_agentcore/memory/client.py b/src/bedrock_agentcore/memory/client.py index b74e55c9..0650fcf1 100644 --- a/src/bedrock_agentcore/memory/client.py +++ b/src/bedrock_agentcore/memory/client.py @@ -20,6 +20,7 @@ from botocore.config import Config from botocore.exceptions import ClientError +from bedrock_agentcore._utils.namespace import resolve_namespace_templates from bedrock_agentcore._utils.snake_case import accept_snake_case_kwargs from bedrock_agentcore._utils.user_agent import build_user_agent_suffix @@ -1329,11 +1330,21 @@ def add_semantic_strategy( name: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Add a semantic memory strategy. Note: Configuration is no longer provided for built-in strategies as per API changes. + + Args: + memory_id: The memory resource ID. + name: Strategy name. + description: Optional strategy description. + namespaces: DEPRECATED. Use ``namespace_templates`` instead. + namespace_templates: List of namespace templates for this strategy. """ + resolved_templates = resolve_namespace_templates(namespaces, namespace_templates) + strategy: Dict = { StrategyType.SEMANTIC.value: { "name": name, @@ -1342,8 +1353,8 @@ def add_semantic_strategy( if description: strategy[StrategyType.SEMANTIC.value]["description"] = description - if namespaces: - strategy[StrategyType.SEMANTIC.value]["namespaces"] = namespaces + if resolved_templates: + strategy[StrategyType.SEMANTIC.value]["namespaceTemplates"] = resolved_templates return self._add_strategy(memory_id, strategy) @@ -1353,6 +1364,7 @@ def add_semantic_strategy_and_wait( name: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, max_wait: int = 300, poll_interval: int = 10, ) -> Dict[str, Any]: @@ -1360,9 +1372,24 @@ def add_semantic_strategy_and_wait( This addresses the issue where adding a strategy puts the memory into CREATING state temporarily, preventing subsequent operations. + + Args: + memory_id: The memory resource ID. + name: Strategy name. + description: Optional strategy description. + namespaces: DEPRECATED. Use ``namespace_templates`` instead. + namespace_templates: List of namespace templates for this strategy. + max_wait: Maximum seconds to wait for ACTIVE state. + poll_interval: Seconds between polling attempts. """ # Add the strategy - self.add_semantic_strategy(memory_id, name, description, namespaces) + self.add_semantic_strategy( + memory_id, + name, + description, + namespaces=namespaces, + namespace_templates=namespace_templates, + ) # Wait for memory to return to ACTIVE return self._wait_for_memory_active(memory_id, max_wait, poll_interval) @@ -1373,11 +1400,21 @@ def add_summary_strategy( name: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Add a summary memory strategy. Note: Configuration is no longer provided for built-in strategies as per API changes. + + Args: + memory_id: The memory resource ID. + name: Strategy name. + description: Optional strategy description. + namespaces: DEPRECATED. Use ``namespace_templates`` instead. + namespace_templates: List of namespace templates for this strategy. """ + resolved_templates = resolve_namespace_templates(namespaces, namespace_templates) + strategy: Dict = { StrategyType.SUMMARY.value: { "name": name, @@ -1386,8 +1423,8 @@ def add_summary_strategy( if description: strategy[StrategyType.SUMMARY.value]["description"] = description - if namespaces: - strategy[StrategyType.SUMMARY.value]["namespaces"] = namespaces + if resolved_templates: + strategy[StrategyType.SUMMARY.value]["namespaceTemplates"] = resolved_templates return self._add_strategy(memory_id, strategy) @@ -1397,11 +1434,18 @@ def add_summary_strategy_and_wait( name: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, max_wait: int = 300, poll_interval: int = 10, ) -> Dict[str, Any]: """Add a summary strategy and wait for memory to return to ACTIVE state.""" - self.add_summary_strategy(memory_id, name, description, namespaces) + self.add_summary_strategy( + memory_id, + name, + description, + namespaces=namespaces, + namespace_templates=namespace_templates, + ) return self._wait_for_memory_active(memory_id, max_wait, poll_interval) def add_user_preference_strategy( @@ -1410,11 +1454,21 @@ def add_user_preference_strategy( name: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Add a user preference memory strategy. Note: Configuration is no longer provided for built-in strategies as per API changes. + + Args: + memory_id: The memory resource ID. + name: Strategy name. + description: Optional strategy description. + namespaces: DEPRECATED. Use ``namespace_templates`` instead. + namespace_templates: List of namespace templates for this strategy. """ + resolved_templates = resolve_namespace_templates(namespaces, namespace_templates) + strategy: Dict = { StrategyType.USER_PREFERENCE.value: { "name": name, @@ -1423,8 +1477,8 @@ def add_user_preference_strategy( if description: strategy[StrategyType.USER_PREFERENCE.value]["description"] = description - if namespaces: - strategy[StrategyType.USER_PREFERENCE.value]["namespaces"] = namespaces + if resolved_templates: + strategy[StrategyType.USER_PREFERENCE.value]["namespaceTemplates"] = resolved_templates return self._add_strategy(memory_id, strategy) @@ -1434,41 +1488,64 @@ def add_user_preference_strategy_and_wait( name: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, max_wait: int = 300, poll_interval: int = 10, ) -> Dict[str, Any]: """Add a user preference strategy and wait for memory to return to ACTIVE state.""" - self.add_user_preference_strategy(memory_id, name, description, namespaces) + self.add_user_preference_strategy( + memory_id, + name, + description, + namespaces=namespaces, + namespace_templates=namespace_templates, + ) return self._wait_for_memory_active(memory_id, max_wait, poll_interval) def add_episodic_strategy( self, memory_id: str, name: str, - reflection_namespaces: List[str], + reflection_namespaces: Optional[List[str]] = None, description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, + reflection_namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Add an episodic memory strategy. Args: memory_id: Memory resource ID name: Strategy name - reflection_namespaces: Namespaces for reflections (can be less nested than episode namespaces) + reflection_namespaces: DEPRECATED. Use ``reflection_namespace_templates`` instead. description: Optional description - namespaces: Optional namespaces for episodes + namespaces: DEPRECATED. Use ``namespace_templates`` instead. + namespace_templates: List of namespace templates for episodes. + reflection_namespace_templates: List of namespace templates for reflections (can be + less nested than episode namespace templates). """ + resolved_templates = resolve_namespace_templates(namespaces, namespace_templates) + resolved_reflection_templates = resolve_namespace_templates( + reflection_namespaces, reflection_namespace_templates, param_name="reflection_namespaces" + ) + + if resolved_reflection_templates is None: + raise ValueError( + "add_episodic_strategy requires 'reflection_namespace_templates' (or the deprecated " + "'reflection_namespaces')." + ) + strategy: Dict = { StrategyType.EPISODIC.value: { "name": name, - "reflectionConfiguration": {"namespaces": reflection_namespaces}, + "reflectionConfiguration": {"namespaceTemplates": resolved_reflection_templates}, } } if description: strategy[StrategyType.EPISODIC.value]["description"] = description - if namespaces: - strategy[StrategyType.EPISODIC.value]["namespaces"] = namespaces + if resolved_templates: + strategy[StrategyType.EPISODIC.value]["namespaceTemplates"] = resolved_templates return self._add_strategy(memory_id, strategy) @@ -1476,14 +1553,24 @@ def add_episodic_strategy_and_wait( self, memory_id: str, name: str, - reflection_namespaces: List[str], + reflection_namespaces: Optional[List[str]] = None, description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, + reflection_namespace_templates: Optional[List[str]] = None, max_wait: int = 300, poll_interval: int = 10, ) -> Dict[str, Any]: """Add an episodic strategy and wait for memory to return to ACTIVE state.""" - self.add_episodic_strategy(memory_id, name, reflection_namespaces, description, namespaces) + self.add_episodic_strategy( + memory_id, + name, + reflection_namespaces=reflection_namespaces, + description=description, + namespaces=namespaces, + namespace_templates=namespace_templates, + reflection_namespace_templates=reflection_namespace_templates, + ) return self._wait_for_memory_active(memory_id, max_wait, poll_interval) def add_custom_semantic_strategy( @@ -1494,6 +1581,7 @@ def add_custom_semantic_strategy( consolidation_config: Dict[str, Any], description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Add a custom semantic strategy with prompts. @@ -1505,8 +1593,11 @@ def add_custom_semantic_strategy( consolidation_config: Consolidation configuration with prompt and model: {"prompt": "...", "modelId": "..."} description: Optional description - namespaces: Optional namespaces list + namespaces: DEPRECATED. Use ``namespace_templates`` instead. + namespace_templates: Optional list of namespace templates for this strategy. """ + resolved_templates = resolve_namespace_templates(namespaces, namespace_templates) + strategy = { StrategyType.CUSTOM.value: { "name": name, @@ -1527,8 +1618,8 @@ def add_custom_semantic_strategy( if description: strategy[StrategyType.CUSTOM.value]["description"] = description - if namespaces: - strategy[StrategyType.CUSTOM.value]["namespaces"] = namespaces + if resolved_templates: + strategy[StrategyType.CUSTOM.value]["namespaceTemplates"] = resolved_templates return self._add_strategy(memory_id, strategy) @@ -1540,12 +1631,19 @@ def add_custom_semantic_strategy_and_wait( consolidation_config: Dict[str, Any], description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, max_wait: int = 300, poll_interval: int = 10, ) -> Dict[str, Any]: """Add a custom semantic strategy and wait for memory to return to ACTIVE state.""" self.add_custom_semantic_strategy( - memory_id, name, extraction_config, consolidation_config, description, namespaces + memory_id, + name, + extraction_config, + consolidation_config, + description, + namespaces=namespaces, + namespace_templates=namespace_templates, ) return self._wait_for_memory_active(memory_id, max_wait, poll_interval) @@ -1558,6 +1656,7 @@ def add_custom_episodic_strategy( reflection_config: Dict[str, Any], description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Add a custom episodic strategy with prompts. @@ -1566,9 +1665,12 @@ def add_custom_episodic_strategy( name: Strategy name extraction_config: {"prompt": "...", "modelId": "..."} consolidation_config: {"prompt": "...", "modelId": "..."} - reflection_config: {"prompt": "...", "modelId": "...", "namespaces": [...]} + reflection_config: {"prompt": "...", "modelId": "...", + "namespaceTemplates": [...]} — legacy ``"namespaces"`` key is also accepted + but deprecated. description: Optional description - namespaces: Optional namespaces list + namespaces: DEPRECATED. Use ``namespace_templates`` instead. + namespace_templates: Optional list of namespace templates for this strategy. """ for config, config_name in [ (extraction_config, "extraction_config"), @@ -1579,6 +1681,21 @@ def add_custom_episodic_strategy( if key not in config: raise ValueError(f"{config_name} missing required key: {key}") + resolved_templates = resolve_namespace_templates(namespaces, namespace_templates) + resolved_reflection_templates = resolve_namespace_templates( + reflection_config.get("namespaces"), + reflection_config.get("namespaceTemplates"), + param_name="reflection_config['namespaces']", + new_param_name="reflection_config['namespaceTemplates']", + ) + + reflection_block: Dict[str, Any] = { + "appendToPrompt": reflection_config["prompt"], + "modelId": reflection_config["modelId"], + } + if resolved_reflection_templates is not None: + reflection_block["namespaceTemplates"] = resolved_reflection_templates + strategy = { StrategyType.CUSTOM.value: { "name": name, @@ -1592,15 +1709,7 @@ def add_custom_episodic_strategy( "appendToPrompt": consolidation_config["prompt"], "modelId": consolidation_config["modelId"], }, - "reflection": { - "appendToPrompt": reflection_config["prompt"], - "modelId": reflection_config["modelId"], - **( - {"namespaces": reflection_config["namespaces"]} - if "namespaces" in reflection_config - else {} - ), - }, + "reflection": reflection_block, } }, } @@ -1608,8 +1717,8 @@ def add_custom_episodic_strategy( if description: strategy[StrategyType.CUSTOM.value]["description"] = description - if namespaces: - strategy[StrategyType.CUSTOM.value]["namespaces"] = namespaces + if resolved_templates: + strategy[StrategyType.CUSTOM.value]["namespaceTemplates"] = resolved_templates return self._add_strategy(memory_id, strategy) @@ -1622,12 +1731,20 @@ def add_custom_episodic_strategy_and_wait( reflection_config: Dict[str, Any], description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, max_wait: int = 300, poll_interval: int = 10, ) -> Dict[str, Any]: """Add a custom episodic strategy and wait for memory to return to ACTIVE state.""" self.add_custom_episodic_strategy( - memory_id, name, extraction_config, consolidation_config, reflection_config, description, namespaces + memory_id, + name, + extraction_config, + consolidation_config, + reflection_config, + description, + namespaces=namespaces, + namespace_templates=namespace_templates, ) return self._wait_for_memory_active(memory_id, max_wait, poll_interval) @@ -1637,15 +1754,27 @@ def modify_strategy( strategy_id: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, configuration: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: - """Modify a strategy with full control over configuration.""" + """Modify a strategy with full control over configuration. + + Args: + memory_id: Memory resource ID + strategy_id: Strategy ID to modify + description: Optional new description + namespaces: DEPRECATED. Use ``namespace_templates`` instead. + namespace_templates: Optional new list of namespace templates. + configuration: Optional new configuration. + """ + resolved_templates = resolve_namespace_templates(namespaces, namespace_templates) + modify_config: Dict = {"memoryStrategyId": strategy_id} # Using old field name for input if description is not None: modify_config["description"] = description - if namespaces is not None: - modify_config["namespaces"] = namespaces + if resolved_templates is not None: + modify_config["namespaceTemplates"] = resolved_templates if configuration is not None: modify_config["configuration"] = configuration @@ -1870,6 +1999,14 @@ def _normalize_memory_response(self, memory: Dict[str, Any]) -> Dict[str, Any]: elif "memoryStrategyType" in strategy and "type" not in normalized: normalized["type"] = strategy["memoryStrategyType"] + # Ensure both field name versions exist for namespace templates. + # The service may return either `namespaceTemplates` (new) or `namespaces` + # (deprecated); populate both so caller code reading either key still works. + if "namespaceTemplates" in strategy and "namespaces" not in normalized: + normalized["namespaces"] = strategy["namespaceTemplates"] + elif "namespaces" in strategy and "namespaceTemplates" not in normalized: + normalized["namespaceTemplates"] = strategy["namespaces"] + normalized_strategies.append(normalized) memory["strategies"] = normalized_strategies @@ -1913,7 +2050,11 @@ def _wait_for_memory_active(self, memory_id: str, max_wait: int, poll_interval: raise TimeoutError("Memory %s did not return to ACTIVE state within %d seconds" % (memory_id, max_wait)) def _add_default_namespaces(self, strategies: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """Add default namespaces to strategies that don't have them.""" + """Add default namespace templates to strategies that don't have them. + + Respects either ``namespaceTemplates`` (preferred) or the deprecated + ``namespaces`` key if the caller already provided one. + """ processed = [] for strategy in strategies: @@ -1922,9 +2063,11 @@ def _add_default_namespaces(self, strategies: List[Dict[str, Any]]) -> List[Dict strategy_type_key = list(strategy.keys())[0] strategy_config = strategy_copy[strategy_type_key] - if "namespaces" not in strategy_config: + if "namespaceTemplates" not in strategy_config and "namespaces" not in strategy_config: strategy_type = StrategyType(strategy_type_key) - strategy_config["namespaces"] = DEFAULT_NAMESPACES.get(strategy_type, ["custom/{actorId}/{sessionId}/"]) + strategy_config["namespaceTemplates"] = DEFAULT_NAMESPACES.get( + strategy_type, ["custom/{actorId}/{sessionId}/"] + ) self._validate_strategy_config(strategy_copy, strategy_type_key) @@ -1947,7 +2090,8 @@ def _validate_strategy_config(self, strategy: Dict[str, Any], strategy_type: str """Validate strategy configuration parameters.""" strategy_config = strategy[strategy_type] - namespaces = strategy_config.get("namespaces", []) + # Support both the new `namespaceTemplates` field and the deprecated `namespaces` field + namespaces = strategy_config.get("namespaceTemplates") or strategy_config.get("namespaces", []) for namespace in namespaces: self._validate_namespace(namespace) diff --git a/src/bedrock_agentcore/memory/controlplane.py b/src/bedrock_agentcore/memory/controlplane.py index 2b6a64de..ff8d4457 100644 --- a/src/bedrock_agentcore/memory/controlplane.py +++ b/src/bedrock_agentcore/memory/controlplane.py @@ -13,6 +13,7 @@ import boto3 from botocore.exceptions import ClientError +from .._utils.namespace import resolve_namespace_templates from .constants import ( MemoryStatus, ) @@ -414,6 +415,7 @@ def update_strategy( strategy_id: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, + namespace_templates: Optional[List[str]] = None, configuration: Optional[Dict[str, Any]] = None, wait_for_active: bool = False, max_wait: int = 300, @@ -425,7 +427,8 @@ def update_strategy( memory_id: Memory resource ID strategy_id: Strategy ID to update description: Optional new description - namespaces: Optional new namespaces list + namespaces: DEPRECATED. Use ``namespace_templates`` instead. + namespace_templates: Optional new list of namespace templates configuration: Optional new configuration wait_for_active: Whether to wait for strategy to become ACTIVE max_wait: Maximum seconds to wait if wait_for_active is True @@ -434,14 +437,16 @@ def update_strategy( Returns: Updated memory object """ + resolved_templates = resolve_namespace_templates(namespaces, namespace_templates) + # Note: API expects memoryStrategyId for input but returns strategyId in response modify_config: Dict = {"memoryStrategyId": strategy_id} if description is not None: modify_config["description"] = description - if namespaces is not None: - modify_config["namespaces"] = namespaces + if resolved_templates is not None: + modify_config["namespaceTemplates"] = resolved_templates if configuration is not None: modify_config["configuration"] = configuration diff --git a/src/bedrock_agentcore/memory/integrations/strands/README.md b/src/bedrock_agentcore/memory/integrations/strands/README.md index d7c7b2ff..72ebe2db 100644 --- a/src/bedrock_agentcore/memory/integrations/strands/README.md +++ b/src/bedrock_agentcore/memory/integrations/strands/README.md @@ -112,19 +112,19 @@ comprehensive_memory = client.create_memory_and_wait( { "summaryMemoryStrategy": { "name": "SessionSummarizer", - "namespaces": ["/summaries/{actorId}/{sessionId}/"] + "namespaceTemplates": ["/summaries/{actorId}/{sessionId}/"] } }, { "userPreferenceMemoryStrategy": { "name": "PreferenceLearner", - "namespaces": ["/preferences/{actorId}/"] + "namespaceTemplates": ["/preferences/{actorId}/"] } }, { "semanticMemoryStrategy": { "name": "FactExtractor", - "namespaces": ["/facts/{actorId}/"] + "namespaceTemplates": ["/facts/{actorId}/"] } } ] diff --git a/tests/bedrock_agentcore/memory/test_client.py b/tests/bedrock_agentcore/memory/test_client.py index d5333fd9..ae98d771 100644 --- a/tests/bedrock_agentcore/memory/test_client.py +++ b/tests/bedrock_agentcore/memory/test_client.py @@ -113,7 +113,7 @@ def test_namespace_defaults(): strategies = [{StrategyType.SEMANTIC.value: {"name": "TestStrategy"}}] processed = client._add_default_namespaces(strategies) - assert "namespaces" in processed[0][StrategyType.SEMANTIC.value] + assert "namespaceTemplates" in processed[0][StrategyType.SEMANTIC.value] def test_create_memory(): @@ -872,7 +872,7 @@ def test_add_user_preference_strategy(): user_pref_config = strategy["userPreferenceMemoryStrategy"] assert user_pref_config["name"] == "Test User Preference Strategy" assert user_pref_config["description"] == "User preference test description" - assert user_pref_config["namespaces"] == ["preferences/{actorId}/"] + assert user_pref_config["namespaceTemplates"] == ["preferences/{actorId}/"] # Verify client token and memory ID assert kwargs["memoryId"] == "mem-456" @@ -928,7 +928,7 @@ def test_add_custom_semantic_strategy(): custom_config = strategy["customMemoryStrategy"] assert custom_config["name"] == "Test Custom Semantic Strategy" assert custom_config["description"] == "Custom semantic strategy test description" - assert custom_config["namespaces"] == ["custom/{actorId}/{sessionId}/"] + assert custom_config["namespaceTemplates"] == ["custom/{actorId}/{sessionId}/"] # Verify the semantic override configuration assert "configuration" in custom_config @@ -1496,7 +1496,7 @@ def test_modify_strategy(): modified_strategy = kwargs["memoryStrategies"]["modifyMemoryStrategies"][0] assert modified_strategy["memoryStrategyId"] == "strat-789" assert modified_strategy["description"] == "Modified description" - assert modified_strategy["namespaces"] == ["custom/namespace/"] + assert modified_strategy["namespaceTemplates"] == ["custom/namespace/"] def test_retrieve_memories_resource_not_found_error(): @@ -3143,8 +3143,8 @@ def test_add_episodic_strategy(): episodic_config = strategy["episodicMemoryStrategy"] assert episodic_config["name"] == "Test Episodic Strategy" assert episodic_config["description"] == "Episodic test description" - assert episodic_config["namespaces"] == ["episodes/{actorId}/{sessionId}/"] - assert episodic_config["reflectionConfiguration"] == {"namespaces": ["reflections/{actorId}/"]} + assert episodic_config["namespaceTemplates"] == ["episodes/{actorId}/{sessionId}/"] + assert episodic_config["reflectionConfiguration"] == {"namespaceTemplates": ["reflections/{actorId}/"]} assert kwargs["memoryId"] == "mem-123" @@ -3198,7 +3198,7 @@ def test_add_custom_episodic_strategy(): assert episodic_override["extraction"]["appendToPrompt"] == "Extract episodes from conversation" assert episodic_override["consolidation"]["appendToPrompt"] == "Consolidate episodes" assert episodic_override["reflection"]["appendToPrompt"] == "Generate reflections from episodes" - assert episodic_override["reflection"]["namespaces"] == ["reflections/{actorId}/"] + assert episodic_override["reflection"]["namespaceTemplates"] == ["reflections/{actorId}/"] def test_add_episodic_strategy_and_wait(): diff --git a/tests/bedrock_agentcore/memory/test_controlplane.py b/tests/bedrock_agentcore/memory/test_controlplane.py index 8510840b..5be560a4 100644 --- a/tests/bedrock_agentcore/memory/test_controlplane.py +++ b/tests/bedrock_agentcore/memory/test_controlplane.py @@ -451,7 +451,7 @@ def test_update_strategy(): modify_strategy = kwargs["memoryStrategies"]["modifyMemoryStrategies"][0] assert modify_strategy["memoryStrategyId"] == "strat-456" assert modify_strategy["description"] == "Updated strategy description" - assert modify_strategy["namespaces"] == ["custom/namespace1/", "custom/namespace2/"] + assert modify_strategy["namespaceTemplates"] == ["custom/namespace1/", "custom/namespace2/"] assert modify_strategy["configuration"] == {"modelId": "test-model"} diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 2b40f8b1..09d11b60 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -5,6 +5,7 @@ import pytest from botocore.exceptions import ClientError +from bedrock_agentcore._utils.namespace import build_namespace_params, resolve_namespace_templates from bedrock_agentcore._utils.polling import wait_until, wait_until_deleted @@ -112,3 +113,58 @@ def test_timeout(self, _mock_time, _mock_sleep): poll_fn = Mock(return_value={"status": "DELETING"}) with pytest.raises(TimeoutError): wait_until_deleted(poll_fn) + + +class TestBuildNamespaceParams: + """Tests for build_namespace_params utility.""" + + def test_namespace_only(self): + assert build_namespace_params(namespace="/actor/Jane/") == {"namespace": "/actor/Jane/"} + + def test_namespace_path_only(self): + assert build_namespace_params(namespace_path="/org/team/") == {"namespacePath": "/org/team/"} + + def test_both_raises(self): + with pytest.raises(ValueError, match="mutually exclusive"): + build_namespace_params(namespace="/a/", namespace_path="/b/") + + def test_neither_raises(self): + with pytest.raises(ValueError, match="At least one"): + build_namespace_params() + + def test_wildcard_in_namespace_raises(self): + with pytest.raises(ValueError, match="[Ww]ildcard"): + build_namespace_params(namespace="/actor/*/") + + def test_wildcard_in_namespace_path_raises(self): + with pytest.raises(ValueError, match="[Ww]ildcard"): + build_namespace_params(namespace_path="/org/*/team/") + + +class TestResolveNamespaceTemplates: + """Tests for resolve_namespace_templates utility (CP deprecation handling).""" + + def test_namespace_templates_only(self): + assert resolve_namespace_templates(namespace_templates=["/a/", "/b/"]) == ["/a/", "/b/"] + + def test_namespaces_only_emits_deprecation_warning(self): + with pytest.warns(DeprecationWarning, match="deprecated"): + result = resolve_namespace_templates(namespaces=["/a/"]) + assert result == ["/a/"] + + def test_neither_returns_none(self): + assert resolve_namespace_templates() is None + + def test_both_raises(self): + with pytest.raises(ValueError, match="mutually exclusive"): + resolve_namespace_templates(namespaces=["/a/"], namespace_templates=["/b/"]) + + def test_custom_param_name_in_error(self): + with pytest.raises(ValueError, match="reflection_namespaces.*reflection_namespace_templates"): + resolve_namespace_templates( + namespaces=["/a/"], namespace_templates=["/b/"], param_name="reflection_namespaces" + ) + + def test_custom_param_name_in_warning(self): + with pytest.warns(DeprecationWarning, match="reflection_namespaces"): + resolve_namespace_templates(namespaces=["/a/"], param_name="reflection_namespaces") diff --git a/tests_integ/memory/integrations/test_session_manager.py b/tests_integ/memory/integrations/test_session_manager.py index fbdad214..f420d0bd 100644 --- a/tests_integ/memory/integrations/test_session_manager.py +++ b/tests_integ/memory/integrations/test_session_manager.py @@ -72,16 +72,16 @@ def test_memory_ltm(self, memory_client): { "summaryMemoryStrategy": { "name": "SessionSummarizer", - "namespaces": ["/summaries/{actorId}/{sessionId}/"], + "namespaceTemplates": ["/summaries/{actorId}/{sessionId}/"], } }, { "userPreferenceMemoryStrategy": { "name": "PreferenceLearner", - "namespaces": ["/preferences/{actorId}/"], + "namespaceTemplates": ["/preferences/{actorId}/"], } }, - {"semanticMemoryStrategy": {"name": "FactExtractor", "namespaces": ["/facts/{actorId}/"]}}, + {"semanticMemoryStrategy": {"name": "FactExtractor", "namespaceTemplates": ["/facts/{actorId}/"]}}, ], ) yield memory From 30d789f6a018e13f0b661fbd48e8d88d24f9f000 Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Fri, 1 May 2026 14:55:57 -0400 Subject: [PATCH 2/2] fix: maintain original position arguments for updated methods --- src/bedrock_agentcore/memory/client.py | 16 ++++++++-------- src/bedrock_agentcore/memory/controlplane.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/bedrock_agentcore/memory/client.py b/src/bedrock_agentcore/memory/client.py index 0650fcf1..f09b12b5 100644 --- a/src/bedrock_agentcore/memory/client.py +++ b/src/bedrock_agentcore/memory/client.py @@ -1364,9 +1364,9 @@ def add_semantic_strategy_and_wait( name: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, - namespace_templates: Optional[List[str]] = None, max_wait: int = 300, poll_interval: int = 10, + namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Add a semantic strategy and wait for memory to return to ACTIVE state. @@ -1434,9 +1434,9 @@ def add_summary_strategy_and_wait( name: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, - namespace_templates: Optional[List[str]] = None, max_wait: int = 300, poll_interval: int = 10, + namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Add a summary strategy and wait for memory to return to ACTIVE state.""" self.add_summary_strategy( @@ -1488,9 +1488,9 @@ def add_user_preference_strategy_and_wait( name: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, - namespace_templates: Optional[List[str]] = None, max_wait: int = 300, poll_interval: int = 10, + namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Add a user preference strategy and wait for memory to return to ACTIVE state.""" self.add_user_preference_strategy( @@ -1556,10 +1556,10 @@ def add_episodic_strategy_and_wait( reflection_namespaces: Optional[List[str]] = None, description: Optional[str] = None, namespaces: Optional[List[str]] = None, - namespace_templates: Optional[List[str]] = None, - reflection_namespace_templates: Optional[List[str]] = None, max_wait: int = 300, poll_interval: int = 10, + namespace_templates: Optional[List[str]] = None, + reflection_namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Add an episodic strategy and wait for memory to return to ACTIVE state.""" self.add_episodic_strategy( @@ -1631,9 +1631,9 @@ def add_custom_semantic_strategy_and_wait( consolidation_config: Dict[str, Any], description: Optional[str] = None, namespaces: Optional[List[str]] = None, - namespace_templates: Optional[List[str]] = None, max_wait: int = 300, poll_interval: int = 10, + namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Add a custom semantic strategy and wait for memory to return to ACTIVE state.""" self.add_custom_semantic_strategy( @@ -1731,9 +1731,9 @@ def add_custom_episodic_strategy_and_wait( reflection_config: Dict[str, Any], description: Optional[str] = None, namespaces: Optional[List[str]] = None, - namespace_templates: Optional[List[str]] = None, max_wait: int = 300, poll_interval: int = 10, + namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Add a custom episodic strategy and wait for memory to return to ACTIVE state.""" self.add_custom_episodic_strategy( @@ -1754,8 +1754,8 @@ def modify_strategy( strategy_id: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, - namespace_templates: Optional[List[str]] = None, configuration: Optional[Dict[str, Any]] = None, + namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Modify a strategy with full control over configuration. diff --git a/src/bedrock_agentcore/memory/controlplane.py b/src/bedrock_agentcore/memory/controlplane.py index ff8d4457..4dd70267 100644 --- a/src/bedrock_agentcore/memory/controlplane.py +++ b/src/bedrock_agentcore/memory/controlplane.py @@ -415,11 +415,11 @@ def update_strategy( strategy_id: str, description: Optional[str] = None, namespaces: Optional[List[str]] = None, - namespace_templates: Optional[List[str]] = None, configuration: Optional[Dict[str, Any]] = None, wait_for_active: bool = False, max_wait: int = 300, poll_interval: int = 10, + namespace_templates: Optional[List[str]] = None, ) -> Dict[str, Any]: """Update a strategy in a memory resource.