Skip to content

Commit 691a1fb

Browse files
authored
Update DP APIs to support namespace re-design (#449)
* chore: update DP APIs to support namespace re-design * fix: deprecate namespace_prefix --------- Signed-off-by: Nicolas <nickdb@amazon.com>
1 parent 23fd7dc commit 691a1fb

9 files changed

Lines changed: 307 additions & 60 deletions

File tree

src/bedrock_agentcore/_utils/namespace.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,37 @@ def resolve_namespace_templates(
7373
return namespaces
7474

7575
return namespace_templates
76+
77+
78+
def resolve_namespace_prefix_deprecation(
79+
namespace_prefix: Optional[str] = None,
80+
namespace: Optional[str] = None,
81+
) -> Optional[str]:
82+
"""Collapse the deprecated ``namespace_prefix`` kwarg into ``namespace``.
83+
84+
Used by high-level session-manager retrieval helpers that historically took a
85+
``namespace_prefix`` parameter. During the service redesign's grace period,
86+
``namespace`` preserves the pre-redesign string-prefix behavior, so migrating
87+
``namespace_prefix`` callers to ``namespace`` keeps results identical until
88+
allowlisting is removed.
89+
90+
Returns the effective ``namespace`` value (or ``None`` if neither was provided
91+
and the caller supplied ``namespace_path`` instead). When ``namespace_prefix``
92+
is used, emits a ``DeprecationWarning``.
93+
94+
Raises:
95+
ValueError: if both ``namespace_prefix`` and ``namespace`` are provided.
96+
"""
97+
if namespace_prefix is not None and namespace is not None:
98+
raise ValueError("'namespace' and 'namespace_prefix' (deprecated) are mutually exclusive.")
99+
if namespace_prefix is not None:
100+
warnings.warn(
101+
"The 'namespace_prefix' parameter is deprecated and will be removed in a future "
102+
"release. Use 'namespace' for exact-match retrieval (current pre-redesign behavior "
103+
"during the service grace period) or 'namespace_path' for hierarchical path-prefix "
104+
"retrieval.",
105+
DeprecationWarning,
106+
stacklevel=3,
107+
)
108+
return namespace_prefix
109+
return namespace

src/bedrock_agentcore/memory/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,14 @@ session.add_turns([
186186
# Search long-term memories (after memory extraction has occurred)
187187
memories = session.search_long_term_memories(
188188
query="what food does the user like",
189-
namespace_prefix="/food/user-123/",
189+
namespace_path="/food/user-123/",
190190
top_k=5
191191
)
192192

193193
# Or search across multiple users
194194
memories = manager.search_long_term_memories(
195195
query="Food preferences",
196-
namespace_prefix="/food/", # Search all food-related memories
196+
namespace_path="/food/", # Search all food-related memories
197197
top_k=10
198198
)
199199
```
@@ -301,7 +301,7 @@ session2 = manager.create_memory_session(
301301
```python
302302
# List all memory records in a namespace
303303
records = session.list_long_term_memory_records(
304-
namespace_prefix="/user/preferences/user-123/",
304+
namespace_path="/user/preferences/user-123/",
305305
max_results=20
306306
)
307307

@@ -327,7 +327,7 @@ Learn more here!: [Working example](metadata-workflow.ipynb)
327327
# Step 1: Retrieve relevant memories
328328
memories = session.search_long_term_memories(
329329
query="previous discussion",
330-
namespace_prefix="support/facts/session-456/",
330+
namespace_path="support/facts/session-456/",
331331
top_k=5
332332
)
333333

src/bedrock_agentcore/memory/client.py

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from botocore.config import Config
2121
from botocore.exceptions import ClientError
2222

23-
from bedrock_agentcore._utils.namespace import resolve_namespace_templates
23+
from bedrock_agentcore._utils.namespace import build_namespace_params, resolve_namespace_templates
2424
from bedrock_agentcore._utils.snake_case import accept_snake_case_kwargs
2525
from bedrock_agentcore._utils.user_agent import build_user_agent_suffix
2626

@@ -127,8 +127,11 @@ def __getattr__(self, name: str):
127127
# Access any boto3 method directly
128128
client = MemoryClient()
129129
130-
# These calls are forwarded to the appropriate boto3 client
131-
response = client.list_memory_records(memoryId="mem-123", namespace="test/")
130+
# These calls are forwarded to the appropriate boto3 client.
131+
# Use `namespace` for exact match, or `namespace_path` for
132+
# hierarchical path-prefix retrieval.
133+
response = client.list_memory_records(memoryId="mem-123", namespace="/actor/Jane/")
134+
response = client.list_memory_records(memoryId="mem-123", namespace_path="/org/MyOrg/")
132135
metadata = client.get_memory_metadata(memoryId="mem-123")
133136
"""
134137
if name in self._ALLOWED_GMDP_METHODS and hasattr(self.gmdp_client, name):
@@ -308,46 +311,48 @@ def create_memory_and_wait(
308311
raise TimeoutError("Memory %s did not become ACTIVE within %d seconds" % (memory_id, max_wait))
309312

310313
def retrieve_memories(
311-
self, memory_id: str, namespace: str, query: str, actor_id: Optional[str] = None, top_k: int = 3
314+
self,
315+
memory_id: str,
316+
namespace: Optional[str] = None,
317+
query: str = None,
318+
actor_id: Optional[str] = None,
319+
top_k: int = 3,
320+
namespace_path: Optional[str] = None,
312321
) -> List[Dict[str, Any]]:
313-
"""Retrieve relevant memories from a namespace.
322+
"""Retrieve relevant memories using exact match or hierarchical path prefix.
314323
315-
Note: Wildcards (*) are NOT supported in namespaces. You must provide the
316-
exact namespace path with all variables resolved.
324+
Exactly one of ``namespace`` or ``namespace_path`` must be provided.
317325
318326
Args:
319327
memory_id: Memory resource ID
320-
namespace: Exact namespace path (no wildcards)
321-
query: Search query
328+
namespace: Exact namespace to match (e.g., "/actor/Jane/")
329+
query: Search query (required)
322330
actor_id: Optional actor ID (deprecated, use namespace)
323331
top_k: Number of results to return
332+
namespace_path: Hierarchical path prefix (e.g., "/org/team/")
324333
325334
Returns:
326-
List of memory records
327-
328-
Example:
329-
# Correct - exact namespace
330-
memories = client.retrieve_memories(
331-
memory_id="mem-123",
332-
namespace="support/facts/session-456/",
333-
query="customer preferences"
334-
)
335-
336-
# Incorrect - wildcards not supported
337-
# memories = client.retrieve_memories(..., namespace="support/facts/*/", ...)
335+
List of memory records. Returns an empty list if the namespace
336+
arguments are invalid (both provided, neither provided, or contain
337+
wildcards) or if the service call fails.
338338
"""
339-
if "*" in namespace:
340-
logger.error("Wildcards are not supported in namespaces. Please provide exact namespace.")
339+
if query is None:
340+
raise TypeError("retrieve_memories() missing required argument: 'query'")
341+
342+
try:
343+
ns_params = build_namespace_params(namespace, namespace_path)
344+
except ValueError as e:
345+
logger.error(str(e))
341346
return []
342347

348+
ns_value = namespace or namespace_path
349+
343350
try:
344-
# Let service handle all namespace validation
345351
response = self.gmdp_client.retrieve_memory_records(
346-
memoryId=memory_id, namespace=namespace, searchCriteria={"searchQuery": query, "topK": top_k}
352+
memoryId=memory_id, searchCriteria={"searchQuery": query, "topK": top_k}, **ns_params
347353
)
348-
349354
memories = response.get("memoryRecordSummaries", [])
350-
logger.info("Retrieved %d memories from namespace: %s", len(memories), namespace)
355+
logger.info("Retrieved %d memories from namespace: %s", len(memories), ns_value)
351356
return memories
352357

353358
except ClientError as e:
@@ -358,7 +363,7 @@ def retrieve_memories(
358363
logger.warning(
359364
"Memory or namespace not found. Ensure memory %s exists and namespace '%s' is configured",
360365
memory_id,
361-
namespace,
366+
ns_value,
362367
)
363368
elif error_code == "ValidationException":
364369
logger.warning("Invalid search parameters: %s", error_msg)
@@ -663,6 +668,7 @@ def process_turn_with_llm(
663668
retrieval_query: Optional[str] = None,
664669
top_k: int = 3,
665670
event_timestamp: Optional[datetime] = None,
671+
retrieval_namespace_path: Optional[str] = None,
666672
) -> Tuple[List[Dict[str, Any]], str, Dict[str, Any]]:
667673
r"""Complete conversation turn with LLM callback integration.
668674
@@ -677,10 +683,11 @@ def process_turn_with_llm(
677683
llm_callback: Function that takes (user_input, memories) and returns agent_response
678684
The callback receives the user input and retrieved memories,
679685
and should return the agent's response string
680-
retrieval_namespace: Namespace to search for memories (optional)
686+
retrieval_namespace: Namespace for exact match retrieval (optional)
681687
retrieval_query: Custom search query (defaults to user_input)
682688
top_k: Number of memories to retrieve
683689
event_timestamp: Optional timestamp for the event
690+
retrieval_namespace_path: Namespace path for hierarchical prefix retrieval (optional)
684691
685692
Returns:
686693
Tuple of (retrieved_memories, agent_response, created_event)
@@ -710,10 +717,14 @@ def my_llm(user_input: str, memories: List[Dict]) -> str:
710717
"""
711718
# Step 1: Retrieve relevant memories
712719
retrieved_memories = []
713-
if retrieval_namespace:
720+
if retrieval_namespace or retrieval_namespace_path:
714721
search_query = retrieval_query or user_input
715722
retrieved_memories = self.retrieve_memories(
716-
memory_id=memory_id, namespace=retrieval_namespace, query=search_query, top_k=top_k
723+
memory_id=memory_id,
724+
namespace=retrieval_namespace,
725+
namespace_path=retrieval_namespace_path,
726+
query=search_query,
727+
top_k=top_k,
717728
)
718729
logger.info("Retrieved %d memories for LLM context", len(retrieved_memories))
719730

src/bedrock_agentcore/memory/integrations/strands/session_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,7 @@ def retrieve_for_namespace(namespace: str, retrieval_config: RetrievalConfig):
854854

855855
memories = self.memory_client.retrieve_memories(
856856
memory_id=self.config.memory_id,
857-
namespace=resolved_namespace,
857+
namespace_path=resolved_namespace,
858858
query=user_query,
859859
top_k=retrieval_config.top_k,
860860
)

src/bedrock_agentcore/memory/session.py

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from botocore.config import Config as BotocoreConfig
1111
from botocore.exceptions import ClientError
1212

13+
from bedrock_agentcore._utils.namespace import build_namespace_params, resolve_namespace_prefix_deprecation
1314
from bedrock_agentcore._utils.snake_case import accept_snake_case_kwargs
1415

1516
from .constants import BlobMessage, ConversationalMessage, MessageRole, RetrievalConfig
@@ -398,8 +399,9 @@ def _retrieve_memories_for_llm(
398399
strategyId=config.strategy_id or "",
399400
)
400401
search_query = f"{config.retrieval_query} {user_input}" if config.retrieval_query else user_input
402+
# TODO: revisit after full deprecation of namespace fields
401403
memory_records = self.search_long_term_memories(
402-
query=search_query, namespace_prefix=resolved_namespace, top_k=config.top_k
404+
query=search_query, namespace=resolved_namespace, top_k=config.top_k
403405
)
404406
# Filter memory records with a relevance score which is lower than config.relevance_score
405407
if config.relevance_score:
@@ -895,26 +897,45 @@ def delete_event(self, actor_id: str, session_id: str, event_id: str):
895897
def search_long_term_memories(
896898
self,
897899
query: str,
898-
namespace_prefix: str,
900+
namespace_prefix: Optional[str] = None,
899901
top_k: int = 3,
900902
strategy_id: str = None,
901903
max_results: int = 20,
904+
namespace: Optional[str] = None,
905+
namespace_path: Optional[str] = None,
902906
) -> List[MemoryRecord]:
903907
"""Performs a semantic search against the long-term memory for this actor.
904908
905909
Maps to: bedrock-agentcore.retrieve_memory_records.
910+
911+
Exactly one of ``namespace`` (exact match), ``namespace_path`` (hierarchical
912+
path prefix), or ``namespace_prefix`` (DEPRECATED alias for ``namespace``)
913+
must be provided.
914+
915+
Args:
916+
query: Search query
917+
namespace_prefix: DEPRECATED. Use ``namespace`` or ``namespace_path`` instead.
918+
top_k: Number of top-scoring records to return
919+
strategy_id: Optional strategy filter
920+
max_results: Maximum records to return
921+
namespace: Exact-match namespace (preserves pre-redesign behavior during the
922+
service grace period)
923+
namespace_path: Hierarchical path-prefix namespace
906924
"""
907-
logger.info(" -> Querying long-term memory in namespace '%s' with query: '%s'...", namespace_prefix, query)
925+
resolved_namespace = resolve_namespace_prefix_deprecation(namespace_prefix, namespace)
926+
ns_params = build_namespace_params(resolved_namespace, namespace_path)
927+
ns_value = resolved_namespace or namespace_path
928+
929+
logger.info(" -> Querying long-term memory in namespace '%s' with query: '%s'...", ns_value, query)
908930
search_criteria = {"searchQuery": query, "topK": top_k}
909931
if strategy_id:
910932
search_criteria["memoryStrategyId"] = strategy_id
911933

912-
namespace = namespace_prefix
913934
params = {
914935
"memoryId": self._memory_id,
915936
"searchCriteria": search_criteria,
916-
"namespace": namespace,
917937
"maxResults": max_results,
938+
**ns_params,
918939
}
919940

920941
try:
@@ -927,20 +948,41 @@ def search_long_term_memories(
927948
raise
928949

929950
def list_long_term_memory_records(
930-
self, namespace_prefix: str, strategy_id: Optional[str] = None, max_results: int = 10
951+
self,
952+
namespace_prefix: Optional[str] = None,
953+
strategy_id: Optional[str] = None,
954+
max_results: int = 10,
955+
namespace: Optional[str] = None,
956+
namespace_path: Optional[str] = None,
931957
) -> List[MemoryRecord]:
932958
"""Lists all long-term memory records for this actor without a semantic query.
933959
934960
Maps to: bedrock-agentcore.list_memory_records.
961+
962+
Exactly one of ``namespace`` (exact match), ``namespace_path`` (hierarchical
963+
path prefix), or ``namespace_prefix`` (DEPRECATED alias for ``namespace``)
964+
must be provided.
965+
966+
Args:
967+
namespace_prefix: DEPRECATED. Use ``namespace`` or ``namespace_path`` instead.
968+
strategy_id: Optional strategy filter
969+
max_results: Maximum records to return
970+
namespace: Exact-match namespace (preserves pre-redesign behavior during the
971+
service grace period)
972+
namespace_path: Hierarchical path-prefix namespace
935973
"""
936-
logger.info(" -> Listing all long-term records in namespace '%s'...", namespace_prefix)
974+
resolved_namespace = resolve_namespace_prefix_deprecation(namespace_prefix, namespace)
975+
ns_params = build_namespace_params(resolved_namespace, namespace_path)
976+
ns_value = resolved_namespace or namespace_path
977+
978+
logger.info(" -> Listing all long-term records in namespace '%s'...", ns_value)
937979

938980
try:
939981
paginator = self._data_plane_client.get_paginator("list_memory_records")
940982

941983
params = {
942984
"memoryId": self._memory_id,
943-
"namespace": namespace_prefix,
985+
**ns_params,
944986
}
945987

946988
if strategy_id:
@@ -1049,7 +1091,8 @@ def delete_all_long_term_memories_in_namespace(self, namespace: str) -> Dict[str
10491091
logger.info("🗑️ Deleting all long-term memories in namespace '%s'...", namespace)
10501092

10511093
# Retrieve all memory records in the specified namespace
1052-
memory_records = self.list_long_term_memory_records(namespace_prefix=namespace)
1094+
# TODO: revisit after full deprecation of namespace fields
1095+
memory_records = self.list_long_term_memory_records(namespace=namespace)
10531096
logger.info(" -> Found %d memory records to delete", len(memory_records))
10541097

10551098
if not memory_records:
@@ -1203,19 +1246,40 @@ def delete_memory_record(self, record_id: str):
12031246
def search_long_term_memories(
12041247
self,
12051248
query: str,
1206-
namespace_prefix: str,
1249+
namespace_prefix: Optional[str] = None,
12071250
top_k: int = 3,
12081251
strategy_id: Optional[str] = None,
12091252
max_results: int = 20,
1253+
namespace: Optional[str] = None,
1254+
namespace_path: Optional[str] = None,
12101255
) -> List[MemoryRecord]:
12111256
"""Delegates to manager.search_long_term_memories."""
1212-
return self._manager.search_long_term_memories(query, namespace_prefix, top_k, strategy_id, max_results)
1257+
return self._manager.search_long_term_memories(
1258+
query,
1259+
namespace_prefix,
1260+
top_k,
1261+
strategy_id,
1262+
max_results,
1263+
namespace=namespace,
1264+
namespace_path=namespace_path,
1265+
)
12131266

12141267
def list_long_term_memory_records(
1215-
self, namespace_prefix: str, strategy_id: Optional[str] = None, max_results: int = 10
1268+
self,
1269+
namespace_prefix: Optional[str] = None,
1270+
strategy_id: Optional[str] = None,
1271+
max_results: int = 10,
1272+
namespace: Optional[str] = None,
1273+
namespace_path: Optional[str] = None,
12161274
) -> List[MemoryRecord]:
12171275
"""Delegates to manager.list_long_term_memory_records."""
1218-
return self._manager.list_long_term_memory_records(namespace_prefix, strategy_id, max_results)
1276+
return self._manager.list_long_term_memory_records(
1277+
namespace_prefix,
1278+
strategy_id,
1279+
max_results,
1280+
namespace=namespace,
1281+
namespace_path=namespace_path,
1282+
)
12191283

12201284
def list_actors(self) -> List[ActorSummary]:
12211285
"""Delegates to manager.list_actors."""

0 commit comments

Comments
 (0)