Skip to content

Commit 857ef2e

Browse files
committed
add metadata support for LTM
1 parent b64a0d9 commit 857ef2e

7 files changed

Lines changed: 897 additions & 6 deletions

File tree

src/bedrock_agentcore/memory/client.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
Role,
3838
StrategyType,
3939
)
40-
from .models.filters import EventMetadataFilter, MetadataValue
40+
from .models.filters import EventMetadataFilter, IndexedKey, MemoryMetadataFilter, MetadataValue
4141

4242
logger = logging.getLogger(__name__)
4343

@@ -160,8 +160,25 @@ def create_memory(
160160
event_expiry_days: int = 90,
161161
memory_execution_role_arn: Optional[str] = None,
162162
stream_delivery_resources: Optional[Dict[str, Any]] = None,
163+
indexed_keys: Optional[List[IndexedKey]] = None,
163164
) -> Dict[str, Any]:
164-
"""Create a memory with simplified configuration."""
165+
"""Create a memory with simplified configuration.
166+
167+
Args:
168+
name: Name for the memory resource
169+
strategies: Optional list of strategy configurations
170+
description: Optional description
171+
event_expiry_days: How long to retain events (default: 90 days)
172+
memory_execution_role_arn: IAM role ARN for memory execution
173+
stream_delivery_resources: Optional delivery configuration for streaming memory records
174+
indexed_keys: Optional list of metadata keys to index for filtering.
175+
Each entry should have 'key' (str) and 'type' ('STRING', 'STRINGLIST', or 'NUMBER').
176+
Once declared, indexed keys cannot be removed.
177+
Example: [{"key": "priority", "type": "NUMBER"}, {"key": "agent_type", "type": "STRING"}]
178+
179+
Returns:
180+
Created memory object
181+
"""
165182
if strategies is None:
166183
strategies = []
167184

@@ -184,6 +201,9 @@ def create_memory(
184201
if stream_delivery_resources is not None:
185202
params["streamDeliveryResources"] = stream_delivery_resources
186203

204+
if indexed_keys is not None:
205+
params["indexedKeys"] = indexed_keys
206+
187207
response = self.gmcp_client.create_memory(**params)
188208

189209
memory = response["memory"]
@@ -205,9 +225,21 @@ def create_or_get_memory(
205225
event_expiry_days: int = 90,
206226
memory_execution_role_arn: Optional[str] = None,
207227
stream_delivery_resources: Optional[Dict[str, Any]] = None,
228+
indexed_keys: Optional[List[IndexedKey]] = None,
208229
) -> Dict[str, Any]:
209230
"""Create a memory resource or fetch the existing memory details if it already exists.
210231
232+
Args:
233+
name: Name for the memory resource
234+
strategies: Optional list of strategy configurations
235+
description: Optional description
236+
event_expiry_days: How long to retain events (default: 90 days)
237+
memory_execution_role_arn: IAM role ARN for memory execution
238+
stream_delivery_resources: Optional delivery configuration for streaming memory records
239+
indexed_keys: Optional list of metadata keys to index for filtering.
240+
Once declared, indexed keys cannot be removed; new keys can be added
241+
via `update_memory(addIndexedKeys=...)`.
242+
211243
Returns:
212244
Memory object, either newly created or existing
213245
"""
@@ -219,6 +251,7 @@ def create_or_get_memory(
219251
event_expiry_days=event_expiry_days,
220252
memory_execution_role_arn=memory_execution_role_arn,
221253
stream_delivery_resources=stream_delivery_resources,
254+
indexed_keys=indexed_keys,
222255
)
223256
return memory
224257
except ClientError as e:
@@ -241,6 +274,7 @@ def create_memory_and_wait(
241274
event_expiry_days: int = 90,
242275
memory_execution_role_arn: Optional[str] = None,
243276
stream_delivery_resources: Optional[Dict[str, Any]] = None,
277+
indexed_keys: Optional[List[IndexedKey]] = None,
244278
max_wait: int = 300,
245279
poll_interval: int = 10,
246280
) -> Dict[str, Any]:
@@ -256,6 +290,10 @@ def create_memory_and_wait(
256290
event_expiry_days: How long to retain events (default: 90 days)
257291
memory_execution_role_arn: IAM role ARN for memory execution
258292
stream_delivery_resources: Optional delivery configuration for streaming memory records
293+
indexed_keys: Optional list of metadata keys to index for filtering.
294+
Each entry should have 'key' (str) and 'type' ('STRING', 'STRINGLIST', or 'NUMBER').
295+
Once declared, indexed keys cannot be removed; new keys can be added
296+
via `update_memory(addIndexedKeys=...)`.
259297
max_wait: Maximum seconds to wait (default: 300)
260298
poll_interval: Seconds between status checks (default: 10)
261299
@@ -274,6 +312,7 @@ def create_memory_and_wait(
274312
event_expiry_days=event_expiry_days,
275313
memory_execution_role_arn=memory_execution_role_arn,
276314
stream_delivery_resources=stream_delivery_resources,
315+
indexed_keys=indexed_keys,
277316
)
278317

279318
memory_id = memory.get("memoryId", memory.get("id")) # Handle both field names
@@ -318,6 +357,7 @@ def retrieve_memories(
318357
actor_id: Optional[str] = None,
319358
top_k: int = 3,
320359
namespace_path: Optional[str] = None,
360+
metadata_filters: Optional[List[MemoryMetadataFilter]] = None,
321361
) -> List[Dict[str, Any]]:
322362
"""Retrieve relevant memories using exact match or hierarchical path prefix.
323363
@@ -330,15 +370,29 @@ def retrieve_memories(
330370
actor_id: Optional actor ID (deprecated, use namespace)
331371
top_k: Number of results to return
332372
namespace_path: Hierarchical path prefix (e.g., "/org/team/")
373+
metadata_filters: Optional list of metadata filter expressions to scope results.
374+
Use MemoryMetadataFilter.build_expression() to construct filters.
375+
The service accepts 1-5 filters. An empty list is treated as no filter.
376+
Example: [MemoryMetadataFilter.build_expression(
377+
MemoryRecordLeftExpression.build("priority"),
378+
MemoryRecordOperatorType.EQUALS_TO,
379+
MemoryRecordRightExpression.build_string("high"),
380+
)]
333381
334382
Returns:
335383
List of memory records. Returns an empty list if the namespace
336384
arguments are invalid (both provided, neither provided, or contain
337385
wildcards) or if the service call fails.
386+
387+
Raises:
388+
ValueError: If `metadata_filters` exceeds the service maximum of 5.
338389
"""
339390
if query is None:
340391
raise TypeError("retrieve_memories() missing required argument: 'query'")
341392

393+
if metadata_filters is not None and len(metadata_filters) > 5:
394+
raise ValueError(f"metadata_filters supports a maximum of 5 expressions; received {len(metadata_filters)}.")
395+
342396
try:
343397
ns_params = build_namespace_params(namespace, namespace_path)
344398
except ValueError as e:
@@ -348,8 +402,13 @@ def retrieve_memories(
348402
ns_value = namespace or namespace_path
349403

350404
try:
405+
search_criteria = {"searchQuery": query, "topK": top_k}
406+
if metadata_filters:
407+
search_criteria["metadataFilters"] = metadata_filters
408+
logger.debug("Applying %d metadata filter(s)", len(metadata_filters))
409+
351410
response = self.gmdp_client.retrieve_memory_records(
352-
memoryId=memory_id, searchCriteria={"searchQuery": query, "topK": top_k}, **ns_params
411+
memoryId=memory_id, searchCriteria=search_criteria, **ns_params
353412
)
354413
memories = response.get("memoryRecordSummaries", [])
355414
logger.info("Retrieved %d memories from namespace: %s", len(memories), ns_value)

src/bedrock_agentcore/memory/models/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@
55
from .DictWrapper import DictWrapper
66
from .filters import (
77
EventMetadataFilter,
8+
IndexedKey,
89
LeftExpression,
10+
MemoryMetadataFilter,
11+
MemoryRecordLeftExpression,
12+
MemoryRecordOperatorType,
13+
MemoryRecordRightExpression,
914
MetadataKey,
1015
MetadataValue,
16+
MetadataValueType,
1117
OperatorType,
1218
RightExpression,
1319
StringValue,
@@ -101,4 +107,10 @@ def __init__(self, session_summary: Dict[str, Any]):
101107
"OperatorType",
102108
"RightExpression",
103109
"EventMetadataFilter",
110+
"MemoryRecordOperatorType",
111+
"MemoryRecordLeftExpression",
112+
"MemoryRecordRightExpression",
113+
"MemoryMetadataFilter",
114+
"MetadataValueType",
115+
"IndexedKey",
104116
]

0 commit comments

Comments
 (0)