From d7c9ba0628b325b430433b0bca933ccde6e9b1f5 Mon Sep 17 00:00:00 2001 From: Sukruth Gowdru Lingaraju Date: Sat, 8 Nov 2025 14:20:12 -0500 Subject: [PATCH 1/4] fix: list_events having branch & eventMetadata filter --- src/bedrock_agentcore/memory/session.py | 12 ++++++++---- tests/bedrock_agentcore/memory/test_session.py | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/bedrock_agentcore/memory/session.py b/src/bedrock_agentcore/memory/session.py index 8ab6998..14d688d 100644 --- a/src/bedrock_agentcore/memory/session.py +++ b/src/bedrock_agentcore/memory/session.py @@ -653,15 +653,19 @@ def list_events( if next_token: params["nextToken"] = next_token + # Initialize the filterMap + filterMap = {} + # Add branch filter if specified (but not for "main") if branch_name and branch_name != "main": - params["filter"] = { - "branch": {"name": branch_name, "includeParentBranches": include_parent_branches} - } + filterMap["branch"] = {"name": branch_name, "includeParentBranches": include_parent_branches} # Add eventMetadata filter if specified if eventMetadata: - params["filter"] = {"eventMetadata": eventMetadata} + filterMap["eventMetadata"] = eventMetadata + + if filterMap: + params["filter"] = filterMap response = self._data_plane_client.list_events(**params) diff --git a/tests/bedrock_agentcore/memory/test_session.py b/tests/bedrock_agentcore/memory/test_session.py index 27d70cb..549d346 100644 --- a/tests/bedrock_agentcore/memory/test_session.py +++ b/tests/bedrock_agentcore/memory/test_session.py @@ -2505,8 +2505,8 @@ def test_list_events_with_both_branch_and_metadata_filters(self): call_args = mock_client_instance.list_events.call_args[1] assert "filter" in call_args assert call_args["filter"]["eventMetadata"] == event_metadata_filter - # Branch filter should not be present when eventMetadata is specified - assert "branch" not in call_args["filter"] + # Branch filter should be present when eventMetadata is specified + assert "branch" in call_args["filter"] def test_memory_session_list_events_with_event_metadata(self): """Test MemorySession.list_events with eventMetadata parameter.""" From dddaaa872f4d7861072d76aa28affd7fd47dee32 Mon Sep 17 00:00:00 2001 From: Sukruth Gowdru Lingaraju Date: Mon, 10 Nov 2025 14:22:22 -0500 Subject: [PATCH 2/4] feat: add documentation for metadata support in STM --- pyproject.toml | 1 + src/bedrock_agentcore/memory/README.md | 7 + .../memory/metadata-workflow.ipynb | 406 ++++++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 src/bedrock_agentcore/memory/metadata-workflow.ipynb diff --git a/pyproject.toml b/pyproject.toml index 94b9d3f..6e25421 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ select = [ [tool.ruff.lint.per-file-ignores] "!src/**/*.py" = ["D"] +"src/bedrock_agentcore/memory/metadata-workflow.ipynb" = ["E501"] [tool.ruff.lint.pydocstyle] convention = "google" diff --git a/src/bedrock_agentcore/memory/README.md b/src/bedrock_agentcore/memory/README.md index d986125..615a678 100644 --- a/src/bedrock_agentcore/memory/README.md +++ b/src/bedrock_agentcore/memory/README.md @@ -19,6 +19,7 @@ conversation handling. - [Branch Management](#branch-management) - [Session and Actor Management](#session-and-actor-management) - [Memory Record Management](#memory-record-management) + - [Event Management with Metadata](#event-management-with-metadata) - [Alternative Pattern: Separated Operations](#alternative-pattern-separated-operations) - [Error Handling](#error-handling) - [Common Exceptions](#common-exceptions) @@ -312,6 +313,12 @@ print(f"Record content: {record.content}") session.delete_memory_record("record-id-123") ``` +### Event Management with Metadata + +Events can now be managed by defining custom metadata. + +Learn more here!: [Working example](src/bedrock_agentcore/memory/metadata-workflow.ipynb) + ### Alternative Pattern: Separated Operations ```python diff --git a/src/bedrock_agentcore/memory/metadata-workflow.ipynb b/src/bedrock_agentcore/memory/metadata-workflow.ipynb new file mode 100644 index 0000000..3ef0118 --- /dev/null +++ b/src/bedrock_agentcore/memory/metadata-workflow.ipynb @@ -0,0 +1,406 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dda1a609", + "metadata": {}, + "source": [ + "### Metadata in Short-Term Memory" + ] + }, + { + "cell_type": "markdown", + "id": "56d5d8ce", + "metadata": {}, + "source": [ + "Event metadata lets you attach additional context information to your short-term memory events as key-value pairs. When creating events using the CreateEvent operation, you can include metadata that isn't part of the core event content but provides valuable context for retrieval. For example, a travel booking agent can attach location metadata to events, making it easy to find all conversations that mentioned specific destinations. You can then use the ListEvents operation with metadata filters to efficiently retrieve events based on these attached properties, enabling your agent to quickly locate relevant conversation history without scanning through entire sessions. This capability is useful for agents that need to track and retrieve specific attributes across conversations, such as product categories in e-commerce, case types in customer support, or project identifiers in task management applications. Event metadata is not meant to store sensitive content, as it is not encrypted with customer managed key." + ] + }, + { + "cell_type": "markdown", + "id": "e434dd7f", + "metadata": {}, + "source": [ + "Below is a short workflow on how metadata can be attached when creating events along with filtering the conversational history to retrieve relevant memories based on varying conditions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b003e91c", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "from typing import Optional\n", + "\n", + "from bedrock_agentcore_starter_toolkit.operations.memory.manager import MemoryManager\n", + "\n", + "from bedrock_agentcore.memory import MemorySessionManager\n", + "from bedrock_agentcore.memory.constants import ConversationalMessage, MessageRole\n", + "from bedrock_agentcore.memory.models import (\n", + " EventMetadataFilter,\n", + " LeftExpression,\n", + " OperatorType,\n", + " RightExpression,\n", + " StringValue,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "de576a5c", + "metadata": {}, + "source": [ + "#### Setting up Memory Resources" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec4707ee", + "metadata": {}, + "outputs": [], + "source": [ + "region = \"us-west-2\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa7d9d82", + "metadata": {}, + "outputs": [], + "source": [ + "memory_manager = MemoryManager(region_name=region)\n", + "\n", + "memory = memory_manager.get_or_create_memory(name=\"travel_support_agent_1\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d20a3b02", + "metadata": {}, + "outputs": [], + "source": [ + "session_manager = MemorySessionManager(memory_id=memory[\"id\"], region_name=region)\n", + "\n", + "session = session_manager.create_memory_session(actor_id=\"user-123\", session_id=\"session-1\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6235dfe", + "metadata": {}, + "outputs": [], + "source": [ + "event_1 = [\n", + " ConversationalMessage(\n", + " \"I am planning to travel to the US next Summer, can you help me plan my trip!\", MessageRole.USER\n", + " ),\n", + " ConversationalMessage(\n", + " \"That's great to hear! I'd be happy to help you plan the trip. What would be the first city you'd like to visit in the US?\",\n", + " MessageRole.ASSISTANT,\n", + " ),\n", + " ConversationalMessage(\n", + " \"I am planning on starting off my summer vacation in NYC! I will be visting for 5 days!\", MessageRole.USER\n", + " ),\n", + "]\n", + "\n", + "metadata_1 = {\"location\": StringValue.build(\"NYC\"), \"season\": StringValue.build(\"Summer\")}\n", + "session.add_turns(event_1, metadata=metadata_1)\n", + "time.sleep(2) # To avoid being throttled\n", + "\n", + "event_2 = [\n", + " ConversationalMessage(\n", + " \"For outdoor experiences you can consider visiting the Central Park, Brooklyn Bridge Park, The High Line.\",\n", + " MessageRole.ASSISTANT,\n", + " ),\n", + " ConversationalMessage(\n", + " \"That's great to hear, what are some of the classic summer activities I can do?\", MessageRole.USER\n", + " ),\n", + " ConversationalMessage(\n", + " \"For classic summer activities, you can try visiting: Coney Island, Yankees Game, Statue of Liberty!\",\n", + " MessageRole.ASSISTANT,\n", + " ),\n", + " ConversationalMessage(\"Thank you for helping me in providing these suggestions\", MessageRole.USER),\n", + "]\n", + "\n", + "metadata_2 = {\n", + " \"location\": StringValue.build(\"NYC\"),\n", + " \"season\": StringValue.build(\"Summer\"),\n", + " \"attractions\": StringValue.build(\"Central Park/Brooklyn Bridge Park/High Line\"),\n", + " \"activities\": StringValue.build(\"Coney Island/Yankees Game/Statue of Liberty\"),\n", + "}\n", + "session.add_turns(event_2, metadata=metadata_2)\n", + "time.sleep(2) # To avoid being throttled\n", + "\n", + "event_3 = [\n", + " ConversationalMessage(\"After NYC, where would you like to visit next!?\", MessageRole.ASSISTANT),\n", + " ConversationalMessage(\"I would be visiting Chicago next!\", MessageRole.USER),\n", + " ConversationalMessage(\n", + " \"Would you like me to provide you suggestion on how to spend time in Chicago?\", MessageRole.ASSISTANT\n", + " ),\n", + " ConversationalMessage(\"Yes! However, I would be visiting in Chicago for just 2 days!\", MessageRole.USER),\n", + "]\n", + "\n", + "metadata_3 = {\"location\": StringValue.build(\"Chicago\"), \"season\": StringValue.build(\"Summer\")}\n", + "session.add_turns(event_3, metadata=metadata_3)\n", + "time.sleep(2) # To avoid being throttled\n", + "\n", + "event_4 = [\n", + " ConversationalMessage(\n", + " \"Great! Since your visit is short, you can visting the Millennium Park, Skydeck, Chicago Riverwalk!\",\n", + " MessageRole.ASSISTANT,\n", + " ),\n", + " ConversationalMessage(\"Thank you for the suggestion!\", MessageRole.USER),\n", + "]\n", + "\n", + "metadata_4 = {\n", + " \"location\": StringValue.build(\"Chicago\"),\n", + " \"season\": StringValue.build(\"Summer\"),\n", + " \"attractions\": StringValue.build(\"Millennium Park/Skydeck/Chicago Riverwalk\"),\n", + "}\n", + "session.add_turns(event_4, metadata=metadata_4)\n", + "time.sleep(2) # To avoid being throttled" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0724059d", + "metadata": {}, + "outputs": [], + "source": [ + "events = session.list_events()\n", + "for index, event in enumerate(events, start=1):\n", + " print(f\"=== Event #{index} ===\")\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "id": "7c829b97", + "metadata": {}, + "source": [ + "#### Listing Events with Metadata Filter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d67058f", + "metadata": {}, + "outputs": [], + "source": [ + "def build_metadata_filter(key: str, operator: OperatorType, val: Optional[str] = None) -> EventMetadataFilter:\n", + " params = {\"left_operand\": LeftExpression.build(key=key), \"operator\": operator}\n", + " if val:\n", + " params[\"right_operand\"] = RightExpression.build(value=val)\n", + " return EventMetadataFilter.build_expression(**params)" + ] + }, + { + "cell_type": "markdown", + "id": "d5171699", + "metadata": {}, + "source": [ + "##### Listing events based on a key-value pairs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "589ff9d4", + "metadata": {}, + "outputs": [], + "source": [ + "# Example: location = \"NYC\"\n", + "\n", + "metadata_filter_1 = build_metadata_filter(key=\"location\", operator=OperatorType.EQUALS_TO, val=\"NYC\")\n", + "\n", + "filtered_events_1 = session.list_events(eventMetadata=[metadata_filter_1])\n", + "\n", + "print(\"=== Listing events with metadata filter, where: key = value ===\")\n", + "for index, event in enumerate(filtered_events_1, start=1):\n", + " print(f\"=== Event #{index} ===\")\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "id": "dfde1da8", + "metadata": {}, + "source": [ + "##### Listing events based on key existence" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52c41d4d", + "metadata": {}, + "outputs": [], + "source": [ + "# Example: exists(key) = \"attractions\"\n", + "\n", + "metadata_filter_2 = build_metadata_filter(key=\"attractions\", operator=OperatorType.EXISTS)\n", + "\n", + "filtered_events_2 = session.list_events(eventMetadata=[metadata_filter_2])\n", + "\n", + "print(\"=== Listing events with metadata filter, where: exists(key) ===\")\n", + "for index, event in enumerate(filtered_events_2, start=1):\n", + " print(f\"=== Event #{index} ===\")\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "id": "0935d112", + "metadata": {}, + "source": [ + "##### Listing events based on key non-existence" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b5afdeb", + "metadata": {}, + "outputs": [], + "source": [ + "# Example: does_not_exist(key) = \"activites\"\n", + "# Note: In the above 4 events created, only 1 event consists of the key \"activites\" present in its metadata.\n", + "# The below listEvents query should return the remaining events.\n", + "\n", + "metadata_filter_3 = build_metadata_filter(key=\"activities\", operator=OperatorType.NOT_EXISTS)\n", + "\n", + "filtered_events_3 = session.list_events(eventMetadata=[metadata_filter_3])\n", + "\n", + "print(\"=== Listing events with metadata filter, where: does_not_exist(key) ===\")\n", + "for index, event in enumerate(filtered_events_3, start=1):\n", + " print(f\"=== Event #{index} ===\")\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "id": "e14dbe3e", + "metadata": {}, + "source": [ + "#### Listing Events with branch and metadata filters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eaea20a2", + "metadata": {}, + "outputs": [], + "source": [ + "# Let's branch off of Event #2\n", + "root_event = events[2]\n", + "\n", + "branched_event = [\n", + " ConversationalMessage(\"After NYC, where would you like to visit next!?\", MessageRole.ASSISTANT),\n", + " ConversationalMessage(\n", + " \"Actually, I changed my mind. I will be visiting NYC during next winter. Could you provide me suggestions on places to visit here?\",\n", + " MessageRole.USER,\n", + " ),\n", + " ConversationalMessage(\n", + " \"I would be glad to help you! You can visit the iconic Rockefeller Center that has christmas decorations and trees, and also go ice-skating in the Bryant Park\",\n", + " MessageRole.ASSISTANT,\n", + " ),\n", + " ConversationalMessage(\"Thank you for the suggestion\", MessageRole.USER),\n", + "]\n", + "\n", + "branched_event_metadata = {\"location\": StringValue.build(\"NYC\"), \"season\": StringValue.build(\"Winter\")}\n", + "branch_name = \"branch-1\"\n", + "branch = {\"rootEventId\": root_event[\"eventId\"], \"name\": branch_name}\n", + "\n", + "session.add_turns(branched_event, branch=branch, metadata=branched_event_metadata)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01a32040", + "metadata": {}, + "outputs": [], + "source": [ + "branch_name = \"branch-1\"\n", + "# List all the events in \"branch-1\"\n", + "filtered_events_4 = session.list_events(branch_name=branch_name, include_parent_branches=True)\n", + "\n", + "print(f\"=== Listing events in branch: {branch_name} ===\")\n", + "for index, event in enumerate(filtered_events_4, start=1):\n", + " print(f\"=== Event #{index} ===\")\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "id": "09f54720", + "metadata": {}, + "source": [ + "##### Listing events with multiple metadata filters\n", + "\n", + "The ListEvents API accepts a list of metadata filters. \n", + "\n", + "When there exists more than one metadata filter, an implicit `AND` operation is performed on the metadata filters provided. \n", + "This implies only the retrieval of events that meet the conditions of all the metadata filters that are provided. \n", + "\n", + "Below is an example of metadata filtering with more than one metadata filter + branch filtering" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f84cf6f", + "metadata": {}, + "outputs": [], + "source": [ + "# Example:\n", + "# Let us consider two metadata filters to be provided when listing events.\n", + "# key1 = \"location\", value1= \"NYC\"\n", + "# key2 = \"Season\", value1= \"Winter\"\n", + "\n", + "metadata_filter_4 = build_metadata_filter(key=\"location\", operator=OperatorType.EQUALS_TO, val=\"NYC\")\n", + "\n", + "metadata_filter_5 = build_metadata_filter(key=\"season\", operator=OperatorType.EQUALS_TO, val=\"Winter\")\n", + "\n", + "filtered_events_5 = session.list_events(\n", + " branch_name=branch_name, include_parent_branches=True, eventMetadata=[metadata_filter_4, metadata_filter_5]\n", + ")\n", + "\n", + "print(\"=== Listing events with branch and metadata filters ===\")\n", + "for index, event in enumerate(filtered_events_5, start=1):\n", + " print(f\"=== Event #{index} ===\")\n", + " print(event)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "agentcore-sdk", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 91cc050ceb2489ec1dd1a637d8840960ed2c1653 Mon Sep 17 00:00:00 2001 From: Sukruth Gowdru Lingaraju Date: Mon, 24 Nov 2025 15:52:32 -0500 Subject: [PATCH 3/4] fix: metadata workflow link --- src/bedrock_agentcore/memory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bedrock_agentcore/memory/README.md b/src/bedrock_agentcore/memory/README.md index 615a678..a698ceb 100644 --- a/src/bedrock_agentcore/memory/README.md +++ b/src/bedrock_agentcore/memory/README.md @@ -317,7 +317,7 @@ session.delete_memory_record("record-id-123") Events can now be managed by defining custom metadata. -Learn more here!: [Working example](src/bedrock_agentcore/memory/metadata-workflow.ipynb) +Learn more here!: [Working example](metadata-workflow.ipynb) ### Alternative Pattern: Separated Operations From 90d03d4891b24ae56a9c51f728a7199bb20c8962 Mon Sep 17 00:00:00 2001 From: Sukruth Gowdru Lingaraju Date: Mon, 24 Nov 2025 17:48:09 -0500 Subject: [PATCH 4/4] chore: bump strands-agents version --- pyproject.toml | 2 +- uv.lock | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6e25421..2e13519 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,7 +144,7 @@ dev = [ "pytest-cov>=6.0.0", "ruff>=0.12.0", "wheel>=0.45.1", - "strands-agents>=1.1.0", + "strands-agents>=1.18.0", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 57549e4..4dae85f 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" [[package]] @@ -100,7 +100,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=0.24.0" }, { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "ruff", specifier = ">=0.12.0" }, - { name = "strands-agents", specifier = ">=1.1.0" }, + { name = "strands-agents", specifier = ">=1.18.0" }, { name = "wheel", specifier = ">=0.45.1" }, ] @@ -1381,12 +1381,13 @@ wheels = [ [[package]] name = "strands-agents" -version = "1.1.0" +version = "1.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boto3" }, { name = "botocore" }, { name = "docstring-parser" }, + { name = "jsonschema" }, { name = "mcp" }, { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation-threading" }, @@ -1395,9 +1396,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "watchdog" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/93/2b3603c08a94feebbe836b78b4a925b4fcdf7a4060c2ccd53b987239b532/strands_agents-1.1.0.tar.gz", hash = "sha256:bba7ad79a1b9e39986135e0907dad97b90e318ab3f3fb4f33742e12714ec6aa8", size = 230143, upload-time = "2025-07-24T21:13:57.305Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/58/08665fc8d5330fc32727793c39ad019f6cc3f75272ede3629dd67b4fe6a5/strands_agents-1.18.0.tar.gz", hash = "sha256:85bd251d50de5d441cc2578068b4656ca86bdbaa1764e292d56b0edf0cf54c52", size = 521216, upload-time = "2025-11-21T21:30:26.394Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/46/02ab678f266bd07d0f4ebd0f43499eabc02a832ea5d35c4f7a2b2bf770c8/strands_agents-1.1.0-py3-none-any.whl", hash = "sha256:183d0b6f7533d4bb3655cda1d7a8ab6189bffeb20184dd88b45881daf83dc5f5", size = 163806, upload-time = "2025-07-24T21:13:53.733Z" }, + { url = "https://files.pythonhosted.org/packages/56/37/79c63bf7ac194754748fc42b5c9fb8f964dc742d92c19052e6cc74fede39/strands_agents-1.18.0-py3-none-any.whl", hash = "sha256:c9cf9662b24344413d204a7ee60d3e3ab88791568e9f572020901eb3df9b6b5e", size = 255594, upload-time = "2025-11-21T21:30:24.662Z" }, ] [[package]]